fix(gum): store into the current slot before advancing in sceGumPushMatrix - #206
Open
ItsNoHax wants to merge 1 commit into
Open
fix(gum): store into the current slot before advancing in sceGumPushMatrix#206ItsNoHax wants to merge 1 commit into
ItsNoHax wants to merge 1 commit into
Conversation
ItsNoHax
force-pushed
the
fix-gum-push-matrix-stack-slot
branch
from
August 13, 2026 22:33
15a633c to
e507375
Compare
…atrix `sceGumPushMatrix` advanced `CURRENT_MATRIX` and then stored the VFPU matrix, so it saved into the slot above the stack pointer. `sceGumPopMatrix` decrements and then loads, so it reads the slot below the pointer it was left at. The two never refer to the same slot, and what pop returns is not what push saved. `CURRENT_MATRIX` names the slot that mirrors the live matrix -- `sceGumMatrixMode` and `sceGumUpdateMatrix` both write the VFPU matrix to `*CURRENT_MATRIX` -- so storing before the increment is also the only ordering that works. Saving into the slot above and having pop read that slot back is self-consistent in isolation, but `sceGumUpdateMatrix` writes the live matrix to exactly that slot, and `sceGumDrawArray` calls it, so the first draw after a push would overwrite what the push saved. This matches PSPSDK's pspgum.c, which stores and then increments. The bug hid behind that same sync. `MATRIX_STACK` is zero initialised, so a pop with nothing beneath it loads an all-zero matrix and collapses everything drawn afterwards to a point; but any draw performed before the first push syncs the correct matrix into the slot pop reads, after which push/pop pairs appear to work. Code that drew before pushing was fine. Code whose first drawing act was a push -- for example a model whose wheel meshes are ordered before its body meshes -- lost every mesh after the first pop. Adds four regression tests to ci/tests covering push/pop, nesting, a matrix write between push and pop, and that a push leaves the working matrix alone. Against master the first three fail with (0.0, 0.0, 0.0) against the expected translation and the suite ends FINAL_FAILURE; with this change all four pass and the suite ends FINAL_SUCCESS.
ItsNoHax
force-pushed
the
fix-gum-push-matrix-stack-slot
branch
from
August 13, 2026 23:08
e507375 to
85f72b9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #205.
The bug
sceGumPushMatrixadvancesCURRENT_MATRIXand then stores the VFPU matrix, so it saves into theslot above the stack pointer.
sceGumPopMatrixdecrements and then loads, so it reads the slotbelow the pointer it was left at. Push writes
P+1, pop readsP; the saved copy is never readback.
The fix
Store into the slot
CURRENT_MATRIXalready points at, then advance — the ordering PSPSDK'spspgum.cuses. One statement moved.This is also the only ordering that survives a matrix sync.
CURRENT_MATRIXnames the slot thatmirrors the live matrix: both
sceGumMatrixModeandsceGumUpdateMatrixwrite the VFPU matrix to*CURRENT_MATRIX. Saving into the slot above the pointer and having pop read that slot back isself-consistent in isolation, but
sceGumDrawArraycallssceGumUpdateMatrix, so the first drawafter a push would overwrite exactly what the push saved. Storing below the pointer keeps the saved
copy out of the way of the sync.
Why this went unnoticed
The sync is also what hides it. Any draw performed before the first push writes the correct matrix
into the slot pop later reads, after which push/pop pairs appear to work. So the failure depends on
draw order rather than on the matrix code, and only shows when something pushes before it has drawn
anything — at which point pop loads a slot nothing ever wrote.
MATRIX_STACKis zero-initialised,so the restored matrix is all zeros and everything drawn afterwards collapses to a point.
The report in #205 has the real-world version: a car drawn as a body plus four wheels, where six
assets ordered their body meshes first and worked, and the seventh ordered its wheels first and lost
its entire body.
Tests
Four cases in
ci/tests, all pure matrix arithmetic read back withsceGumStoreMatrix— norendering, so they need no GU:
gum_push_pop_restores_translation— the core propertygum_push_pop_nested_restores_translation— two deep, catching an off-by-one either waygum_push_pop_survives_matrix_write— a write to*CURRENT_MATRIXbetween push and pop, which iswhat a draw does; this is the case that rules out the alternative fix
gum_push_leaves_current_matrix_alone— a push saves the matrix without disturbing itsceGumMatrixModeprovokes the write in the third rather thansceGumUpdateMatrixitself, becausesceGumUpdateMatrixends by handing every stack tosceGuSetMatrixand this suite never brings theGU up. It stores through the same four instructions.
Run with
PPSSPPHeadless ... --timeout=40 -r .asci/concourse/run-tests.shdoes.Before (master, a8be764):
After:
Verification
Verified end to end: a game that had been working around this by rebuilding its model matrix per
mesh was reverted to plain
sceGumPushMatrix/sceGumPopMatrixand built twice against identicalgame source and assets — once against master, once against this branch. On master the affected car
renders as a few pixels; on this branch it renders correctly.
The rest of the suite is unaffected, and
cargo fmtis clean for bothpspandci/tests.One incidental note found while testing this: opening with
sceGumMatrixModeon a cold VFPU contextis a break instruction rather than a panic, since only
sceGumLoadIdentityandsceGumLoadMatrixcreate the context and everything else calls
get_context_unchecked. That is #189 and is nottouched here.