games/NXDoom: add cooperative SIGTERM quit path for external supervisors.

A supervisor process (e.g. an app-store UI that owns the framebuffer
while a launched game runs directly against /dev/fb0) has no reachable
in-game quit path to drive - no keyboard/touch input is wired up for
that - so it can only ask NXDoom to exit from the outside. The only
existing option for that was a forced task_delete() from the
supervisor's side, which was found on real hardware to hang the
entire board (not just this one task) when it landed mid framebuffer/
heap access, on this flat-memory build where a bad access in one task
isn't contained to that task.

Add i_install_quit_signal()/i_poll_quit_signal()/a SIGTERM handler
(i_system.c/i_system.h): the handler itself only sets a volatile
sig_atomic_t flag - it must not call i_quit() (or anything it does:
munmap, fclose, exit()'s atexit chain) directly, since a signal can
land at literally any point in this process's own execution,
including mid-malloc()/mid-blit, the same "unsafe mid-operation
teardown" risk as being force-killed from outside, just moved into
this process's own context. i_poll_quit_signal() defers the actual
shutdown to a call in the main per-frame loop (d_doomloop(), d_main.c)
- a point that's definitely safe (outside any framebuffer/heap access)
and reached every frame regardless of what else NXDoom is doing, which
is what's actually verified working end-to-end against a real
supervisor's SIGTERM/close path on hardware.

i_install_quit_signal() is called once from main() (i_main.c), and
also:

- Checks sigaction()'s return value and logs via syslog on failure
  instead of ignoring it silently - the game still runs either way,
  but silently leaving close non-functional with no trace of why
  would make a real close-path bug harder to diagnose than it needs
  to be.

- Resets quit_requested and exit_funcs at the start of
  i_install_quit_signal(). This board's flat, single address-space
  build normally relaunches NXDoom as a fresh loadable ELF module with
  its own zeroed .bss, but GAMES_NXDOOM is a tristate Kconfig symbol
  and can also be built in as a true built-in sharing this process's
  address space across "launches" with no fresh .bss at all - a
  leaked quit_requested flag would call i_quit() again before the game
  even starts on a second invocation, and a leaked exit_funcs chain
  would re-run every previous invocation's exit handlers a second
  time.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: aviralgarg05 <gargaviral99@gmail.com>
This commit is contained in:
aviralgarg05 2026-07-23 15:59:23 +05:30
parent cddef4d8da
commit 5a3d77b65a
4 changed files with 109 additions and 0 deletions

View file

@ -1294,6 +1294,12 @@ void d_doomloop(void)
while (1)
{
/* Safe point (outside any framebuffer/heap access) for nxstore's
* SIGTERM-driven close request to actually take effect - see
* i_install_quit_signal() in i_system.h.
*/
i_poll_quit_signal();
d_run_frame();
}
}

View file

@ -57,6 +57,15 @@ void d_doom_main(void);
int main(int argc, char **argv)
{
/* Lets nxstore (or any other supervisor) ask this process to exit
* cleanly via SIGTERM instead of the only other option being a forced
* task_delete() from outside - see i_system.h/i_system.c for why that
* matters on this board (a forced kill mid framebuffer/heap access was
* observed to hang the whole system, not just this task).
*/
i_install_quit_signal();
/* save arguments */
myargc = argc;

View file

@ -22,10 +22,13 @@
* Included Files
****************************************************************************/
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "config.h"
@ -75,6 +78,15 @@ static atexit_listentry_t *exit_funcs = NULL;
static boolean already_quitting = false;
/* Set only by i_quit_signal_handler() (async-signal-safe: a single
* sig_atomic_t store, nothing else) and read only by
* i_poll_quit_signal(), called from a safe point in the main loop - see
* the comment on i_install_quit_signal() in i_system.h for why the
* actual i_quit() cleanup is deferred out of the signal handler itself.
*/
static volatile sig_atomic_t quit_requested = 0;
/* Read Access Violation emulation.
*
* From PrBoom+, by entryway.
@ -320,6 +332,71 @@ void i_quit(void)
exit(0);
}
/* i_quit_signal_handler
*
* A supervisor process (nxstore) has no reachable in-game quit path to
* drive (no keyboard/touch input is wired up here) - it can only ask
* from the outside, via SIGTERM. This handler does the one thing a
* signal handler is safe to do: set a flag. It must NOT call i_quit()
* (or anything it does - munmap, fclose, exit()'s atexit chain) directly,
* because a signal can land at literally any point in this process's own
* execution, including mid-malloc()/mid-blit - exactly the same "unsafe
* mid-operation teardown" risk as being force-killed from outside, just
* moved from another task's context into this one. i_poll_quit_signal()
* defers the real work to a known-safe boundary instead.
*/
static void i_quit_signal_handler(int signo)
{
(void)signo;
quit_requested = 1;
}
void i_install_quit_signal(void)
{
struct sigaction sa;
/* This board's flat, single address-space build can relaunch NXDoom
* (via nxpkg) as a fresh loadable ELF module - a proper posix_spawn of
* a new module load, which gets its own zeroed .bss/re-initialized
* .data - but GAMES_NXDOOM is a tristate Kconfig symbol and can also
* be built in as a true built-in (MODULE=n) sharing this process's
* address space across "launches" with no fresh .bss at all. Reset
* both pieces of state a stale second invocation could see: a leaked
* quit_requested flag would call i_quit() again before the game even
* starts, and a leaked exit_funcs chain would run every previous
* invocation's exit handlers a second time (double free()s, etc.) in
* addition to this invocation's own.
*/
quit_requested = 0;
exit_funcs = NULL;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = i_quit_signal_handler;
if (sigaction(SIGTERM, &sa, NULL) < 0)
{
/* Not fatal - the game still runs, it just can't be asked to
* close cleanly from the outside (nxstore's close button will
* have nothing to signal into). Surface it rather than silently
* leaving close non-functional with no trace of why.
*/
syslog(LOG_WARNING,
"nxdoom: failed to install SIGTERM handler: %d\n", errno);
}
}
void i_poll_quit_signal(void)
{
if (quit_requested)
{
syslog(LOG_WARNING, "nxdoom: quit signal seen, calling i_quit\n");
i_quit();
}
}
void i_error(const char *error, ...)
{
char msgbuf[512];

View file

@ -73,6 +73,23 @@ ticcmd_t *i_base_ticcmd(void);
void i_quit(void) NORETURN;
/* Installs a SIGTERM handler that only sets a flag (async-signal-safe) -
* the actual i_quit() cleanup (unmapping the framebuffer, closing fds)
* runs later from i_poll_quit_signal(), called once per tic from a known
* safe point in the main loop rather than from the signal handler itself,
* so a supervisor process (nxstore) requesting an exit can never land in
* the middle of a frame's worth of direct framebuffer/heap access.
*/
void i_install_quit_signal(void);
/* Checks the flag set by the SIGTERM handler and calls i_quit() if it's
* set. Must only be called from a safe point in the main loop - see
* i_install_quit_signal().
*/
void i_poll_quit_signal(void);
void i_error(const char *error, ...) NORETURN PRINTF_ATTR(1, 2);
void i_tactile(int on, int off, int total);