diff --git a/.github/workflows/linux-verification.yml b/.github/workflows/linux-verification.yml
index 0035c70..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
@@ -74,19 +81,28 @@ 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 \
linux-libc-dev:i386 \
mesa-common-dev:i386 \
- meson \
ninja-build \
pkg-config \
- python3
+ python3 \
+ wayland-protocols
- name: Build dependency-isolated i686 dedicated server
run: |
@@ -129,3 +145,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/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",
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 cd5173e..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,16 +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 )
{
- signal( SIGINT, SIG_IGN );
- 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 );
+ /* 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, 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/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..1a35596 100644
--- a/tests/ci_regression_workflow_tests.py
+++ b/tests/ci_regression_workflow_tests.py
@@ -44,6 +44,25 @@ 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("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)
+ 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..d314ea3 100644
--- a/tests/platform_port_safety_tests.py
+++ b/tests/platform_port_safety_tests.py
@@ -12,6 +12,44 @@ 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("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")
+ 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 +334,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)