mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
gte: render-view projection override replaces guest projection explicitly; fix abort test count after merge
Override skips guest-formula PGXP shadows; composition with fov_scale tested and documented. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,11 @@ per swap, LEFT on the left, RIGHT on the right, with each eye using the configur
|
||||
display aspect. Zero disables it. The first implementation uses ordinary game
|
||||
VBlank presentation cadence; it does not submit to OpenXR or create headset poses.
|
||||
Stereo and temporal interpolation should be configured separately; SBS takes
|
||||
precedence when a compatible complete pair is available.
|
||||
precedence when a compatible complete pair is available. A pair is shown only while
|
||||
the plugin keeps submitting: it expires 24 VBlanks of guest time after its capture
|
||||
cycle (3x the slowest 8-VBlank cadence) and the presenter falls back to the normal
|
||||
flat image (menus, same-size FMV, a plugin that stopped submitting). Savestate/rollback loads and
|
||||
session resets drop the pair immediately.
|
||||
|
||||
Inside an eye callback, `psx_mod_render_view_offset(x,y,z)` replaces a scoped
|
||||
camera-space translation. RTPS and RTPT add it alongside TR before projection;
|
||||
@@ -42,6 +46,16 @@ previous host value. Calls outside a render transaction are refused. The GTE
|
||||
test compares this seam against explicit pre-divide translation and measures
|
||||
12 pixels at Z=800 versus 3 pixels at Z=3200 for X=24 and H=400.
|
||||
|
||||
`psx_mod_render_view` (full pose) is an explicit per-eye projection. With
|
||||
`projection=1` it REPLACES the guest X/Y projection entirely: guest `H`, the
|
||||
widescreen/dome squash and the `[video] fov_scale` scaled-H do not feed it, and the
|
||||
guest-formula PGXP shadows are skipped for those vertices. The only coupling is
|
||||
`projection_h_ref`: when non-zero the focal lengths are multiplied by (fov-scaled
|
||||
guest `H`) / `projection_h_ref`, so a plugin can follow per-draw H changes. SZ, the
|
||||
depth cue and divide flags still come from the guest division. The pose exists only
|
||||
inside a render transaction and is restored with it, so nothing outside the sandbox
|
||||
sees it. `test_render_pose` covers the override together with `fov_scale`.
|
||||
|
||||
TCP inspection:
|
||||
|
||||
- `stereo_stats`: pair outcomes, eye entry cycles/state hashes under VERIFY,
|
||||
|
||||
@@ -460,7 +460,10 @@ int psx_mod_render_view_offset(int32_t x, int32_t y, int32_t z);
|
||||
/* Rigid camera transform after guest RT*V+TR, before division. Rotation is
|
||||
* row-major Q12; translation uses camera units. Optional projection supplies
|
||||
* focal lengths and centre deltas from guest OFX/OFY in Q16 pixel units.
|
||||
* Identity rotation with projection=0 preserves the architectural path. */
|
||||
* Identity rotation with projection=0 preserves the architectural path.
|
||||
* projection=1 REPLACES the guest X/Y projection (guest H, widescreen squash and
|
||||
* [video] fov_scale do not apply); projection_h_ref != 0 scales the focal lengths
|
||||
* by (fov-scaled guest H)/ref. Valid only inside a render callback; restored with it. */
|
||||
typedef struct PSXModRenderView {
|
||||
uint32_t struct_size;
|
||||
int32_t rotation_q12[9], translation[3];
|
||||
|
||||
+16
-3
@@ -1054,7 +1054,20 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */
|
||||
int64_t sx16 = gte->OFX + xterm;
|
||||
int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz;
|
||||
if (s_render_pose.projection && gte->SZ[3]) {
|
||||
/* Render-view projection override (psx_mod_render_view, projection=1).
|
||||
* Only reachable inside a render-pass sandbox (set/restored by render_pass.c,
|
||||
* rolled back with the GTE state), so no guest-visible state outside the
|
||||
* pass sees it. It is an explicit per-eye projection and REPLACES the guest
|
||||
* X/Y projection above entirely: the [video] fov_scale scaled-H, the
|
||||
* widescreen/dome squash and guest H do not feed it, except through
|
||||
* projection_h_ref, which deliberately multiplies the focal lengths by
|
||||
* (fov-scaled guest H) / projection_h_ref so a plugin can follow the game's
|
||||
* own per-draw H changes (and fov_scale) when it asks to. The guest-formula
|
||||
* PGXP shadows below (precise projection, projection tracking) describe the
|
||||
* guest projection and are skipped under the override. SZ, MAC0/IR0 depth
|
||||
* cue and the divide flags still come from the guest division above. */
|
||||
const bool proj_override = s_render_pose.projection && gte->SZ[3];
|
||||
if (proj_override) {
|
||||
sx16 = (int64_t)gte->OFX + s_render_pose.cx_delta_q16 +
|
||||
(int64_t)gte->IR1 * s_render_pose.fx_q16 / gte->SZ[3];
|
||||
sy16 = (int64_t)gte->OFY + s_render_pose.cy_delta_q16 +
|
||||
@@ -1088,7 +1101,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
* to native. Shadow only; the guest SXY, MAC and FLAG above are
|
||||
* already final. Skipped while the hooks record nothing (speculative
|
||||
* passes), where the shadow would be dropped anyway. */
|
||||
if (pgxp_preserve_projection() && pgxp_active()) {
|
||||
if (!proj_override && pgxp_preserve_projection() && pgxp_active()) {
|
||||
int32_t ex16, ey16;
|
||||
if (pgxp_project_precise(mac1, mac2, mac3, shift, gte->IR1, gte->IR2,
|
||||
gte->SZ[3], gte_h_scaled(gte), gte->OFX, gte->OFY,
|
||||
@@ -1099,7 +1112,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
}
|
||||
}
|
||||
pgxp_gte_push_sxy(px16, py16, gte->SZ[3], (uint32_t)gte->SXY[2]);
|
||||
if (pgxp_projection_tracking() && shift == 12 && !lm) {
|
||||
if (!proj_override && pgxp_projection_tracking() && shift == 12 && !lm) {
|
||||
const double z = (double)(mac3 >> 12);
|
||||
const double h_proj = (double)gte_h_scaled(gte);
|
||||
double hx = (double)gte->MAC1 * h_proj;
|
||||
|
||||
@@ -1010,6 +1010,42 @@ int test_render_pose() {
|
||||
if ((int16_t)g.SXY[2] != 16 || (int16_t)(g.SXY[2] >> 16) != 1)
|
||||
return fail_value("authored focal ratio",0,0,0,16,g.SXY[2] & 65535);
|
||||
pose = {}; gte_render_pose_set(&pose);
|
||||
/* Composition with [video] fov_scale (scaled guest H): an absolute override
|
||||
* (projection_h_ref == 0) replaces the guest projection entirely, so the
|
||||
* result does not depend on fov_scale; with projection_h_ref the focal
|
||||
* lengths follow the fov-scaled guest H. Outside the override (pose reset)
|
||||
* the guest path, including fov_scale, is untouched. */
|
||||
{
|
||||
GTEState base;
|
||||
base.RT[0][0] = base.RT[1][1] = base.RT[2][2] = 4096;
|
||||
base.V0[0] = 80; base.V0[1] = 160; base.V0[2] = 800;
|
||||
base.H = 400; base.OFX = 256 << 16; base.OFY = 120 << 16;
|
||||
PSXModRenderView ov = {}; ov.struct_size = sizeof ov;
|
||||
ov.rotation_q12[0] = ov.rotation_q12[4] = ov.rotation_q12[8] = 4096;
|
||||
ov.projection = 1; ov.fx_q16 = 200 << 16; ov.fy_q16 = 100 << 16;
|
||||
GTEState a = base, b = base, c = base, guest1 = base, guest2 = base;
|
||||
gte_set_fov_scale(1, 1);
|
||||
gte_render_pose_set(&ov);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&a, a.V0, true);
|
||||
gte_set_fov_scale(1, 2);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&b, b.V0, true);
|
||||
if (a.SXY[2] != b.SXY[2])
|
||||
return fail_value("absolute override independent of fov_scale",0,0,0,a.SXY[2],b.SXY[2]);
|
||||
ov.projection_h_ref = 400;
|
||||
gte_render_pose_set(&ov); /* scaled H = 200 -> half focal */
|
||||
PSXRecomp::GTE::gte_rtps_internal(&c, c.V0, true);
|
||||
if ((int16_t)c.SXY[2] != 256 + 10 || (int16_t)(c.SXY[2] >> 16) != 120 + 10)
|
||||
return fail_value("h_ref override follows fov-scaled H",0,0,0,266,c.SXY[2] & 65535);
|
||||
PSXModRenderView none = {}; gte_render_pose_set(&none);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&guest2, guest2.V0, true);
|
||||
gte_set_fov_scale(1, 1);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&guest1, guest1.V0, true);
|
||||
if (guest1.SXY[2] == guest2.SXY[2])
|
||||
return fail_value("guest path keeps fov_scale after override reset",0,0,0,guest1.SXY[2],guest2.SXY[2]);
|
||||
if (guest1.SXY[2] == a.SXY[2])
|
||||
return fail_value("override differs from guest projection (sanity)",0,0,0,guest1.SXY[2],a.SXY[2]);
|
||||
}
|
||||
gte_set_fov_scale(1, 1);
|
||||
std::puts("PASS: rigid rotation and asymmetric projection before division");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -544,7 +544,8 @@ int main(void) {
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &runaway) == 0,
|
||||
"a runaway span is rolled back");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.watchdog == 2, "the watchdog cut the span off");
|
||||
/* Cumulative: the plain abort, the stereo right-eye abort, and this span. */
|
||||
CHECK(st.watchdog == 3, "the watchdog cut the span off");
|
||||
CHECK(s_span_hi == 0 && s_span_lo == 0, "the abort closed the open span");
|
||||
check_live(&live, "after a span watchdog abort");
|
||||
CHECK(s_prec_begins > 0 && s_prec_open == 0,
|
||||
|
||||
Reference in New Issue
Block a user