Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion games/NXDoom/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#

config GAMES_NXDOOM
bool "NXDoom"
tristate "NXDoom"
default n
depends on ALLOW_GPL_COMPONENTS
depends on VIDEO_FB
Expand Down Expand Up @@ -178,6 +178,26 @@ config GAMES_NXDOOM_MAXDRAWSEGS
memory, so you may reduce the number. However, too few will cause
rendering issues (overflow is checked to avoid crashes).

config GAMES_NXDOOM_HEAP_BUFFERS
bool "Allocate renderer scratch buffers on the heap"
default n
---help---
The visplanes/openings/drawsegs/vissprites renderer scratch buffers
(sized by the options above) are static arrays by default, matching
vanilla DOOM. On a target where their combined size threatens the
internal DRAM budget once linked into a full application image,
enable this to allocate them from the heap instead (this target's
heap may be backed by external RAM/PSRAM). Static allocation is
preferred where DRAM budget is not a concern.

config GAMES_NXDOOM_STATDUMP_MAX_CAPTURES
int "Maximum statdump capture buffer entries"
default 32
---help---
Number of playtime-statistics capture slots statdump.c reserves.
This is diagnostic/debug capture storage, not required for normal
gameplay - reduce it on a DRAM-constrained target.

config GAMES_NXDOOM_RANGECHECK
bool "Perform range checks"
default y
Expand Down
10 changes: 10 additions & 0 deletions games/NXDoom/src/d_iwad.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,16 @@ static void buld_iwad_dir_list(void)

add_iwad_dir(m_dir_name(myargv[0]));

/* Add the board's configured DOOM data directory. Kconfig documents
* CONFIG_GAMES_NXDOOM_PREFDIR as "Directory where DOOM WAD files are
* stored", but until now it was only used for the config/save file
* location -- nothing actually searched it for IWADs, forcing every
* launch to rely on the current directory or DOOMWADDIR/DOOMWADPATH
* being set by hand first.
*/

add_iwad_dir(CONFIG_GAMES_NXDOOM_PREFDIR);

/* Add DOOMWADDIR if it is in the environment */

env = getenv("DOOMWADDIR");
Expand Down
4 changes: 4 additions & 0 deletions games/NXDoom/src/doom/r_bsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ line_t *linedef;
sector_t *frontsector;
sector_t *backsector;

#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
drawseg_t *drawsegs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will put it behind the same Kconfig switch.

#else
drawseg_t drawsegs[CONFIG_GAMES_NXDOOM_MAXDRAWSEGS];
#endif
drawseg_t *ds_p;

/* newend is one past the last valid seg */
Expand Down
4 changes: 4 additions & 0 deletions games/NXDoom/src/doom/r_bsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ extern boolean markceiling;

extern boolean skymap;

#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
extern drawseg_t *drawsegs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

#else
extern drawseg_t drawsegs[CONFIG_GAMES_NXDOOM_MAXDRAWSEGS];
#endif
extern drawseg_t *ds_p;

extern lighttable_t **hscalelight;
Expand Down
18 changes: 18 additions & 0 deletions games/NXDoom/src/doom/r_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,24 @@ fixed_t r_scale_from_global_angle(angle_t visangle)

void r_set_view_size(int blocks, int detail)
{
/* screenblocks is only ever meant to hold 3..11 (set that way by the
* options menu and by the config default of 9). The renderer's view
* geometry math divides by values derived from it - notably
* pspriteiscale = FRACUNIT * SCREENWIDTH / viewwidth in
* r_execute_set_view_size() - so a 0 or otherwise out-of-range value
* turns into a divide-by-zero hardware exception (EXCCAUSE=6), which on
* this flat-memory build takes the whole board down rather than just
* this task. Clamp defensively so a bad/missing config value degrades
* to the default screen size instead of a system crash.
*/

if (blocks < 3 || blocks > 11)
{
printf("r_set_view_size: screenblocks=%d out of range, using 10\n",
blocks);
blocks = 10;
}
Comment on lines +698 to +704

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you actually run into an error that caused this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a truncated config file left screenblocks at 0, and the view-size math divides by a value derived from it, so it hit a divide-by-zero hardware exception and took the whole board down.
This clamp is the second, independent layer of defense on top of the config-parsing fix in m_config.c


setsizeneeded = true;
setblocks = blocks;
setdetail = detail;
Expand Down
87 changes: 81 additions & 6 deletions games/NXDoom/src/doom/r_plane.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,17 @@ planefunction_t ceilingfunc;

/* Here comes the obnoxious "visplane". */

#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
visplane_t *visplanes;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will put it behind the same Kconfig switch.

short *openings;
#else
visplane_t visplanes[CONFIG_GAMES_NXDOOM_MAXVISPLANES];
short openings[MAXOPENINGS];
#endif
visplane_t *lastvisplane;
visplane_t *floorplane;
visplane_t *ceilingplane;

short openings[MAXOPENINGS];
short *lastopening;

/* Clip values are the solid pixel bounding the range. floorclip starts out
Expand Down Expand Up @@ -114,12 +119,34 @@ static void r_map_plane(int y, int x1, int x2)
fixed_t length;
unsigned index;

#ifdef CONFIG_GAMES_NXDOOM_RANGECHECK
if (x2 < x1 || x1 < 0 || x2 >= viewwidth || y > viewheight)
/* y indexes cachedheight[]/cacheddistance[]/cachedxstep[]/cachedystep[]
* below, all sized SCREENHEIGHT - a y outside that range (observed on
* this port: y=255 against a 200-entry array, well past even
* viewheight) is an out-of-bounds array write, not just a "debug
* assertion". This used to be gated behind CONFIG_GAMES_NXDOOM_
* RANGECHECK and fatal (i_error(), which tears down the whole process
* on what vanilla Doom would just render as one glitched span) - both
* wrong: the memory-safety check must not be optional, and killing the
* entire game over one bad plane span is worse than just not drawing
* it. Clamp y into range instead of touching memory outside the
* buffers' real bounds - this still renders the span (as one glitched
* row, the same "wrong but visible" failure mode vanilla DOOM has) so
* a bad plane doesn't leave a blank gap on screen either.
*/

if (x2 < x1 || x1 < 0 || x2 >= viewwidth)
{
i_error("R_MapPlane: %i, %i at %i", x1, x2, y);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the error report?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bounds check itself was already there, just gated behind a debug-only config and fatal when tripped,
so in a normal build it wasn't even compiled in, and the array write happened uncontrolled. I made the check unconditional, but killing the whole process over one glitched plane span felt worse than what vanilla DOOM does (renders the glitch and moves on), so I made it skip the draw instead of calling i_error(). Open to putting the i_error() back if you'd rather fail loud than render a glitch, happy to hear which you'd prefer

@linguini1 linguini1 Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rendering the glitch is better if there are no other side effects? More like vanilla DOOM. I don't know which looks worse from a rendering standpoint, skipping doesn't seem like a huge deal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a middle ground: instead of returning early (which leaves a blank gap where that scanline should be), I can clamp y into [0, SCREENHEIGHT-1] and still draw with the clamped value. That keeps it memory-safe — the actual out-of-bounds write is still gone — while still rendering something for that span instead of a hole, closer to vanilla's "wrong but present" glitch. Switching to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I would render the glitch instead.

}

if (y < 0)
{
y = 0;
}
else if (y >= SCREENHEIGHT)
{
y = SCREENHEIGHT - 1;
}
#endif

if (planeheight != cachedheight[y])
{
Expand Down Expand Up @@ -195,7 +222,55 @@ static void r_make_spans(int x, int t1, int b1, int t2, int b2)

void r_init_planes(void)
{
/* Doh! */
#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
/* These renderer scratch buffers are sized for a comfortable margin
* above vanilla DOOM's original limits and, on a DRAM-constrained
* target, blow the internal DRAM budget as static arrays - opt-in
* heap allocation instead (comes out of the PSRAM-backed user heap
* on this target) via CONFIG_GAMES_NXDOOM_HEAP_BUFFERS.
*/

visplanes = malloc(sizeof(visplane_t) * CONFIG_GAMES_NXDOOM_MAXVISPLANES);
openings = malloc(sizeof(short) * MAXOPENINGS);
drawsegs = malloc(sizeof(drawseg_t) * CONFIG_GAMES_NXDOOM_MAXDRAWSEGS);
vissprites = malloc(sizeof(vissprite_t) *
CONFIG_GAMES_NXDOOM_MAXVISSPRITES);

if (visplanes == NULL || openings == NULL || drawsegs == NULL ||
vissprites == NULL)
{
i_error("r_init_planes: failed to allocate renderer buffers");
}
Comment on lines +233 to +243

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we should be allocating these, at least not without a Kconfig switch. I think the static allocation is better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above


/* i_quit() can be followed by another r_init_planes() call within the
* same boot (relaunching the game via nxpkg on this flat, single
* address-space build), so these heap buffers must be freed on exit
* or every relaunch leaks the previous allocation permanently.
*/

i_at_exit(r_shutdown_planes, true);
#endif
}

/* r_shutdown_planes
* Frees the renderer scratch buffers allocated by r_init_planes. Only
* registered as an exit handler when CONFIG_GAMES_NXDOOM_HEAP_BUFFERS is
* set - the static-array buffers have nothing to free.
*/

void r_shutdown_planes(void)
{
#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
free(visplanes);
free(openings);
free(drawsegs);
free(vissprites);

visplanes = NULL;
openings = NULL;
drawsegs = NULL;
vissprites = NULL;
#endif
}

/* r_clear_planes
Expand Down
1 change: 1 addition & 0 deletions games/NXDoom/src/doom/r_plane.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ extern fixed_t distscale[SCREENWIDTH];
****************************************************************************/

void r_init_planes(void);
void r_shutdown_planes(void);
void r_clear_planes(void);

void r_draw_planes(void);
Expand Down
4 changes: 4 additions & 0 deletions games/NXDoom/src/doom/r_things.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ spriteframe_t sprtemp[29];
int maxframe;
const char *spritename;

#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
vissprite_t *vissprites;
#else
vissprite_t vissprites[CONFIG_GAMES_NXDOOM_MAXVISSPRITES];
#endif
vissprite_t *vissprite_p;
int newvissprite;

Expand Down
4 changes: 4 additions & 0 deletions games/NXDoom/src/doom/r_things.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
* Public Data
****************************************************************************/

#ifdef CONFIG_GAMES_NXDOOM_HEAP_BUFFERS
extern vissprite_t *vissprites;
#else
extern vissprite_t vissprites[CONFIG_GAMES_NXDOOM_MAXVISSPRITES];
#endif
extern vissprite_t *vissprite_p;
extern vissprite_t vsprsortedhead;

Expand Down
2 changes: 1 addition & 1 deletion games/NXDoom/src/doom/statdump.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* Pre-processor Definitions
****************************************************************************/

#define MAX_CAPTURES 32
#define MAX_CAPTURES CONFIG_GAMES_NXDOOM_STATDUMP_MAX_CAPTURES

/****************************************************************************
* Private Data
Expand Down
15 changes: 13 additions & 2 deletions games/NXDoom/src/i_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,28 @@ void d_doom_main(void);

int main(int argc, char **argv)
{
/* save arguments */
/* save arguments
*
* +1 and an explicit NULL terminator: argv is conventionally
* NULL-terminated at argv[argc] (this is what the OS/exec path
* guarantees for the `argv` parameter above), and some of this
* codebase's own argument handling was written assuming that holds
* for myargv too - an under-sized allocation here leaves myargv[argc]
* pointing at whatever the allocator happens to return next, which
* only reads as "probably zero" by chance depending on heap layout.
*/

myargc = argc;
myargv = malloc(argc * sizeof(char *));
myargv = malloc((argc + 1) * sizeof(char *));
assert(myargv != NULL);

for (int i = 0; i < argc; i++)
Comment on lines +62 to 75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you actually run into an error with this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a bad-pointer-dereference crash (LoadProhibitedCause) on real hardware, root-caused through the crash dump and symbol resolution against the built ELF. myargv was allocated for exactly argc pointers but code elsewhere assumed it was NULL-terminated at argv[argc], so that slot pointed at whatever the allocator returned next (usually zero by luck)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include the repro in this PR or with some before/after logs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, a clean repro recipe is tricky here since it's heap-layout dependent — that's exactly what made it read as "NULL by luck" instead of a guaranteed crash. It wasn't triggered by anything unusual on my end though, just an ordinary nxdoom launch on the board. I'll grab the actual crash backtrace from when this first surfaced and attach it, plus a fresh before/after capture so there's real evidence in the PR instead of just my description of it.

{
myargv[i] = m_string_duplicate(argv[i]);
}

myargv[argc] = NULL;

/* Print the program version and exit. */

if (m_parm_exists("-version") || m_parm_exists("--version"))
Expand Down
58 changes: 49 additions & 9 deletions games/NXDoom/src/i_video.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ struct graphics_state_s

uint8_t scale;

/* Pixel offset to center the scaled game viewport within the frame
* buffer when the buffer is larger than SCREENWIDTH/HEIGHT * scale.
*/

int xoffset;
int yoffset;

bool inited; /* Track initialization */
};

Expand Down Expand Up @@ -262,13 +269,25 @@ static void blit_screen(void)
uint8_t p_idx;
void *fbptr;

/* TODO: It would be best to do this more efficiently/with less memory.
* It also would be good if we could handle the palette translation here
* such that DOOM can be played on frame buffers with differing bit depths
* and pixel formats.
*/
/* TODO: It would be best to do this more efficiently/with less memory. */

if (g_graphics_state.pinfo.bpp != 16 && g_graphics_state.pinfo.bpp != 32)
{
/* The xoffset/stride math below assumes one of those two pixel
* sizes, so continuing would read/write past the intended pixel
* bounds on the very first frame. Fail loudly instead of
* silently corrupting framebuffer memory.
*/

i_error("Unsupported framebuffer depth: %u bpp",
g_graphics_state.pinfo.bpp);
}

fbptr = g_graphics_state.fbmem +
g_graphics_state.yoffset * g_graphics_state.pinfo.stride +
g_graphics_state.xoffset *
(g_graphics_state.pinfo.bpp == 16 ? 2 : 4);

fbptr = g_graphics_state.fbmem;
for (unsigned y = 0; y < SCREENHEIGHT * g_graphics_state.scale; y++)
{
for (unsigned x = 0; x < SCREENWIDTH * g_graphics_state.scale; x++)
Expand All @@ -277,9 +296,18 @@ static void blit_screen(void)
.scrnbuf[(y / g_graphics_state.scale) * SCREENWIDTH +
(x / g_graphics_state.scale)];

((uint32_t *)(fbptr))[x] =
ARGBTO32(g_palette[p_idx].a, g_palette[p_idx].r,
g_palette[p_idx].g, g_palette[p_idx].b);
if (g_graphics_state.pinfo.bpp == 16)
{
((uint16_t *)(fbptr))[x] =
RGBTO16(g_palette[p_idx].r, g_palette[p_idx].g,
g_palette[p_idx].b);
}
else
{
((uint32_t *)(fbptr))[x] =
ARGBTO32(g_palette[p_idx].a, g_palette[p_idx].r,
g_palette[p_idx].g, g_palette[p_idx].b);
}
}

fbptr += g_graphics_state.pinfo.stride;
Expand Down Expand Up @@ -724,6 +752,18 @@ void i_init_graphics(void)
yscale = g_graphics_state.vinfo.yres / SCREENHEIGHT;
g_graphics_state.scale = xscale > yscale ? yscale : xscale;

/* Center the scaled viewport within the frame buffer rather than
* pinning it to the top-left corner, since the buffer is typically
* larger than SCREENWIDTH/HEIGHT * scale.
*/

g_graphics_state.xoffset =
(g_graphics_state.vinfo.xres -
SCREENWIDTH * g_graphics_state.scale) / 2;
g_graphics_state.yoffset =
(g_graphics_state.vinfo.yres -
SCREENHEIGHT * g_graphics_state.scale) / 2;

/* Get frame buffer plane info */

if (ioctl(g_graphics_state.fd, FBIOGET_PLANEINFO,
Expand Down
Loading
Loading