diff --git a/.agents/rules/build-project.md b/.agents/rules/build-project.md index 56eba1841..f2d8f63f2 100644 --- a/.agents/rules/build-project.md +++ b/.agents/rules/build-project.md @@ -48,6 +48,16 @@ optimization, benchmarking, or validating runtime speed. Do not reuse a gmake tr The debug/unit-test workflow below is additional correctness validation. It does not replace the release Ninja build required for performance work. +## Benchmark Runtime + +Run the release benchmark harness with the native runtime on a usable desktop: +`EEPP_RUNTIME=native bin/benchmarks/eepp-benchmarks`. +The benchmarks that create windows explicitly request `WindowStyle::Hidden`; other benchmarks +do not create windows. They can use the native renderer without showing desktop windows or taking +focus. Do not switch this harness to headless merely to avoid interrupting the user. Verify the +renderer before reporting hardware rendering performance; headless and Xvfb runs can use software +rendering and measure a different execution path. + ## Debug and Unit-Test Builds All build commands must be executed from the **root project directory**. On macOS, follow the @@ -117,6 +127,13 @@ Run the following command, replacing `` with the correct environment: ## Running GUI Examples Under Xvfb +For ordinary application/example launches and UI inspector automation, try `EEPP_RUNTIME=headless` +first to avoid desktop windows and input interference without requiring Xvfb. This preference does +not apply to the unit-test and benchmark harnesses: their windows are already hidden, so use the +native runtime on a usable desktop. Use the native/Xvfb paths below for examples only when the +required check cannot run with the headless runtime. Verify the renderer before using an offscreen +run to assess hardware rendering or performance. + Xvfb does not support the multisampled OpenGL contexts requested by some eepp examples. In particular, `src/examples/ui_html/ui_html.cpp` normally requests 4x MSAA. Launching that binary through `xvfb-run` or `projects/scripts/xvfb-run-eepp` can therefore fail immediately with diff --git a/.agents/rules/project-introduction.md b/.agents/rules/project-introduction.md index 90d3f7b56..03bd56f0b 100644 --- a/.agents/rules/project-introduction.md +++ b/.agents/rules/project-introduction.md @@ -23,6 +23,24 @@ not `eTerm`. Its implementation lives under `src/modules/eterm/`. `eeiv` is the repository's image viewer. Its application entry point is `src/tools/eeiv/eeiv.cpp`. +## AI Agent Runtime Checks + +AI agents should prefer `EEPP_RUNTIME=headless` when launching ordinary applications or examples +and when automating their UI with the inspector. This avoids opening desktop windows or interrupting +the human's keyboard input and focus. Coordinate interactive checks that require visible native +windows with the user before driving them. + +Unit tests and benchmarks are different: run their release binaries with the native runtime on a +usable desktop. The unit-test build defines `EE_UNIT_TESTS`, which adds `WindowStyle::Hidden` to +the default window style; benchmarks that create windows explicitly request `WindowStyle::Hidden`. +These hidden windows preserve native rendering without appearing or taking desktop focus. Keep +test and benchmark windows hidden when adding or changing cases. Do not select the headless runtime +solely to avoid desktop interruption for these harnesses. + +Use headless tests when no usable display is available, when requested, or when testing the headless +runtime itself. Headless rendering can use a software renderer, so verify the renderer before +assessing GPU behavior or performance. See `.agents/rules/unit-tests.md` for commands and fallbacks. + ## Documentation & Code References When working on this project, rely on the following resources to understand existing implementations: diff --git a/.agents/rules/unit-tests.md b/.agents/rules/unit-tests.md index efbb096fb..473ddcd3e 100644 --- a/.agents/rules/unit-tests.md +++ b/.agents/rules/unit-tests.md @@ -8,44 +8,54 @@ The test binary manages its own current working directory, so you can execute it The required default workflow is: 1. Build the release unit-test target (use ninja build). -2. Run `bin/unit_tests/eepp-unit_tests` directly, without Xvfb. -3. If sandbox restrictions prevent access to the host display, retry that same direct command with - elevated permissions. -4. Use Xvfb only after direct execution has failed outside the sandbox because no usable graphical - display is available. Xvfb is the last fallback, not the default headless convenience path. +2. On a usable desktop, run `EEPP_RUNTIME=native bin/unit_tests/eepp-unit_tests` directly, without + Xvfb. The unit-test build defines `EE_UNIT_TESTS`, making `WindowStyle::Default` include + `WindowStyle::Hidden`; native test windows remain hidden and do not take desktop focus. +3. If sandbox restrictions prevent access to the host display, retry the same native command with + elevated permissions. A sandbox display failure does not establish that the host lacks a display. +4. When no usable display is available, use `EEPP_RUNTIME=headless bin/unit_tests/eepp-unit_tests`. + Also use it when requested or when checking the headless runtime itself. +5. Use Xvfb only if there is no usable desktop display and the required check cannot run with the + headless runtime. Keep the explicit runtime selection in fallback commands below. -Do not infer that a session is headless merely because its first sandboxed command cannot connect to -the display. A sandbox can hide or deny access to an otherwise usable host display. +The preference for headless application launches and interactive UI inspector checks does not +change this harness workflow. Keep all native test windows hidden, including secondary dialog +hosts; showing a UI widget inside a hidden host does not require showing the native window. * **Use the release test binary during normal development:** When AddressSanitizer or other debug-only diagnostics are not required, build and run `bin/unit_tests/eepp-unit_tests`. The optimized release suite is substantially faster and is the required default for iterative testing. Use `bin/unit_tests/eepp-unit_tests-debug` only when investigating memory safety, assertions, or other behavior that specifically requires the debug configuration. -* **Default Execution on a Graphical Linux Desktop:** - Unit-test windows are created hidden, so run the release suite directly against the desktop: - `bin/unit_tests/eepp-unit_tests` - This keeps the windows invisible and unfocused while preserving hardware OpenGL acceleration. +* **Default Execution on a Graphical Desktop:** + `EEPP_RUNTIME=native bin/unit_tests/eepp-unit_tests` + Hidden windows preserve native rendering without appearing or taking desktop focus. Confirm that the renderer log names the real GPU rather than llvmpipe when validating rendering behavior or performance. -* **Filtered Tests on a Graphical Linux Desktop:** - Use the same direct hardware-backed command for focused runs: - `bin/unit_tests/eepp-unit_tests --filter="FontRendering.*Offset*"` -* **Last Fallback for Headless CI and Systems Confirmed to Lack a Usable Desktop Display:** - Keep `projects/scripts/xvfb-run-eepp` as the fallback when no desktop display is available: - `projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests` +* **Filtered Tests:** + Use the same native runtime for focused runs on a usable desktop: + `EEPP_RUNTIME=native bin/unit_tests/eepp-unit_tests --filter="FontRendering.*Offset*"` +* **Headless Execution:** + `EEPP_RUNTIME=headless bin/unit_tests/eepp-unit_tests` + Use this when no usable desktop display is available, when requested, or for headless runtime + validation. It renders offscreen without Xvfb. It can use a software renderer, so its timings + do not necessarily represent hardware GPU performance. Add `--filter` for focused headless runs. +* **Last Fallback When the Headless Runtime Cannot Support the Required Check:** + Keep `projects/scripts/xvfb-run-eepp` as the fallback when no usable desktop display is available: + `EEPP_RUNTIME=native projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests` The wrapper provides a race-safe isolated display at `1280x1024x24` and injects `ASAN_OPTIONS=detect_leaks=0`. Xvfb can use llvmpipe, so do not use wrapper timings to evaluate hardware-rendered performance. * **Wrapper Fallback:** If `projects/scripts/xvfb-run-eepp` itself fails before launching the test binary, report that failure and use: - `xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests` + `EEPP_RUNTIME=native xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests` Do not use plain `xvfb-run` as the first headless attempt. * **Non-window Tests:** Tests known not to create windows can run without selecting a video driver: `bin/unit_tests/eepp-unit_tests --filter="NonWindowTest.*"` * **Filtering Tests:** Use the `--filter` parameter to run specific tests (supports glob patterns). - Keep the same harness as the full suite: direct execution on a graphical Linux desktop, or the - wrapper in a genuinely headless environment. + Keep the same runtime as the full suite for the current environment: native on a usable desktop, + headless when no usable display is available, or the Xvfb fallback when headless cannot support + the required check. ## Cross-Platform Compatibility (Required) diff --git a/src/tests/unit_tests/uiscenenode_tests.cpp b/src/tests/unit_tests/uiscenenode_tests.cpp index fba3dc977..6a897b6c6 100644 --- a/src/tests/unit_tests/uiscenenode_tests.cpp +++ b/src/tests/unit_tests/uiscenenode_tests.cpp @@ -779,13 +779,13 @@ UTEST( UIApplication, CreatesSecondaryWindowWithoutChangingAmbientScene ) { const Sizei primaryScreenSize = primaryWindow->getSizeInScreenCoordinates(); auto* fileDialog = UIFileDialog::NewInApplicationWindow( app, - WindowSettings( 640, 400, "File Dialog UI Context", - WindowStyle::Titlebar | WindowStyle::Resize, WindowBackend::Default, 32, {}, - 1, false, true ), + WindowSettings( 640, 400, "File Dialog UI Context", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), UIFileDialog::DefaultFlags, "*", FileSystem::getCurrentWorkingDirectory(), ContextSettings( false, 0, 0, GLv_default, true, false ), true, UIWindow::ApplicationWindowPosition::CenteredOnPrimary ); ASSERT_TRUE( fileDialog != nullptr ); + EXPECT_FALSE( fileDialog->getUISceneNode()->getWindow()->isVisible() ); EXPECT_EQ( app.getWindowCount(), 3u ); EXPECT_NE( fileDialog->getUISceneNode(), app.getUI() ); EXPECT_NE( fileDialog->getUISceneNode(), secondaryUI ); diff --git a/src/tools/eterm/appconfig.cpp b/src/tools/eterm/appconfig.cpp index 73b5af80d..d93f5d4f1 100644 --- a/src/tools/eterm/appconfig.cpp +++ b/src/tools/eterm/appconfig.cpp @@ -22,8 +22,9 @@ void AppConfig::load() { terminal.shell = mIni.getValue( "terminal", "shell", terminal.shell ); terminal.shellArguments = mIni.getValue( "terminal", "shell_arguments", terminal.shellArguments ); - terminal.workingDirectory = - mIni.getValue( "terminal", "working_directory", terminal.workingDirectory ); + // The legacy working_directory key also captured one-off command-line paths. + terminal.defaultWorkingDirectory = + mIni.getValue( "terminal", "default_working_directory", terminal.defaultWorkingDirectory ); terminal.executeInShell = mIni.getValue( "terminal", "execute_in_shell", terminal.executeInShell ); terminal.historySize = mIni.getValueU( "terminal", "scrollback", terminal.historySize ); @@ -99,7 +100,8 @@ bool AppConfig::savePreferences() { mIni.deleteValue( "window", "max_fps" ); mIni.setValue( "terminal", "shell", terminal.shell ); mIni.setValue( "terminal", "shell_arguments", terminal.shellArguments ); - mIni.setValue( "terminal", "working_directory", terminal.workingDirectory ); + mIni.deleteValue( "terminal", "working_directory" ); + mIni.setValue( "terminal", "default_working_directory", terminal.defaultWorkingDirectory ); mIni.setValue( "terminal", "execute_in_shell", terminal.executeInShell ); mIni.setValueU( "terminal", "scrollback", terminal.historySize ); mIni.setValue( "terminal", "cursor_style", diff --git a/src/tools/eterm/appconfig.hpp b/src/tools/eterm/appconfig.hpp index 1c1dd3369..cf5316462 100644 --- a/src/tools/eterm/appconfig.hpp +++ b/src/tools/eterm/appconfig.hpp @@ -25,7 +25,7 @@ enum class NewTerminalBehavior { CurrentTabBar, VerticalSplit, HorizontalSplit } struct TerminalConfig { std::string shell; std::string shellArguments; - std::string workingDirectory; + std::string defaultWorkingDirectory; std::string executeInShell; size_t historySize{ 10000 }; size_t initialTabs{ 1 }; diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index a04e3527d..5d5682a49 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -701,7 +701,8 @@ int App::run( int argc, char* argv[] ) { parser, "prefers-color-scheme", "Set the preferred color scheme (\"light\", \"dark\" or \"system\")", { 'c', "prefers-color-scheme" } ); - args::Positional wd( parser, "wording-dir", "Working Directory / executable" ); + args::Positional wd( parser, "working-dir", + "Working directory / executable for this launch only" ); args::Flag closeOnExit( parser, "close-on-exit", "close the application when the executable exits", { "close", "close-on-exit" } ); @@ -770,8 +771,6 @@ int App::run( int argc, char* argv[] ) { } config->theme.uiColorScheme = ColorSchemePreferences::fromStringExt( scheme ); } - if ( wd ) - config->terminal.workingDirectory = wd.Get(); config->terminal.executeInShell = executeInShell.Get(); config->theme.colorScheme = colorScheme.Get(); config->window.maxFPS = maxFPS.Get(); @@ -938,9 +937,14 @@ int App::run( int argc, char* argv[] ) { resourceScope.getFontService().addFallbackFont( std::move( fallback ) ); } - const std::string launchPath = config->terminal.workingDirectory.empty() - ? initialWorkingDirectory - : config->terminal.workingDirectory; + std::string launchPath = FileSystem::expandTilde( config->terminal.defaultWorkingDirectory ); + if ( wd ) { + launchPath = wd.Get(); + } else if ( launchPath.empty() || !FileSystem::isDirectory( launchPath ) ) { + if ( !launchPath.empty() ) + Log::warning( "Default working directory does not exist: %s", launchPath ); + launchPath = Sys::getUserDirectory(); + } FileInfo launchFile( launchPath ); const bool launchExecutable = launchFile.isRegularFile() && launchFile.isExecutable(); terminalConfig.program = launchExecutable ? launchFile.getFilepath() : config->terminal.shell; diff --git a/src/tools/eterm/settingspanel.cpp b/src/tools/eterm/settingspanel.cpp index d2e6c23fd..62bdb086e 100644 --- a/src/tools/eterm/settingspanel.cpp +++ b/src/tools/eterm/settingspanel.cpp @@ -307,6 +307,27 @@ UIWindow* SettingsPanel::create( App& app ) { return true; }, true ); + panel->addText( + { "terminalDefaultWorkingDirectory", "terminal.behavior", + app.i18n( "default_working_directory", "Default Working Directory" ), + app.i18n( "default_working_directory_desc", + "Set the starting directory for new terminals. Leave empty to use your home " + "directory. A command-line directory overrides it for that launch." ) }, + [&app] { return app.config->terminal.defaultWorkingDirectory; }, + [&app]( const std::string& value ) { + auto directory = FileSystem::expandTilde( value ); + if ( !directory.empty() ) { + if ( !FileSystem::isDirectory( directory ) ) + return false; + directory = FileSystem::getRealPath( directory ); + } + app.config->terminal.defaultWorkingDirectory = directory; + app.terminalConfig.workingDirectory = + directory.empty() ? Sys::getUserDirectory() : std::move( directory ); + app.savePreferences(); + return true; + }, + true ); panel->addInteger( { "terminalScrollback", "terminal.behavior", app.i18n( "terminal_scrollback", "Terminal Scrollback" ),