Summary
When renderer device initialisation fails, common->FatalError is reached but the process segfaults on a null soundSystem before the error is shown. A recoverable, clearly-diagnosable configuration problem therefore presents to the user as an unexplained SIGSEGV.
Environment
- macOS 27.0, Apple M4 Pro, Xcode 26.6
- openQ4
227ee810 (main), release build, platform_backend=sdl3
Reproduce
Any Vulkan init failure will do. The easiest is to run .install/ without MoltenVK staged where the SDL3 backend probes for it (that packaging mismatch is #95):
cd .install && ./openQ4-client_arm64 +set r_renderApi vulkan
Observed:
openQ4: fatal signal SIGSEGV (11), exiting without unsafe engine shutdown
openQ4: last renderer startup phase: idle
openQ4: last game module phase: idle
Under lldb:
stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: idSoundSystem::StopAllSounds(int) + 20
-> 0x1002d7f7c <+20>: ldr x8, [x0] ; x0 == 0
0x1002d7f80 <+24>: ldr x1, [x8, #0x18]
0x1002d7f88 <+32>: br x1
That is a virtual dispatch through a null object — soundSystem is null at the point the fatal-error path calls into it. (lldb cannot unwind past frame #0 because the thunk tail-calls, so the caller frame is already gone.)
The actual, useful message is only visible with +set logFile 2, in <fs_savepath>/baseoq4/qconsole.log:
----- VK_InitRenderDevice -----
WARNING: Vulkan: window creation failed
FATAL: Vulkan renderer device initialization failed
Note on diagnosis difficulty
Without logFile 2 this is genuinely misleading: stdout is block-buffered, so several KB of output is lost on the crash and the last line to reach the terminal is from a much earlier part of startup (machine-spec detection, mid-Printf):
Video memory: 37.44 GB on an opopenQ4: fatal signal SIGSEGV (11), ...
That points investigators at the wrong subsystem entirely. Flushing (or fflush on the fatal path) before the handler exits would help a lot on its own.
Suggested fix
Null-guard the sound teardown on the fatal-error/shutdown path, as is already done elsewhere in the codebase — e.g. Session.cpp:4594:
if ( soundSystem ) {
soundSystem->StopAllSounds( SOUNDWORLD_GAME );
}
More generally, the fatal-error path can be entered before or during subsystem init, so it should tolerate any subsystem being absent rather than assuming a fully-initialised engine.
Why this matters
A renderer that fails to initialise is exactly the situation where the user most needs to read the error — wrong driver, missing loader, unsupported device. Crashing first turns a one-line fix into a debugging session, and it hides genuinely actionable messages like the window creation failed above.
Summary
When renderer device initialisation fails,
common->FatalErroris reached but the process segfaults on a nullsoundSystembefore the error is shown. A recoverable, clearly-diagnosable configuration problem therefore presents to the user as an unexplainedSIGSEGV.Environment
227ee810(main), release build,platform_backend=sdl3Reproduce
Any Vulkan init failure will do. The easiest is to run
.install/without MoltenVK staged where the SDL3 backend probes for it (that packaging mismatch is #95):Observed:
Under lldb:
That is a virtual dispatch through a null object —
soundSystemis null at the point the fatal-error path calls into it. (lldb cannot unwind past frame #0 because the thunk tail-calls, so the caller frame is already gone.)The actual, useful message is only visible with
+set logFile 2, in<fs_savepath>/baseoq4/qconsole.log:Note on diagnosis difficulty
Without
logFile 2this is genuinely misleading: stdout is block-buffered, so several KB of output is lost on the crash and the last line to reach the terminal is from a much earlier part of startup (machine-spec detection, mid-Printf):That points investigators at the wrong subsystem entirely. Flushing (or
fflushon the fatal path) before the handler exits would help a lot on its own.Suggested fix
Null-guard the sound teardown on the fatal-error/shutdown path, as is already done elsewhere in the codebase — e.g.
Session.cpp:4594:More generally, the fatal-error path can be entered before or during subsystem init, so it should tolerate any subsystem being absent rather than assuming a fully-initialised engine.
Why this matters
A renderer that fails to initialise is exactly the situation where the user most needs to read the error — wrong driver, missing loader, unsupported device. Crashing first turns a one-line fix into a debugging session, and it hides genuinely actionable messages like the
window creation failedabove.