From aeb7da2a488f79aa08df078ad633646dfe2a8834 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Wed, 19 Aug 2026 16:02:13 +0100 Subject: [PATCH 1/4] linux: close i686 build and runtime gaps --- .github/workflows/linux-verification.yml | 49 +++++++++++++++++++++++- .install/README.html | 2 +- BUILD.md | 25 +++++++++++- README.md | 7 +++- code/unix/linux_signals.cpp | 6 ++- docs/templates/README.md.in | 7 +++- docs/templates/install-readme.html.in | 2 +- misc/meson/linux-x86.ini | 3 ++ tests/ci_regression_workflow_tests.py | 17 ++++++++ tests/platform_port_safety_tests.py | 39 +++++++++++++++++++ 10 files changed, 148 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux-verification.yml b/.github/workflows/linux-verification.yml index 0035c70..0134718 100644 --- a/.github/workflows/linux-verification.yml +++ b/.github/workflows/linux-verification.yml @@ -74,10 +74,19 @@ jobs: gcc-multilib \ g++-multilib \ glslang-tools \ + cmake \ + libdbus-1-dev:i386 \ + libegl-dev:i386 \ libc6-dev-i386 \ libgl-dev:i386 \ + libpipewire-0.3-dev:i386 \ + libudev-dev:i386 \ + libwayland-dev:i386 \ libx11-dev:i386 \ + libxcursor-dev:i386 \ libxext-dev:i386 \ + libxi-dev:i386 \ + libxkbcommon-dev:i386 \ libxrandr-dev:i386 \ libxxf86dga-dev:i386 \ libxxf86vm-dev:i386 \ @@ -86,7 +95,8 @@ jobs: meson \ ninja-build \ pkg-config \ - python3 + python3 \ + wayland-protocols - name: Build dependency-isolated i686 dedicated server run: | @@ -129,3 +139,40 @@ jobs: - name: Run complete i686 regression suite run: meson test -C .tmp/meson-linux-renderers --print-errorlogs + + - name: Build bundled SDL3 i686 client + run: | + meson setup .tmp/meson-linux-sdl \ + --cross-file misc/meson/linux-x86.ini \ + --buildtype=debugoptimized \ + --force-fallback-for=sdl3,fontstash \ + -Daudio-tests=false \ + -Dbuild-client=true \ + -Dbuild-server=false \ + -Dcurl=disabled \ + -Dglx-tests=false \ + -Dogg-vorbis=false \ + -Drenderer-dlopen=false \ + -Drenderer-default=glx \ + -Dsdl=enabled + meson compile -C .tmp/meson-linux-sdl fnql + file .tmp/meson-linux-sdl/fnql | grep -F 'ELF 32-bit LSB' + file .tmp/meson-linux-sdl/fnql | grep -F 'Intel 80386' + + - name: Build CMake i686 dedicated server + run: | + cmake -S . -B .tmp/cmake-linux-server -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS=-m32 \ + -DCMAKE_CXX_FLAGS=-m32 \ + -DCMAKE_EXE_LINKER_FLAGS=-m32 \ + -DCMAKE_SHARED_LINKER_FLAGS=-m32 \ + -DFNQL_AUDIO_LOOPBACK_TESTS=OFF \ + -DFNQL_GLX_LOGIC_TESTS=OFF \ + -DUSE_CURL=OFF \ + -DUSE_OGG_VORBIS=OFF \ + -DUSE_SDL=OFF \ + -DUSE_SYSTEM_JPEG=OFF + cmake --build .tmp/cmake-linux-server --parallel 2 --target fnql.ded + file .tmp/cmake-linux-server/fnql.ded | grep -F 'ELF 32-bit LSB' + file .tmp/cmake-linux-server/fnql.ded | grep -F 'Intel 80386' diff --git a/.install/README.html b/.install/README.html index 9033660..58f41b1 100644 --- a/.install/README.html +++ b/.install/README.html @@ -384,7 +384,7 @@

Native Linux dedicated server

- + diff --git a/BUILD.md b/BUILD.md index d8bb296..b480f8f 100644 --- a/BUILD.md +++ b/BUILD.md @@ -163,6 +163,10 @@ Linux has two distinct compatibility surfaces: - Native dedicated servers load the retail `qagamei386.so` from `baseq3/bin.pk3`; FnQL does not build or validate the x86_64 alternative. +- The same retail archive also contains `qagamex64.so` and the Win32 + `qagamex86.dll`, `cgamex86.dll`, and `uix86.dll`. Native Linux i686 uses + `qagamei386.so`; `dlopen` cannot load PE32 DLLs, so the Linux server does not + rename or emulate the Windows module. - Retail Quake Live does not ship native Linux `cgame` or UI modules; those modules and the retained Awesomium WebUI runtime are Win32-only. Native Linux client builds are maintained engine/platform development targets, but are not @@ -192,9 +196,13 @@ legacy X11 backend using system development packages while Meson fetches only missing wrap fallbacks (including the required pinned FontStash source): ```sh +sudo dpkg --add-architecture i386 sudo apt update sudo apt install gcc-multilib g++-multilib libc6-dev-i386 \ - linux-libc-dev:i386 git meson ninja-build pkg-config python3 + libgl-dev:i386 libx11-dev:i386 libxext-dev:i386 \ + libxrandr-dev:i386 libxxf86dga-dev:i386 libxxf86vm-dev:i386 \ + linux-libc-dev:i386 mesa-common-dev:i386 \ + git glslang-tools meson ninja-build pkg-config python3 meson setup meson/build-linux-x86 \ --cross-file misc/meson/linux-x86.ini \ --buildtype=debugoptimized -Dsdl=disabled -Dcurl=disabled \ @@ -208,6 +216,21 @@ OpenAL, cURL, JPEG, Ogg/Vorbis, and X11 development packages. The release workflow is the canonical dependency baseline when distribution package names differ. +The bundled SDL3 fallback additionally needs its Linux platform dependencies. +On Ubuntu 24.04, install them and exercise the same i686 path used by Linux CI: + +```sh +sudo apt install libdbus-1-dev:i386 libegl-dev:i386 \ + libpipewire-0.3-dev:i386 libudev-dev:i386 libwayland-dev:i386 \ + libxcursor-dev:i386 libxi-dev:i386 libxkbcommon-dev:i386 wayland-protocols +meson setup meson/build-linux-sdl-x86 \ + --cross-file misc/meson/linux-x86.ini \ + --force-fallback-for=sdl3,fontstash \ + -Dbuild-server=false -Dcurl=disabled -Dogg-vorbis=false \ + -Drenderer-dlopen=false -Drenderer-default=glx -Dsdl=enabled +meson compile -C meson/build-linux-sdl-x86 fnql +``` + Stage a build without writing into the Steam library, verify it, and then copy the whole flat runtime root together: diff --git a/README.md b/README.md index 7dfb680..e38faa8 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,11 @@ Retail Quake Live ships native Linux server modules, but its client and WebUI modules are Win32-only. The native Linux client executable is therefore an engine/platform development target, not a supported retail-play path. Use the Win32/x86 package for retail client play and the Linux i686 package for native -dedicated hosting. FnQL does not support or build macOS because retail ships no -macOS game, client, UI, or QVM modules, and FnQL will not reconstruct them. +dedicated hosting. Retail `bin.pk3` contains `qagamei386.so` and +`qagamex64.so`; the i686 server loads the former directly and does not try to +load or rename the Win32 PE DLLs. FnQL does not support or build macOS because +retail ships no macOS game, client, UI, or QVM modules, and FnQL will not +reconstruct them. FnQL discovers conventional Steam libraries on each supported OS; for a custom library use `+set fs_steampath ""`. Optional Steam and WebUI services diff --git a/code/unix/linux_signals.cpp b/code/unix/linux_signals.cpp index cd5173e..086369f 100644 --- a/code/unix/linux_signals.cpp +++ b/code/unix/linux_signals.cpp @@ -74,7 +74,11 @@ static void signal_handler( int sig ) void InitSig( void ) { - signal( SIGINT, SIG_IGN ); + /* Keep interactive dedicated servers manageable from a normal terminal. + * Retail's Linux signal setup does not ignore SIGINT, and treating Ctrl+C + * like the existing service-stop signals gives FnQL a chance to shut down + * the retail game module and server state cleanly. */ + signal( SIGINT, signal_handler ); signal( SIGHUP, signal_handler ); signal( SIGQUIT, signal_handler ); signal( SIGILL, signal_handler ); diff --git a/docs/templates/README.md.in b/docs/templates/README.md.in index 01501c1..0d06fb5 100644 --- a/docs/templates/README.md.in +++ b/docs/templates/README.md.in @@ -108,8 +108,11 @@ Retail Quake Live ships native Linux server modules, but its client and WebUI modules are Win32-only. The native Linux client executable is therefore an engine/platform development target, not a supported retail-play path. Use the Win32/x86 package for retail client play and the Linux i686 package for native -dedicated hosting. FnQL does not support or build macOS because retail ships no -macOS game, client, UI, or QVM modules, and FnQL will not reconstruct them. +dedicated hosting. Retail `bin.pk3` contains `qagamei386.so` and +`qagamex64.so`; the i686 server loads the former directly and does not try to +load or rename the Win32 PE DLLs. FnQL does not support or build macOS because +retail ships no macOS game, client, UI, or QVM modules, and FnQL will not +reconstruct them. FnQL discovers conventional Steam libraries on each supported OS; for a custom library use `+set fs_steampath ""`. Optional Steam and WebUI services diff --git a/docs/templates/install-readme.html.in b/docs/templates/install-readme.html.in index e0fdc3e..cdf2e54 100644 --- a/docs/templates/install-readme.html.in +++ b/docs/templates/install-readme.html.in @@ -384,7 +384,7 @@ - + diff --git a/misc/meson/linux-x86.ini b/misc/meson/linux-x86.ini index 679d714..dfcb31d 100644 --- a/misc/meson/linux-x86.ini +++ b/misc/meson/linux-x86.ini @@ -17,6 +17,9 @@ pkg_config_libdir = ['/usr/lib/i386-linux-gnu/pkgconfig', '/usr/share/pkgconfig' [host_machine] system = 'linux' +# The bundled SDL3 Meson fallback queries subsystem() while selecting HID +# sources; Meson cannot infer this value for a Linux cross build. +subsystem = 'linux' cpu_family = 'x86' cpu = 'i686' endian = 'little' diff --git a/tests/ci_regression_workflow_tests.py b/tests/ci_regression_workflow_tests.py index bf0e34b..56642eb 100644 --- a/tests/ci_regression_workflow_tests.py +++ b/tests/ci_regression_workflow_tests.py @@ -44,6 +44,23 @@ def test_every_pull_request_runs_the_complete_i686_meson_suite(self) -> None: workflow, ) + def test_linux_ci_keeps_sdl3_and_cmake_on_the_i686_target(self) -> None: + workflow = (ROOT / ".github" / "workflows" / "linux-verification.yml").read_text( + encoding="utf-8" + ) + + self.assertIn("Build bundled SDL3 i686 client", workflow) + self.assertIn("libxcursor-dev:i386", workflow) + self.assertIn("libxi-dev:i386", workflow) + self.assertIn("--force-fallback-for=sdl3,fontstash", workflow) + self.assertIn("-Dsdl=enabled", workflow) + self.assertIn("meson-linux-sdl fnql", workflow) + self.assertIn("Build CMake i686 dedicated server", workflow) + self.assertIn("-DCMAKE_C_FLAGS=-m32", workflow) + self.assertIn("-DCMAKE_CXX_FLAGS=-m32", workflow) + self.assertIn("--target fnql.ded", workflow) + self.assertNotIn("x86_64", workflow) + def test_release_source_validation_uses_recursive_pytest_discovery(self) -> None: workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text( encoding="utf-8" diff --git a/tests/platform_port_safety_tests.py b/tests/platform_port_safety_tests.py index 092a931..57fc913 100644 --- a/tests/platform_port_safety_tests.py +++ b/tests/platform_port_safety_tests.py @@ -12,6 +12,34 @@ def read(relative_path: str) -> str: class PlatformPortSafetyTests(unittest.TestCase): + def test_linux_sigint_requests_a_clean_shutdown(self) -> None: + signals = read("code/unix/linux_signals.cpp") + + self.assertIn("signal( SIGINT, signal_handler );", signals) + self.assertNotIn("signal( SIGINT, SIG_IGN );", signals) + + def test_retail_linux_i386_module_uses_the_native_so_name(self) -> None: + platform = read("code/qcommon/q_platform.h") + vm = read("code/qcommon/vm.c") + build_guide = read("BUILD.md") + + unix_platform = platform[ + platform.index("#else // !defined _WIN32") : + platform.index("// ============================== Linux") + ] + self.assertIn('#define DLL_EXT ".so"', unix_platform) + self.assertIn( + '#if defined (__i386__)\n#define ARCH_STRING "i386"', + unix_platform, + ) + self.assertIn( + 'Com_sprintf( filename, sizeof( filename ), "%s" ARCH_STRING DLL_EXT, name );', + vm, + ) + self.assertIn("qagamei386.so", build_guide) + self.assertIn("qagamex64.so", build_guide) + self.assertIn("cannot load PE32 DLLs", build_guide) + def test_linux_vulkan_and_x11_types_are_width_safe(self) -> None: qvk = read("code/unix/linux_qvk.cpp") x11 = read("code/unix/linux_glimp.cpp") @@ -296,11 +324,22 @@ def test_linux_ci_builds_and_verifies_only_i686_artifacts(self) -> None: self.assertIn("-Dbuild-server=false", workflow) self.assertIn("-Drenderers=glx,vk,rtx", workflow) self.assertIn("fnql_${renderer}_x86.so", workflow) + self.assertIn("Build bundled SDL3 i686 client", workflow) + self.assertIn("libxcursor-dev:i386", workflow) + self.assertIn("libxi-dev:i386", workflow) + self.assertIn("--force-fallback-for=sdl3,fontstash", workflow) + self.assertIn("-Dsdl=enabled", workflow) + self.assertIn("meson-linux-sdl fnql", workflow) + self.assertIn("Build CMake i686 dedicated server", workflow) + self.assertIn("-DCMAKE_C_FLAGS=-m32", workflow) + self.assertIn("-DCMAKE_CXX_FLAGS=-m32", workflow) + self.assertIn("cmake-linux-server --parallel 2 --target fnql.ded", workflow) self.assertIn("Intel 80386", workflow) self.assertNotIn("x86_64", workflow) self.assertNotIn("ELF 64-bit", workflow) self.assertIn("c_args = ['-m32']", cross_file) self.assertIn("cpp_args = ['-m32']", cross_file) + self.assertIn("subsystem = 'linux'", cross_file) self.assertIn("cpu_family = 'x86'", cross_file) self.assertIn("cpu = 'i686'", cross_file) self.assertIn("/usr/lib/i386-linux-gnu/pkgconfig", cross_file) From 1594e77758916cfaa35b1be74ea617259776d314 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Wed, 19 Aug 2026 16:07:14 +0100 Subject: [PATCH 2/4] rtx: refresh shader reflection manifest --- .../shaders/spirv/shader_reflection.json | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/code/rendererrtx/shaders/spirv/shader_reflection.json b/code/rendererrtx/shaders/spirv/shader_reflection.json index 869a2a9..2e83428 100644 --- a/code/rendererrtx/shaders/spirv/shader_reflection.json +++ b/code/rendererrtx/shaders/spirv/shader_reflection.json @@ -1,7 +1,7 @@ { "schema": "q3rtx.rendererrtx.shader_reflection.v1", "generator": "code/rendererrtx/shaders/build_shaders.py", - "shader_count": 88, + "shader_count": 89, "shaders": [ { "symbol": "color_vert_spv", @@ -415,6 +415,31 @@ "push_constants": [] } }, + { + "symbol": "menu_blur_frag_spv", + "source": "menu_blur.frag", + "stage": "frag", + "defines": [], + "byte_size": 1532, + "reflection": { + "descriptor_bindings": [ + { + "name": "texture0", + "set": 0, + "binding": 0, + "storage_class": "UniformConstant", + "descriptor_class": "combined_image_sampler", + "descriptor_count": 1 + } + ], + "push_constants": [ + { + "name": "pc", + "size_bytes": 16 + } + ] + } + }, { "symbol": "motion_blur_frag_spv", "source": "motion_blur.frag", From 4ba0b507e797ace69b19dc2fd7dc15fcbfa35ff3 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Wed, 19 Aug 2026 16:08:54 +0100 Subject: [PATCH 3/4] ci: pin Meson for bundled SDL3 verification --- .github/workflows/linux-verification.yml | 8 +++++++- tests/ci_regression_workflow_tests.py | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux-verification.yml b/.github/workflows/linux-verification.yml index 0134718..1662d4e 100644 --- a/.github/workflows/linux-verification.yml +++ b/.github/workflows/linux-verification.yml @@ -65,6 +65,13 @@ jobs: steps: - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install pinned Meson + run: python -m pip install meson==1.9.1 + - name: Install i686 Meson build dependencies run: | sudo dpkg --add-architecture i386 @@ -92,7 +99,6 @@ jobs: libxxf86vm-dev:i386 \ linux-libc-dev:i386 \ mesa-common-dev:i386 \ - meson \ ninja-build \ pkg-config \ python3 \ diff --git a/tests/ci_regression_workflow_tests.py b/tests/ci_regression_workflow_tests.py index 56642eb..1a35596 100644 --- a/tests/ci_regression_workflow_tests.py +++ b/tests/ci_regression_workflow_tests.py @@ -50,6 +50,8 @@ def test_linux_ci_keeps_sdl3_and_cmake_on_the_i686_target(self) -> None: ) self.assertIn("Build bundled SDL3 i686 client", workflow) + self.assertIn("actions/setup-python@v6", workflow) + self.assertIn("python -m pip install meson==1.9.1", workflow) self.assertIn("libxcursor-dev:i386", workflow) self.assertIn("libxi-dev:i386", workflow) self.assertIn("--force-fallback-for=sdl3,fontstash", workflow) From 63d1c296f17f977a03e9aa979c5bc6aca73adcca Mon Sep 17 00:00:00 2001 From: themuffinator Date: Wed, 19 Aug 2026 16:25:16 +0100 Subject: [PATCH 4/4] linux: defer signal shutdown to main loop --- code/unix/linux_local.h | 1 + code/unix/linux_signals.cpp | 52 +++++++++++++++++++++++------ code/unix/unix_main.cpp | 2 ++ tests/platform_port_safety_tests.py | 12 ++++++- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/code/unix/linux_local.h b/code/unix/linux_local.h index 251dfda..bac67c2 100644 --- a/code/unix/linux_local.h +++ b/code/unix/linux_local.h @@ -57,6 +57,7 @@ char *strlwr (char *s); // signals.c void InitSig(void); +void Sys_HandlePendingSignals(void); #ifdef __cplusplus } diff --git a/code/unix/linux_signals.cpp b/code/unix/linux_signals.cpp index 086369f..b961c04 100644 --- a/code/unix/linux_signals.cpp +++ b/code/unix/linux_signals.cpp @@ -36,10 +36,17 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #endif static qboolean signalcaught = qfalse; +static volatile sig_atomic_t pendingShutdownSignal = 0; extern void NORETURN Sys_Exit( int code ); -static void signal_handler( int sig ) +static void shutdown_signal_handler( int sig ) +{ + pendingShutdownSignal = sig; +} + + +static void fatal_signal_handler( int sig ) { char msg[32]; @@ -72,20 +79,43 @@ static void signal_handler( int sig ) } +void Sys_HandlePendingSignals( void ) +{ + const int sig = pendingShutdownSignal; + char msg[32]; + + if ( sig == 0 ) + { + return; + } + + pendingShutdownSignal = 0; + printf( "Received signal %d, exiting...\n", sig ); + Com_sprintf( msg, sizeof( msg ), "Signal caught (%d)", sig ); + VM_Forced_Unload_Start(); +#ifndef DEDICATED + CL_Shutdown( msg, qtrue ); +#endif + SV_Shutdown( msg ); + VM_Forced_Unload_Done(); + Sys_Exit( 0 ); +} + + void InitSig( void ) { /* Keep interactive dedicated servers manageable from a normal terminal. * Retail's Linux signal setup does not ignore SIGINT, and treating Ctrl+C * like the existing service-stop signals gives FnQL a chance to shut down * the retail game module and server state cleanly. */ - signal( SIGINT, signal_handler ); - signal( SIGHUP, signal_handler ); - signal( SIGQUIT, signal_handler ); - signal( SIGILL, signal_handler ); - signal( SIGTRAP, signal_handler ); - signal( SIGIOT, signal_handler ); - signal( SIGBUS, signal_handler ); - signal( SIGFPE, signal_handler ); - signal( SIGSEGV, signal_handler ); - signal( SIGTERM, signal_handler ); + signal( SIGINT, shutdown_signal_handler ); + signal( SIGHUP, shutdown_signal_handler ); + signal( SIGQUIT, shutdown_signal_handler ); + signal( SIGTERM, shutdown_signal_handler ); + signal( SIGILL, fatal_signal_handler ); + signal( SIGTRAP, fatal_signal_handler ); + signal( SIGIOT, fatal_signal_handler ); + signal( SIGBUS, fatal_signal_handler ); + signal( SIGFPE, fatal_signal_handler ); + signal( SIGSEGV, fatal_signal_handler ); } diff --git a/code/unix/unix_main.cpp b/code/unix/unix_main.cpp index afd36af..4f6ce85 100644 --- a/code/unix/unix_main.cpp +++ b/code/unix/unix_main.cpp @@ -1156,6 +1156,8 @@ int main( int argc, const char* argv[] ) while (1) { + Sys_HandlePendingSignals(); + #ifdef __linux__ Sys_ConfigureFPU(); #endif diff --git a/tests/platform_port_safety_tests.py b/tests/platform_port_safety_tests.py index 57fc913..d314ea3 100644 --- a/tests/platform_port_safety_tests.py +++ b/tests/platform_port_safety_tests.py @@ -14,9 +14,19 @@ def read(relative_path: str) -> str: class PlatformPortSafetyTests(unittest.TestCase): def test_linux_sigint_requests_a_clean_shutdown(self) -> None: signals = read("code/unix/linux_signals.cpp") + unix = read("code/unix/unix_main.cpp") - self.assertIn("signal( SIGINT, signal_handler );", signals) + self.assertIn("static volatile sig_atomic_t pendingShutdownSignal", signals) + self.assertIn("signal( SIGINT, shutdown_signal_handler );", signals) self.assertNotIn("signal( SIGINT, SIG_IGN );", signals) + handler = signals[ + signals.index("static void shutdown_signal_handler") : + signals.index("static void fatal_signal_handler") + ] + self.assertIn("pendingShutdownSignal = sig;", handler) + self.assertNotIn("SV_Shutdown", handler) + self.assertNotIn("Sys_Exit", handler) + self.assertIn("Sys_HandlePendingSignals();", unix) def test_retail_linux_i386_module_uses_the_native_so_name(self) -> None: platform = read("code/qcommon/q_platform.h")