You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 (RicSnapshotAllViewsToFileFeature → Rim3dView::captureSnapshot → caf::Viewer::snapshotImage → grabFramebuffer) 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::resizeEvent → recreateFbos + 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 docsdocs/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.
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 throughQOpenGLWidget::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.
--savesnapshotsCLI entry point only — no changes toRicfExportSnapshots'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 internalQOffscreenSurface, creates the FBO (explicitly handling never-resized hidden widgets), renders viapaintGL(), and reads back. The preconditionQt::AA_ShareOpenGLContextsis already set inRiaMain.cpp:125. So the entire existing chain (RicSnapshotAllViewsToFileFeature→Rim3dView::captureSnapshot→caf::Viewer::snapshotImage→grabFramebuffer) works with hidden windows.Software GL selection:
Qt::AA_UseSoftwareOpenGL(set before QApplication construction) makes Qt loadopengl32sw.dll(Mesa llvmpipe, already deployed to the build folder by windeployqt).LIBGL_ALWAYS_SOFTWARE=1forces system Mesa llvmpipe. With no display server at all, stock Qt cannot create a GL context — the documented answer isxvfb-run -a ResInsight --headless ....Steps
--headlessoption inApplicationLibCode/Application/Tools/RiaArgumentParser.cpp(near"console", ~line 73) so the unknown-option check inRiaMain.cpp:152-169accepts it. Help text: runs without showing windows using software OpenGL rendering; combine with--savesnapshots; on Linux without a display server run underxvfb-run.--headlesspre-Qt increateApplication()inApplicationExeCode/RiaMain.cpp:83-101(same raw-argv scan pattern as--console). If found in GUI mode: setQApplication::setAttribute( Qt::AA_UseSoftwareOpenGL ), on non-Windowsqputenv( "LIBGL_ALWAYS_SOFTWARE", "1" ), and call newRiaGuiApplication::enableHeadlessMode()before constructing the application object. Do not touchQT_QPA_PLATFORMor theAA_ShareOpenGLContextsline.RiaGuiApplication(.h/.cpp):static void enableHeadlessMode(); static bool isHeadless();backed bystatic bool sm_headlessMode = false;RiaGuiApplication.cppwithisHeadless():initialize()(~:523): disable progress dialogs viacaf::ProgressInfoStatic::setEnabled( false )(cafProgressInfo.h:109)createMainWindow()(:1080): guard:1094) — harmless on a hidden parentm_mainWindow->showWindow(); keep the probeQOpenGLWidget(getOrCreateAndShowMainWindow()(~:1028): guard theshow()callgetOrCreateAndShowMainPlotWindow()(~:1116): guard theshowNormal()/show()/raise()/activateWindow()blockonProjectOpened()(~:1361): guardm_mainPlotWindow->show()/raise()and theshowPlotWindowOnTopblock; keepprocessEvents(),initializeGuiNewProjectLoaded()and dock-state restore so viewer widgets still get created (RimDockWindowController::updateViewerWidgetrequiresRiaGuiApplication::isRunning(), which stays true)handleArguments()savesnapshots block (~:897-920): guardmainPlotWnd->show()/raise()andmainWnd->show()/raise(); keep bothprocessEvents()calls and the trailingloadWinGeoAndDockToolBarLayout()callsshowFormattedTextInMessageBoxOrConsole()(~:1270): headless → print tostd::coutand return (mirrorRiaConsoleApplication.cpp:421) so--help/bad args don't block on a modal dialogaskUserToSaveModifiedProject()(~:278): headless →return true;(main callscloseProject()afterEXIT_COMPLETED; a dialog would hang the batch)useShaders()(~:499): no change needed (caf::Viewer::isShadersSupported()returns true unconditionally, no GL access)Rim3dView::captureSnapshot()(ApplicationLibCode/ProjectDataModel/Rim3dView.cpp:524-551): hidden widgets defer resize events (Qt::WA_PendingResizeEvent), so aftersetFixedSize(...)the layout never propagates the new size to the GL widget andgrabFramebuffer()would render at a stale size. If!m_viewer->layoutWidget()->isVisible(), deliver a synchronousQResizeEventtolayoutWidget()viaQCoreApplication::sendEventso the layout runs immediately. No change to the visible path.opengl32.dll(Windows) — discovered during implementation: VizFwk links statically against systemopengl32.dlland callsglGetError/glGetStringdirectly, whileQt::AA_UseSoftwareOpenGLcreates the context insideopengl32sw.dll. From system opengl32's view no context is current, andcvf_check_ogl'swhile (glGetError() != GL_NO_ERROR)loops forever (cvfOpenGL.cpp:172-182even warns about this). Fix:/DELAYLOAD:opengl32.dll+delayimpon the ResInsight target (ApplicationExeCode/CMakeLists.txt) and a__pfnDliNotifyHook2hook inRiaMain.cppthat resolvesopengl32.dlltoopengl32sw.dllwhen headless. Non-headless runs resolve normally — behavior unchanged.caf::Viewer::snapshotImage()(Fwk/AppFwk/cafViewer/cafViewer.cpp) — discovered during implementation: Qt never callsresizeGL()for a widget that has never been shown (its own grab-FBO gets the right size, butcaf::Viewer's offscreen scene FBO stays at its initial 1×1, producing an all-black image). Fix: when!isVisible(), do a firstgrabFramebuffer()to initialize GL, then send a synchronousQResizeEventto the viewer (which triggersQOpenGLWidget::resizeEvent→recreateFbos+resizeGL), then grab the final image.RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder(ApplicationLibCode/Commands/ExportCommands/RicSnapshotAllViewsToFileFeature.cpp, after the loop at ~:150):RiaLogging::infowith OpenGL version/vendor/renderer fromcaf::Viewer::contextGroup()->info()(cvfOpenGLContextGroup.h:75). Must be after the loop — with hidden widgets the GL context is first created inside the firstgrabFramebuffer(). This is the proof the software path (llvmpipe) is in use.buildfolder) — verified withTestModels/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 showsOpenGL used for snapshots: 3.0 Mesa 11.2.2 / VMware, Inc. / Gallium 0.4 on llvmpipe--headless(GPU) → 99% of sampled pixels identical (differences are text antialiasing)--headless --help→ help text to console, exit 0, no dialogopengl32sw.dllinApplicationExeCode/CMakeLists.txt: the windeployqt post-build step (:394-402) already copies it to the build tree, but:405-410) only scans link dependencies and misses dynamically loaded DLLs. Add inside theinstall(TARGETS ... RUNTIME_DEPENDENCIES ...)(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).docs/class-diagrams/ResInsightHeadlessSnapshots.txt: replace stale Qt 5 notes with--headlessusage for Windows/Linux,xvfb-run -afor display-less Linux, note that-platform offscreenstill works for plots-only projects and that the log line confirms llvmpipe.xvfb-run -aproduces PNGs. — verified on Ubuntu 24.04 / Qt 6.11 / Mesa 25.2.8 llvmpipe, see comment below.cpp/.hfiles 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: explicitmakeCurrent()+ render sequence +glReadPixelsfromcaf::Viewer's existingm_offscreenFbo(cafViewer.cpp:214-233) — not implemented now.RiaGuiApplicationcould hang a batch run; known ones are handled, others fixed as encountered.Links
handleArgumentssavesnapshots block → RicSnapshotAllViewsToFileFeature.cpp → Rim3dView.cppcaptureSnapshot→ cafViewer.cppsnapshotImage