Skip to content

Commit 34f233c

Browse files
ItsNoHaxsajattack
authored andcommitted
fix(gum): store into the current slot before advancing in sceGumPushMatrix
`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.
1 parent 1ed55f8 commit 34f233c

3 files changed

Lines changed: 122 additions & 1 deletion

File tree

ci/tests/src/gum_test.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//! Regression tests for the sceGum matrix stack: a pop restores what its matching push saved.
2+
//!
3+
//! Only the translation column is compared, since it is enough to tell the cases apart and reads
4+
//! better in a failure message than sixteen floats.
5+
6+
use psp::sys::{self, MatrixMode, ScePspFMatrix4, ScePspFVector3, ScePspFVector4};
7+
use psp::test_runner::TestRunner;
8+
9+
fn zero_matrix() -> ScePspFMatrix4 {
10+
let zero = ScePspFVector4 {
11+
x: 0.0,
12+
y: 0.0,
13+
z: 0.0,
14+
w: 0.0,
15+
};
16+
ScePspFMatrix4 {
17+
x: zero,
18+
y: zero,
19+
z: zero,
20+
w: zero,
21+
}
22+
}
23+
24+
fn translation() -> (f32, f32, f32) {
25+
let mut m = zero_matrix();
26+
unsafe { sys::sceGumStoreMatrix(&mut m) };
27+
(m.w.x, m.w.y, m.w.z)
28+
}
29+
30+
fn translate(x: f32, y: f32, z: f32) {
31+
unsafe { sys::sceGumTranslate(&ScePspFVector3 { x, y, z }) };
32+
}
33+
34+
/// The leading `sceGumLoadIdentity` is not redundant: it and `sceGumLoadMatrix` are the only entry
35+
/// points that create the VFPU context, and `sceGumMatrixMode` on a cold one traps. See #189.
36+
fn reset() {
37+
unsafe {
38+
sys::sceGumLoadIdentity();
39+
sys::sceGumMatrixMode(MatrixMode::Model);
40+
sys::sceGumLoadIdentity();
41+
}
42+
}
43+
44+
pub fn test_main(test_runner: &mut TestRunner) {
45+
test_runner.check_list(&[
46+
(
47+
"gum_push_pop_restores_translation",
48+
push_pop_restores(),
49+
(1.0, 2.0, 3.0),
50+
),
51+
(
52+
"gum_push_pop_nested_restores_translation",
53+
nested_push_pop_restores(),
54+
(1.0, 2.0, 3.0),
55+
),
56+
(
57+
"gum_push_pop_survives_matrix_write",
58+
push_pop_survives_matrix_write(),
59+
(1.0, 2.0, 3.0),
60+
),
61+
(
62+
"gum_push_leaves_current_matrix_alone",
63+
push_does_not_disturb_current(),
64+
(1.0, 2.0, 3.0),
65+
),
66+
]);
67+
}
68+
69+
fn push_pop_restores() -> (f32, f32, f32) {
70+
reset();
71+
translate(1.0, 2.0, 3.0);
72+
unsafe { sys::sceGumPushMatrix() };
73+
translate(10.0, 20.0, 30.0);
74+
unsafe { sys::sceGumPopMatrix() };
75+
translation()
76+
}
77+
78+
/// Two deep, so a stack that is off by one in either direction is caught.
79+
fn nested_push_pop_restores() -> (f32, f32, f32) {
80+
reset();
81+
translate(1.0, 2.0, 3.0);
82+
unsafe { sys::sceGumPushMatrix() };
83+
translate(10.0, 20.0, 30.0);
84+
unsafe { sys::sceGumPushMatrix() };
85+
translate(100.0, 200.0, 300.0);
86+
unsafe { sys::sceGumPopMatrix() };
87+
unsafe { sys::sceGumPopMatrix() };
88+
translation()
89+
}
90+
91+
/// A sync to `*CURRENT_MATRIX` between push and pop must not eat the saved copy. This is what a
92+
/// draw does, and it rules out saving into the slot above the stack pointer. A mode switch stands
93+
/// in for `sceGumUpdateMatrix`, which stores through the same instructions but ends in
94+
/// `sceGuSetMatrix`, and this suite never brings the GU up.
95+
fn push_pop_survives_matrix_write() -> (f32, f32, f32) {
96+
reset();
97+
translate(1.0, 2.0, 3.0);
98+
unsafe { sys::sceGumPushMatrix() };
99+
translate(10.0, 20.0, 30.0);
100+
unsafe {
101+
sys::sceGumMatrixMode(MatrixMode::View);
102+
sys::sceGumMatrixMode(MatrixMode::Model);
103+
sys::sceGumPopMatrix();
104+
}
105+
translation()
106+
}
107+
108+
/// Pushing saves the matrix without disturbing it.
109+
fn push_does_not_disturb_current() -> (f32, f32, f32) {
110+
reset();
111+
translate(1.0, 2.0, 3.0);
112+
unsafe { sys::sceGumPushMatrix() };
113+
let after_push = translation();
114+
unsafe { sys::sceGumPopMatrix() };
115+
after_push
116+
}

ci/tests/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extern crate alloc;
88
use psp::test_runner::TestRunner;
99

1010
mod bmp_screenshot_test;
11+
mod gum_test;
1112
mod math_test;
1213
mod vfpu_test;
1314
mod vram_test;
@@ -17,6 +18,7 @@ psp::module!("ci_tests", 1, 1);
1718
fn psp_main() {
1819
let tests = &[
1920
bmp_screenshot_test::test_main,
21+
gum_test::test_main,
2022
math_test::test_main,
2123
vfpu_test::test_main,
2224
vram_test::test_main,

psp/src/sys/gum.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,6 @@ pub unsafe extern "C" fn sceGumPopMatrix() {
458458
#[allow(non_snake_case)]
459459
#[no_mangle]
460460
pub unsafe extern "C" fn sceGumPushMatrix() {
461-
CURRENT_MATRIX = CURRENT_MATRIX.offset(1);
462461
get_context_unchecked().prepare(MatrixSet::VMAT3, MatrixSet::empty());
463462

464463
vfpu_asm!(
@@ -469,6 +468,10 @@ pub unsafe extern "C" fn sceGumPushMatrix() {
469468
in(reg) CURRENT_MATRIX,
470469
options(nostack),
471470
);
471+
472+
// Advance only after the store: `CURRENT_MATRIX` mirrors the live matrix, so the saved copy has
473+
// to land in the slot below it, which is where `sceGumPopMatrix` looks.
474+
CURRENT_MATRIX = CURRENT_MATRIX.offset(1);
472475
}
473476

474477
/// Rotate around the X axis

0 commit comments

Comments
 (0)