Skip to content

Fix Death Mountain Trail rockfall crash (gfx pool headMagic overflow, graph.c:356)#6744

Open
halo9004 wants to merge 1 commit into
HarbourMasters:developfrom
halo9004:fix-dmt-gfxpool-crash
Open

Fix Death Mountain Trail rockfall crash (gfx pool headMagic overflow, graph.c:356)#6744
halo9004 wants to merge 1 commit into
HarbourMasters:developfrom
halo9004:fix-dmt-gfxpool-crash

Conversation

@halo9004

@halo9004 halo9004 commented Jun 14, 2026

Copy link
Copy Markdown

Ship of Harkinian [BUG].log

Summary

Hard crash when standing at the Death Mountain Trail summit during the rockfall,
as adult Link. The game faults in Graph_Update via Fault_AddHungupAndCrash
at soh/src/code/graph.c:356. It does not happen as child Link in the same
spot, and it is independent of resolution, enhancements, texture/model mods, and
the OTR (reproduced with all mods removed, enhancements off, N64 mode 4:3/240p,
and a freshly regenerated OTR from a clean, hash-verified ROM).

Version

  • Ship of Harkinian: Ackbar Delta (9.2.3), Git commit cb71e22
  • OS: Windows
  • Reproduced on the official release build and on a self-built copy from source.

Steps to reproduce

  1. As adult Link, go to Death Mountain Trail and climb toward the summit.
  2. run into the area where the burning rocks/boulders are falling.
  3. After a short time [usually as soon as a rock hits the ground] the game crashes with the SoH crash dialog.

100% reproducible at that spot for me.

Crash signature

Exception: 0xc0000005
Fault_AddHungupAndCrashImpl   fault.c:1106
Fault_AddHungupAndCrash       fault.c:1113
Graph_Update                  graph.c:356
RunFrame                      graph.c:492
Graph_ThreadEntry             graph.c:520
Scene: SCENE_DEATH_MOUNTAIN_TRAIL

(Full crash log attached.)

Root cause analysis

graph.c:356 is the gfx-pool head-guard check in Graph_Update:

GfxPool* pool = &gGfxPools[gfxCtx->gfxPoolIdx & 1];
if (pool->headMagic != GFXPOOL_HEAD_MAGIC) {   // line 356
    Fault_AddHungupAndCrash(__FILE__, __LINE__);
}

headMagic sits at offset 0 of GfxPool, immediately before polyOpaBuffer[0]
(offset 0x08). The opaque arena is a two-headed arena:

  • display-list commands grow the head upward from polyOpaBuffer[0];
  • matrices/vertices (Matrix_NewMtx -> Graph_Alloc -> THGA_AllocEnd) grow the
    tail downward from the end of polyOpaBuffer.

THA_AllocEnd (soh/src/code/TwoHeadArena.c) has no lower-bound check:

void* THA_AllocEnd(TwoHeadArena* tha, size_t size) {
    ...
    tha->tail = (((uintptr_t)tha->tail & mask) - size) & mask;
    return tha->tail;
}

In a single frame that draws a very large number of actors at once (the DMT
summit rockfall: many En_Fire_Rock, boulders, plus adult Link's heavier
model/equipment), the tail decrements past polyOpaBuffer[0] and writes into
headMagic. The next frame's Graph_Update sees the guard clobbered and
hard-crashes. Because the write lands on headMagic rather than tripping
THGA_IsCrash (which would just skip the frame), it's a hard crash rather than a
dropped frame. Adult vs. child Link is a threshold effect -- adult's extra
per-frame matrices push that frame over the edge.

Proposed fix

Give the opaque arena more headroom in soh/include/z64.h -- polyOpaBuffer from
0x2FC0 to 0x6000 Gfx entries (~95 KB -> ~192 KB per pool, ~200 KB total extra
across the two pools). Nothing else hardcodes the old size.

 typedef struct {
     /* 0x00000 */ u16 headMagic; // GFXPOOL_HEAD_MAGIC
-    /* 0x00008 */ Gfx polyOpaBuffer[0x2FC0];
+    /* 0x00008 */ Gfx polyOpaBuffer[0x6000];
     /* ... */    Gfx polyXluBuffer[0x1000];
     /* ... */    Gfx overlayBuffer[0x800];
     /* ... */    Gfx workBuffer[0x100];
     /* ... */    Gfx unusedBuffer[0x40];
     /* ... */    u16 tailMagic; // GFXPOOL_TAIL_MAGIC
 } GfxPool;

I built this change from source (commit cb71e22) and confirmed the crash no
longer occurs
at the DMT summit rockfall as adult Link.

Optional hardening (not required for the fix, but converts any future over-budget
frame from a hard crash into a harmless dropped frame): clamp THA_AllocEnd so
the tail can't underflow below tha->bufp, letting the existing THGA_IsCrash
path in Graph_Update handle it gracefully.

Build Artifacts

The opaque gfx pool (polyOpaBuffer) is filled from both ends in a frame:
display lists from the front, matrices/vertices from the back via
THGA_AllocEnd, which has no lower-bound check. Very dense frames push the
back allocations past the start of the buffer and overwrite headMagic,
hard-crashing Graph_Update (graph.c:356). Enlarging polyOpaBuffer from
0x2FC0 to 0x6000 gives those frames enough headroom.

Reproduced 100% at the Death Mountain Trail summit during the rockfall as
adult Link; does not occur there as child Link. Independent of resolution,
enhancements, mods, and the OTR. Tested from a source build: the crash no
longer occurs.
Comment thread soh/include/z64.h
Comment on lines +98 to +102
// SOH [Port] polyOpaBuffer enlarged from 0x2FC0. It's allocated from both
// ends (display lists grow up, matrices/vertices grow down via THGA_AllocEnd,
// which has no lower bound). Very dense frames like the Death Mountain Trail
// rockfall drive the tail below the buffer into headMagic and hard-crash
// Graph_Update (graph.c:356). More headroom keeps those frames in bounds.

@serprex serprex Jun 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// SOH [Port] polyOpaBuffer enlarged from 0x2FC0. It's allocated from both
// ends (display lists grow up, matrices/vertices grow down via THGA_AllocEnd,
// which has no lower bound). Very dense frames like the Death Mountain Trail
// rockfall drive the tail below the buffer into headMagic and hard-crash
// Graph_Update (graph.c:356). More headroom keeps those frames in bounds.

we'd already doubled the size before

guessing you're on high FPS

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yeah im running it on my 480hz monitor actually 😅 I play counter strike so im a sucker for high fps

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we need to cap fps until interpolation doesn't generate N steps per frame that all get buffered here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ahh ok yeah agreed this is just a bandaid fix, I see the there is already a fps limit of 360FPS in the settings but toggling match refresh rate overrides that. Maybe capping it at 360FPS internally would be a better solution. Anyway hmu if you want me to test any fps cap or interpolation change or anything else that I can contribute too😄

@mergify

mergify Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants