Skip to content

Add headless mode with software OpenGL rendering for 3D snapshots #1015

Description

@magnesj

Description

ResInsight can currently only export 3D view snapshots from the GUI application with visible windows and a working GPU/display. The documented headless path (-platform offscreen) works for 2D plots only — projects containing 3D views crash because snapshots go through QOpenGLWidget::grabFramebuffer(), and the offscreen QPA plugin cannot create OpenGL contexts (no WGL backend on Windows, GLX requires an X display on Linux). Console mode (--console) silently ignores all snapshot options.

Goal: ResInsight --headless --project x.rsp --savesnapshots all --snapshotsize 1000 745 [--snapshotfolder <dir>] renders 3D views with software OpenGL (no GPU needed), writes PNGs, and exits — without showing any windows.

Scope: Keep the existing widget-based GUI pipeline (minimal diff). Windows + Linux. --savesnapshots CLI entry point only — no changes to RicfExportSnapshots's GUI gate or the regression test runner.

Strategy

Keep the native QPA platform ("windows"/"xcb"), never show any window, and force software OpenGL. Do not use QT_QPA_PLATFORM=offscreen (no OpenGL support on Windows at all).

Why this works: in Qt 6, QOpenGLWidget::grabFramebuffer() does not require the widget to be shown — it initializes the widget's GL context on an internal QOffscreenSurface, creates the FBO (explicitly handling never-resized hidden widgets), renders via paintGL(), and reads back. The precondition Qt::AA_ShareOpenGLContexts is already set in RiaMain.cpp:125. So the entire existing chain (RicSnapshotAllViewsToFileFeatureRim3dView::captureSnapshotcaf::Viewer::snapshotImagegrabFramebuffer) works with hidden windows.

Software GL selection:

  • Windows: Qt::AA_UseSoftwareOpenGL (set before QApplication construction) makes Qt load opengl32sw.dll (Mesa llvmpipe, already deployed to the build folder by windeployqt).
  • Linux: LIBGL_ALWAYS_SOFTWARE=1 forces system Mesa llvmpipe. With no display server at all, stock Qt cannot create a GL context — the documented answer is xvfb-run -a ResInsight --headless ....

Steps

  • Register --headless option in ApplicationLibCode/Application/Tools/RiaArgumentParser.cpp (near "console", ~line 73) so the unknown-option check in RiaMain.cpp:152-169 accepts it. Help text: runs without showing windows using software OpenGL rendering; combine with --savesnapshots; on Linux without a display server run under xvfb-run.
  • Detect --headless pre-Qt in createApplication() in ApplicationExeCode/RiaMain.cpp:83-101 (same raw-argv scan pattern as --console). If found in GUI mode: set QApplication::setAttribute( Qt::AA_UseSoftwareOpenGL ), on non-Windows qputenv( "LIBGL_ALWAYS_SOFTWARE", "1" ), and call new RiaGuiApplication::enableHeadlessMode() before constructing the application object. Do not touch QT_QPA_PLATFORM or the AA_ShareOpenGLContexts line.
  • Add headless state to RiaGuiApplication (.h/.cpp): static void enableHeadlessMode(); static bool isHeadless(); backed by static bool sm_headlessMode = false;
  • Guard show/raise/dialog calls in RiaGuiApplication.cpp with isHeadless():
    • initialize() (~:523): disable progress dialogs via caf::ProgressInfoStatic::setEnabled( false ) (cafProgressInfo.h:109)
    • createMainWindow() (:1080): guard m_mainWindow->showWindow(); keep the probe QOpenGLWidget (:1094) — harmless on a hidden parent
    • getOrCreateAndShowMainWindow() (~:1028): guard the show() call
    • getOrCreateAndShowMainPlotWindow() (~:1116): guard the showNormal()/show()/raise()/activateWindow() block
    • onProjectOpened() (~:1361): guard m_mainPlotWindow->show()/raise() and the showPlotWindowOnTop block; keep processEvents(), initializeGuiNewProjectLoaded() and dock-state restore so viewer widgets still get created (RimDockWindowController::updateViewerWidget requires RiaGuiApplication::isRunning(), which stays true)
    • handleArguments() savesnapshots block (~:897-920): guard mainPlotWnd->show()/raise() and mainWnd->show()/raise(); keep both processEvents() calls and the trailing loadWinGeoAndDockToolBarLayout() calls
    • showFormattedTextInMessageBoxOrConsole() (~:1270): headless → print to std::cout and return (mirror RiaConsoleApplication.cpp:421) so --help/bad args don't block on a modal dialog
    • askUserToSaveModifiedProject() (~:278): headless → return true; (main calls closeProject() after EXIT_COMPLETED; a dialog would hang the batch)
    • useShaders() (~:499): no change needed (caf::Viewer::isShadersSupported() returns true unconditionally, no GL access)
  • Hidden-widget resize fix in Rim3dView::captureSnapshot() (ApplicationLibCode/ProjectDataModel/Rim3dView.cpp:524-551): hidden widgets defer resize events (Qt::WA_PendingResizeEvent), so after setFixedSize(...) the layout never propagates the new size to the GL widget and grabFramebuffer() would render at a stale size. If !m_viewer->layoutWidget()->isVisible(), deliver a synchronous QResizeEvent to layoutWidget() via QCoreApplication::sendEvent so the layout runs immediately. No change to the visible path.
  • Delay-load redirect of opengl32.dll (Windows) — discovered during implementation: VizFwk links statically against system opengl32.dll and calls glGetError/glGetString directly, while Qt::AA_UseSoftwareOpenGL creates the context inside opengl32sw.dll. From system opengl32's view no context is current, and cvf_check_ogl's while (glGetError() != GL_NO_ERROR) loops forever (cvfOpenGL.cpp:172-182 even warns about this). Fix: /DELAYLOAD:opengl32.dll + delayimp on the ResInsight target (ApplicationExeCode/CMakeLists.txt) and a __pfnDliNotifyHook2 hook in RiaMain.cpp that resolves opengl32.dll to opengl32sw.dll when headless. Non-headless runs resolve normally — behavior unchanged.
  • Size the viewer's internal FBOs for never-shown widgets in caf::Viewer::snapshotImage() (Fwk/AppFwk/cafViewer/cafViewer.cpp) — discovered during implementation: Qt never calls resizeGL() for a widget that has never been shown (its own grab-FBO gets the right size, but caf::Viewer's offscreen scene FBO stays at its initial 1×1, producing an all-black image). Fix: when !isVisible(), do a first grabFramebuffer() to initialize GL, then send a synchronous QResizeEvent to the viewer (which triggers QOpenGLWidget::resizeEventrecreateFbos + resizeGL), then grab the final image.
  • Log the GL renderer in RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder (ApplicationLibCode/Commands/ExportCommands/RicSnapshotAllViewsToFileFeature.cpp, after the loop at ~:150): RiaLogging::info with OpenGL version/vendor/renderer from caf::Viewer::contextGroup()->info() (cvfOpenGLContextGroup.h:75). Must be after the loop — with hidden widgets the GL context is first created inside the first grabFramebuffer(). This is the proof the software path (llvmpipe) is in use.
  • Build and verify on Windows (ninja from build folder) — verified with TestModels/TEST10K_FLT_LGR_NNC/10KWithWellLog.rsp (one 3D view + two well log plots):
    • --headless --savesnapshots all --snapshotsize 1000 745 → no window, exit 0 in ~2.3 s, 3 PNGs at 1000×745, log shows OpenGL used for snapshots: 3.0 Mesa 11.2.2 / VMware, Inc. / Gallium 0.4 on llvmpipe
    • Same command without --headless (GPU) → 99% of sampled pixels identical (differences are text antialiasing)
    • --headless --help → help text to console, exit 0, no dialog
    • Plot snapshots (QWidget::grab path) written correctly in the same run
  • Install opengl32sw.dll in ApplicationExeCode/CMakeLists.txt: the windeployqt post-build step (:394-402) already copies it to the build tree, but install(TARGETS ... RUNTIME_DEPENDENCIES ...) (:405-410) only scans link dependencies and misses dynamically loaded DLLs. Add inside the if(WIN32 AND WINDEPLOYQT_EXECUTABLE) block: install(FILES $<TARGET_FILE_DIR:ResInsight>/opengl32sw.dll DESTINATION ${RESINSIGHT_INSTALL_FOLDER} OPTIONAL). Linux needs nothing shipped (system Mesa).
  • Update docs docs/class-diagrams/ResInsightHeadlessSnapshots.txt: replace stale Qt 5 notes with --headless usage for Windows/Linux, xvfb-run -a for display-less Linux, note that -platform offscreen still works for plots-only projects and that the log line confirms llvmpipe.
  • Linux verification (CI or Linux box): desktop run shows llvmpipe in log; bare run without DISPLAY fails fast (documented); xvfb-run -a produces PNGs. — verified on Ubuntu 24.04 / Qt 6.11 / Mesa 25.2.8 llvmpipe, see comment below
  • Run clang-format on all changed .cpp/.h files before committing.

Risks

  • grabFramebuffer() on never-shown widgets: high confidence (stable Qt behavior since 5.4, explicit hidden-widget handling in Qt source). Fallback if it fails: explicit makeCurrent() + render sequence + glReadPixels from caf::Viewer's existing m_offscreenFbo (cafViewer.cpp:214-233) — not implemented now.
  • Stray modal dialogs from loaders outside RiaGuiApplication could hang a batch run; known ones are handled, others fixed as encountered.
  • llvmpipe renders slowly on large models and output may differ slightly from GPU renders (AA/line widths) — acceptable for batch use.
  • Windows service sessions (no interactive desktop) are untested; document as unsupported if they fail.

Links

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions