#15098: Add headless mode with software OpenGL rendering

This commit is contained in:
Magne Sjaastad committed 2026-11-03 15:23:31 +01:00
1 parent 7b5a977401
commit 278248b71d
10 files changed
+280 -35

No files matched your search

+16
View File
@@ -239,6 +239,14 @@ if(UNIX AND NOT APPLE)
target_link_libraries(ResInsight PRIVATE xcb)
endif()
if(MSVC)
# Delay-load opengl32.dll so that the statically linked OpenGL calls can be
# redirected to the software rasterizer opengl32sw.dll in headless mode, see
# riaDelayLoadHook in RiaMainTools.cpp
target_link_libraries(ResInsight PRIVATE delayimp)
target_link_options(ResInsight PRIVATE /DELAYLOAD:opengl32.dll)
endif()
# ##############################################################################
# Unity builds
# ##############################################################################
@@ -421,6 +429,14 @@ if(RESINSIGHT_PRIVATE_INSTALL)
RUNTIME_DEPENDENCIES PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-"
POST_EXCLUDE_REGEXES ".*system32/.*\\.dll"
)
# The software OpenGL rasterizer is loaded dynamically (used by --headless),
# so it is not picked up by the RUNTIME_DEPENDENCIES scan above
install(
FILES $<TARGET_FILE_DIR:ResInsight>/opengl32sw.dll
DESTINATION ${RESINSIGHT_INSTALL_FOLDER}
OPTIONAL
)
endif()
if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+12
View File
@@ -86,6 +86,7 @@ void riaFilteredMessageHandler( QtMsgType type, const QMessageLogContext& contex
RiaApplication* createApplication( int& argc, char* argv[] )
{
bool isHeadless = false;
for ( int i = 1; i < argc; ++i )
{
if ( !qstrcmp( argv[i], "--console" ) || !qstrcmp( argv[i], "--unittest" ) )
@@ -96,7 +97,18 @@ RiaApplication* createApplication( int& argc, char* argv[] )
return new RiaConsoleApplication( argc, argv );
#endif
}
if ( !qstrcmp( argv[i], "--headless" ) )
{
isHeadless = true;
}
}
if ( isHeadless )
{
RiaMainTools::enableHeadlessRendering();
}
#ifdef ENABLE_GRPC
return new RiaGrpcGuiApplication( argc, argv );
#else
+90
View File
@@ -19,6 +19,7 @@
#include "RiaMainTools.h"
#include "RiaBaseDefs.h"
#include "RiaFileLogger.h"
#include "RiaGuiApplication.h"
#include "RiaLogging.h"
#include "RiaOpenTelemetryManager.h"
#include "RiaPreferencesOpenTelemetry.h"
@@ -41,6 +42,7 @@
#include <cstdlib>
#include <deque>
#include <exception>
#include <iostream>
#include <map>
#include <sstream>
#include <thread>
@@ -73,6 +75,74 @@
#endif
#endif
#ifdef WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <delayimp.h>
#endif
#ifdef WIN32
//--------------------------------------------------------------------------------------------------
/// In headless mode Qt creates its OpenGL context using the software rasterizer opengl32sw.dll
/// (Qt::AA_UseSoftwareOpenGL), while the statically linked OpenGL calls in the visualization
/// framework would go to the system opengl32.dll where no context is current, causing an endless
/// loop in glGetError(). opengl32.dll is therefore delay-loaded (see CMakeLists.txt) and redirected
/// here to the same software rasterizer.
//--------------------------------------------------------------------------------------------------
FARPROC WINAPI riaDelayLoadHook( unsigned dliNotify, PDelayLoadInfo pdli )
{
if ( dliNotify == dliNotePreLoadLibrary && RiaGuiApplication::isHeadless() &&
_stricmp( pdli->szDll, "opengl32.dll" ) == 0 )
{
if ( HMODULE softwareOpenGl = LoadLibraryW( L"opengl32sw.dll" ) )
{
return reinterpret_cast<FARPROC>( softwareOpenGl );
}
}
return nullptr;
}
extern "C" const PfnDliHook __pfnDliNotifyHook2 = riaDelayLoadHook;
//--------------------------------------------------------------------------------------------------
/// ResInsight is built as a GUI application on Windows, so stdout is only available when redirected to a file or
/// pipe. Attach to the console of the launching process (e.g. PowerShell or cmd) so headless log output is visible.
//--------------------------------------------------------------------------------------------------
void riaAttachToParentConsole()
{
// Only files and pipes are real redirections. A GUI process launched from a console (e.g. Windows Terminal) can
// inherit the console handles of the parent without being attached to the console, and writing to those blocks
auto isRedirected = []( DWORD stdHandle )
{
HANDLE handle = GetStdHandle( stdHandle );
if ( handle == nullptr || handle == INVALID_HANDLE_VALUE ) return false;
const DWORD fileType = GetFileType( handle );
return fileType == FILE_TYPE_DISK || fileType == FILE_TYPE_PIPE;
};
const bool stdOutRedirected = isRedirected( STD_OUTPUT_HANDLE );
const bool stdErrRedirected = isRedirected( STD_ERROR_HANDLE );
if ( stdOutRedirected && stdErrRedirected ) return;
if ( !AttachConsole( ATTACH_PARENT_PROCESS ) ) return;
FILE* stream = nullptr;
if ( !stdOutRedirected ) freopen_s( &stream, "CONOUT$", "w", stdout );
if ( !stdErrRedirected ) freopen_s( &stream, "CONOUT$", "w", stderr );
std::cout.clear();
std::cerr.clear();
}
#endif
namespace internal
{
#if RIA_HAS_STD_STACKTRACE
@@ -516,4 +586,24 @@ void deleteStaleSettingsLockFiles()
}
}
//--------------------------------------------------------------------------------------------------
/// Configure software OpenGL rendering for headless mode, allowing 3D views to be rendered and
/// grabbed without a GPU and without showing any windows. Must be called before the QApplication
/// object is created.
//--------------------------------------------------------------------------------------------------
void enableHeadlessRendering()
{
// Use software OpenGL rendering so 3D views can be rendered without a GPU. On Windows this
// makes Qt load opengl32sw.dll (Mesa llvmpipe). Must be set before the QApplication exists.
QApplication::setAttribute( Qt::AA_UseSoftwareOpenGL );
#ifndef WIN32
// On Linux the attribute has no effect, force Mesa software rendering instead
qputenv( "LIBGL_ALWAYS_SOFTWARE", "1" );
#else
riaAttachToParentConsole();
#endif
RiaGuiApplication::enableHeadlessMode();
}
} // namespace RiaMainTools
+1
View File
@@ -26,6 +26,7 @@ namespace RiaMainTools
void initializeSingletons();
void releaseSingletonAndFactoryObjects();
void deleteStaleSettingsLockFiles();
void enableHeadlessRendering();
// True once crash logging has started. The crash handler pumps the Qt event loop while flushing
// telemetry, so timer-driven application logic must check this flag and stay idle to avoid
@@ -30,6 +30,7 @@
#include "RiaBaseDefs.h"
#include "RiaFilePathTools.h"
#include "RiaFontCache.h"
#include "RiaGuiApplication.h"
#include "RiaImportEclipseCaseTools.h"
#include "RiaLogging.h"
#include "RiaNetworkTools.h"
@@ -1555,7 +1556,8 @@ void RiaApplication::applyPreferences()
project()->rootPlotTemplateItem()->updateConnectedEditors();
}
caf::ProgressInfoStatic::setEnabled( RiaPreferencesSystem::current()->showProgressBar() );
// No windows are shown in headless mode, so progress dialogs must stay hidden independent of preferences
caf::ProgressInfoStatic::setEnabled( !RiaGuiApplication::isHeadless() && RiaPreferencesSystem::current()->showProgressBar() );
if ( m_preferencesFileName.isEmpty() )
{
@@ -158,6 +158,8 @@
///
//==================================================================================================
bool RiaGuiApplication::sm_headlessMode = false;
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -166,6 +168,22 @@ bool RiaGuiApplication::isRunning()
return dynamic_cast<RiaGuiApplication*>( RiaApplication::instance() ) != nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaGuiApplication::enableHeadlessMode()
{
sm_headlessMode = true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaGuiApplication::isHeadless()
{
return sm_headlessMode;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -284,6 +302,9 @@ QString RiaGuiApplication::promptForProjectSaveAsFileName() const
//--------------------------------------------------------------------------------------------------
bool RiaGuiApplication::askUserToSaveModifiedProject()
{
// A modal dialog would hang a headless batch run
if ( isHeadless() ) return true;
if ( RiaPreferencesSystem::current()->showProjectChangedDialog() && caf::PdmUiModelChangeDetector::instance()->isModelChanged() )
{
QMessageBox msgBox( activeMainWindow() );
@@ -549,6 +570,9 @@ void RiaGuiApplication::initialize()
{
RiaApplication::initialize();
// Progress dialogs would pop up windows and can block a headless batch run
if ( isHeadless() ) caf::ProgressInfoStatic::setEnabled( false );
applyGuiPreferences( nullptr );
// Create main windows
@@ -576,6 +600,23 @@ void RiaGuiApplication::initialize()
RiaLogging::appendLoggerInstance( std::move( logger ) );
}
if ( isHeadless() )
{
// The message panels are never shown in headless mode, so also write to stdout to make batch runs diagnosable
auto stdLogger = std::make_unique<RiaStdOutLogger>();
if ( m_logLevelFromCommandLine.has_value() )
{
stdLogger->setLevel( m_logLevelFromCommandLine.value() );
}
else
{
stdLogger->setLevel( int( RiaLogging::logLevelBasedOnPreferences() ) );
}
RiaLogging::appendLoggerInstance( std::move( stdLogger ) );
}
m_socketServer = new RiaSocketServer( this );
}
@@ -671,10 +712,28 @@ RiaApplication::ApplicationStatus RiaGuiApplication::handleArguments( cvf::Progr
// Use a logger writing to stdout instead of message panel
// This is useful when executing regression tests on a build server, and this is the reason for creating the
// logger when parsing the command line options
auto stdLogger = std::make_unique<RiaStdOutLogger>();
stdLogger->setLevel( int( RILogLevel::RI_LL_DEBUG ) );
// In headless mode a stdout logger already exists, reuse it to avoid duplicated log lines
RiaStdOutLogger* existingStdLogger = nullptr;
for ( auto logger : RiaLogging::loggerInstances() )
{
if ( auto candidate = dynamic_cast<RiaStdOutLogger*>( logger ) )
{
existingStdLogger = candidate;
break;
}
}
RiaLogging::appendLoggerInstance( std::move( stdLogger ) );
if ( existingStdLogger )
{
existingStdLogger->setLevel( int( RILogLevel::RI_LL_DEBUG ) );
}
else
{
auto stdLogger = std::make_unique<RiaStdOutLogger>();
stdLogger->setLevel( int( RILogLevel::RI_LL_DEBUG ) );
RiaLogging::appendLoggerInstance( std::move( stdLogger ) );
}
RiaRegressionTestRunner::instance()->executeRegressionTests( regressionTestPath, QStringList() );
return ApplicationStatus::EXIT_COMPLETED;
@@ -926,29 +985,53 @@ RiaApplication::ApplicationStatus RiaGuiApplication::handleArguments( cvf::Progr
snapshotFolder = snapshotFolderFromCommandLine;
}
QStringList snapshotErrors;
if ( snapshotPlots )
{
auto mainPlotWnd = mainPlotWindow();
if ( mainPlotWnd )
{
mainPlotWnd->show();
mainPlotWnd->raise();
if ( !isHeadless() )
{
mainPlotWnd->show();
mainPlotWnd->raise();
}
processEvents();
RicSnapshotAllPlotsToFileFeature::exportSnapshotOfPlotsIntoFolder( snapshotFolder, snapshotWidth, snapshotHeight );
auto result =
RicSnapshotAllPlotsToFileFeature::exportSnapshotOfPlotsIntoFolder( snapshotFolder, snapshotWidth, snapshotHeight );
if ( !result ) snapshotErrors.push_back( result.error() );
}
else
{
// The plot window is only created when the project shows plots, so there is nothing to export
RiaLogging::warning( "No plot window available, no plot snapshots exported" );
}
}
if ( snapshotViews )
{
auto mainWnd = RiuMainWindow::instance();
mainWnd->show();
mainWnd->raise();
if ( mainWnd )
{
if ( !isHeadless() )
{
mainWnd->show();
mainWnd->raise();
}
processEvents();
processEvents();
RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( snapshotFolder, snapshotWidth, snapshotHeight );
auto result =
RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( snapshotFolder, snapshotWidth, snapshotHeight );
if ( !result ) snapshotErrors.push_back( result.error() );
}
else
{
snapshotErrors.push_back( "No main window available for view snapshot export" );
}
}
auto mainPlotWnd = mainPlotWindow();
@@ -959,6 +1042,14 @@ RiaApplication::ApplicationStatus RiaGuiApplication::handleArguments( cvf::Progr
if ( RiuMainWindow::instance() ) RiuMainWindow::instance()->loadWinGeoAndDockToolBarLayout();
if ( !snapshotErrors.empty() )
{
// Report a non-zero exit code, as a batch run with missing images is otherwise indistinguishable from success
RiaLogging::error( snapshotErrors.join( ". " ).toStdString() );
return ApplicationStatus::EXIT_WITH_ERROR;
}
return ApplicationStatus::EXIT_COMPLETED;
}
@@ -1063,7 +1154,7 @@ RiuMainWindow* RiaGuiApplication::getOrCreateAndShowMainWindow()
{
createMainWindow();
}
else
else if ( !isHeadless() )
{
m_mainWindow->show();
@@ -1141,7 +1232,7 @@ void RiaGuiApplication::createMainWindow()
m_mainWindow->setDefaultWindowSize();
m_mainWindow->setDefaultToolbarVisibility();
m_mainWindow->loadWinGeoAndDockToolBarLayout();
m_mainWindow->showWindow();
if ( !isHeadless() ) m_mainWindow->showWindow();
// if there is an existing logger, reconnect to it
@@ -1199,18 +1290,21 @@ RiuPlotMainWindow* RiaGuiApplication::getOrCreateAndShowMainPlotWindow()
}
}
if ( m_mainPlotWindow->isMinimized() )
if ( !isHeadless() )
{
m_mainPlotWindow->showNormal();
m_mainPlotWindow->update();
}
else
{
m_mainPlotWindow->show();
}
if ( m_mainPlotWindow->isMinimized() )
{
m_mainPlotWindow->showNormal();
m_mainPlotWindow->update();
}
else
{
m_mainPlotWindow->show();
}
m_mainPlotWindow->raise();
m_mainPlotWindow->activateWindow();
m_mainPlotWindow->raise();
m_mainPlotWindow->activateWindow();
}
return m_mainPlotWindow.get();
}
@@ -1339,6 +1433,19 @@ void RiaGuiApplication::clearAllSelections()
//--------------------------------------------------------------------------------------------------
void RiaGuiApplication::showFormattedTextInMessageBoxOrConsole( const QString& text )
{
// A modal dialog would hang a headless batch run
if ( isHeadless() )
{
const std::string stdText = text.toStdString();
std::cout << stdText;
// Make sure the text ends with a newline and reaches the console even if the application aborts later
if ( stdText.empty() || stdText.back() != '\n' ) std::cout << '\n';
std::cout.flush();
return;
}
// Create a message dialog with cut/paste friendly text
QDialog dlg( widgetToUseAsParent() );
dlg.setModal( true );
@@ -1444,9 +1551,9 @@ void RiaGuiApplication::onProjectOpened()
if ( !m_mainPlotWindow )
{
createMainPlotWindow();
m_mainPlotWindow->show();
if ( !isHeadless() ) m_mainPlotWindow->show();
}
else
else if ( !isHeadless() )
{
m_mainPlotWindow->show();
m_mainPlotWindow->raise();
@@ -1478,7 +1585,7 @@ void RiaGuiApplication::onProjectOpened()
// Make sure to process events before this function to avoid strange Qt crash
RiuPlotMainWindowTools::refreshToolbars();
if ( m_project->showPlotWindow() && m_project->showPlotWindowOnTop() )
if ( m_project->showPlotWindow() && m_project->showPlotWindowOnTop() && !isHeadless() )
{
m_mainPlotWindow->raise();
m_mainPlotWindow->activateWindow();
@@ -1873,7 +1980,8 @@ void RiaGuiApplication::runMultiCaseSnapshots( const QString& templateProj
bool loadOk = loadProject( templateProjectFileName, ProjectLoadAction::PLA_NONE, &modifier );
if ( loadOk )
{
RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( snapshotFolderName );
auto result = RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( snapshotFolderName );
if ( !result ) RiaLogging::error( result.error().toStdString() );
}
}
m_mainWindow->dockManager()->restoreState( curState );
@@ -82,6 +82,9 @@ public:
static bool isRunning();
static RiaGuiApplication* instance();
static void enableHeadlessMode();
static bool isHeadless();
RiaGuiApplication( int& argc, char** argv );
~RiaGuiApplication() override;
@@ -172,4 +175,6 @@ private:
QPointer<RiuPlotMainWindow> m_mainPlotWindow;
std::unique_ptr<RiuRecentFileActionProvider> m_recentFileActionProvider;
static bool sm_headlessMode;
};
@@ -71,6 +71,11 @@ bool RiaArgumentParser::parseArguments( cvf::ProgramOptions* progOpt )
cvf::ProgramOptions::MULTI_VALUE );
progOpt->registerOption( "size", "<width> <height>", "Set size of the main application window.", cvf::ProgramOptions::MULTI_VALUE );
progOpt->registerOption( "console", "", "Launch as a console application without graphics" );
progOpt->registerOption( "headless",
"",
"Run without showing any windows, using software OpenGL rendering. "
"Combine with --savesnapshots to export images on machines without a GPU. "
"On Linux without a display server, run under xvfb-run." );
progOpt->registerOption( "server", "[<portnumber>]", "Launch as a GRPC server. Default port is 50051", cvf::ProgramOptions::SINGLE_VALUE );
progOpt->registerOption( "portnumberfile", "[<filename>]", "Write the port number to this file.", cvf::ProgramOptions::SINGLE_VALUE );
@@ -250,9 +250,10 @@ void RiaRegressionTestRunner::runRegressionTest()
if ( regressionTestConfig.exportSnapshots3dViews )
{
QSize defaultSize = RiaRegressionTestRunner::regressionDefaultImageSize();
RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( fullPathGeneratedFolder,
defaultSize.width(),
defaultSize.height() );
auto result = RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( fullPathGeneratedFolder,
defaultSize.width(),
defaultSize.height() );
if ( !result ) RiaLogging::error( result.error().toStdString() );
QApplication::processEvents();
}
@@ -260,10 +261,11 @@ void RiaRegressionTestRunner::runRegressionTest()
if ( regressionTestConfig.exportSnapshotsPlots )
{
QSize defaultSize = RiaRegressionTestRunner::regressionDefaultImageSize();
RicSnapshotAllPlotsToFileFeature::exportSnapshotOfPlotsIntoFolder( fullPathGeneratedFolder,
defaultSize.width(),
defaultSize.height(),
true /*activate widget*/ );
auto result = RicSnapshotAllPlotsToFileFeature::exportSnapshotOfPlotsIntoFolder( fullPathGeneratedFolder,
defaultSize.width(),
defaultSize.height(),
true /*activate widget*/ );
if ( !result ) RiaLogging::error( result.error().toStdString() );
}
uint64_t usedMemoryBeforeClose = caf::MemoryInspector::getApplicationPhysicalMemoryUsageMiB();
@@ -1788,7 +1788,11 @@ void RiuMainWindow::slotSnapshotAllViewsToFile()
// Save images in snapshot catalog relative to project directory
QString absolutePathToSnapshotDir = app->createAbsolutePathFromProjectRelativePath( "snapshots" );
RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( absolutePathToSnapshotDir );
if ( auto result = RicSnapshotAllViewsToFileFeature::exportSnapshotOfViewsIntoFolder( absolutePathToSnapshotDir ); !result )
{
RiaLogging::error( result.error().toStdString() );
}
}
//--------------------------------------------------------------------------------------------------