D955: at 90+ fps, object motion and the sky are blended between ticks

Guards, doors and props moved at the 60 Hz tick inside a 90 Hz picture. Each object's joint
matrices are now blended, keyed by (Model*, joint) from a game-thread snapshot and rebased onto
the new camera; a chr blends together with its held weapons and hat; room-origin changes, failed
gates and split-screen stay exact. The CPU-projected sky is re-projected to the in-between camera.
Destroyed objects (world-space matrices) stay exact. Port layer only; porting-notes D46.
This commit is contained in:
James Dansereau committed 2026-10-13 22:09:22 -04:00
1 parent b7280e5386
commit a77bd4e03d
8 files changed
+568 -2

No files matched your search

+1
View File
@@ -201,6 +201,7 @@ A single campaign pass with these on the checklist would clear most of them.
| Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. | | Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. |
| Alt-tab haze + fast alt-tab recovery | open (v0.7.0, maintainer 2026-10-10) | D578 | Maintainer 2026-10-10 (v0.6.0 build, Windows 120 Hz): alt-tabbing leaves a hazy picture; fast repeated alt-tab is wonky and recovers only after a while or more alt-tabbing. Not new in v0.6.0 (was an owed check). Known issue in the v0.6.0 notes. QoL fix: clean focus-loss/regain path (context/FBO state, present queue reset). | | Alt-tab haze + fast alt-tab recovery | open (v0.7.0, maintainer 2026-10-10) | D578 | Maintainer 2026-10-10 (v0.6.0 build, Windows 120 Hz): alt-tabbing leaves a hazy picture; fast repeated alt-tab is wonky and recovers only after a while or more alt-tabbing. Not new in v0.6.0 (was an owed check). Known issue in the v0.6.0 notes. QoL fix: clean focus-loss/regain path (context/FBO state, present queue reset). |
| Doors and guards flicker at 90 fps (not at 60) | fixed 2026-10-13 (D954); maintainer Deck check owed | D954 | In-between presents drew props (chrs, doors, effects) at the exact camera while rooms used the blended one, so props jumped against the world whenever the camera moved (Surface turn on Windows: 0.4-0.5 deg mean, up to 1.8 deg). Present in v0.6.0 at 90 fps. Fix in `gfx_pc.cpp`: props' plain perspective gets the room camera blend (`P * R^-1 * Rb`) up to the first-person gun. **Owed:** Deck at 90 fps, Surface doors/guards, and the gun must not move while turning. | | Doors and guards flicker at 90 fps (not at 60) | fixed 2026-10-13 (D954); maintainer Deck check owed | D954 | In-between presents drew props (chrs, doors, effects) at the exact camera while rooms used the blended one, so props jumped against the world whenever the camera moved (Surface turn on Windows: 0.4-0.5 deg mean, up to 1.8 deg). Present in v0.6.0 at 90 fps. Fix in `gfx_pc.cpp`: props' plain perspective gets the room camera blend (`P * R^-1 * Rb`) up to the first-person gun. **Owed:** Deck at 90 fps, Surface doors/guards, and the gun must not move while turning. |
| Objects move at 60 Hz steps at 90 fps (props, doors, guards) | implemented 2026-10-13 (D955); maintainer Deck check owed | D955 | In-between presents drew objects at their tick pose (only the camera was blended). Now blended per joint, keyed by (Model*, joint) from a game-thread snapshot, rebased onto the new camera; exact on room-origin changes, failed gates, split-screen. Dam on Windows: 99.6% of object joint loads blended. **Owed:** Deck at 90: guards walking, gun in hand, hats, deaths, shadows, doors, Facility stall doors/sinks (the old ghost site), room boundaries. Vehicles later. |
| Windows display scaling: the game is not DPI-aware on scaled displays | fixed 2026-10-10 (D626); maintainer by-eye on a scaled display owed | D626 | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. | | Windows display scaling: the game is not DPI-aware on scaled displays | fixed 2026-10-10 (D626); maintainer by-eye on a scaled display owed | D626 | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. |
## 5. Modern-remaster layer (port features beyond the N64) ## 5. Modern-remaster layer (port features beyond the N64)
+1
View File
@@ -28,6 +28,7 @@ Two classes:
| `GE_INTERPPORTAL_OFF` | `port/fast3d/gfx_pc.cpp:4156` | D578: disable the portal-scissor replay on in-between passes. | live switch | | `GE_INTERPPORTAL_OFF` | `port/fast3d/gfx_pc.cpp:4156` | D578: disable the portal-scissor replay on in-between passes. | live switch |
| `GE_INTERPROOM_OFF` | `port/fast3d/gfx_pc.cpp:6059` | D578: disable the room-change exact fallback. | live switch | | `GE_INTERPROOM_OFF` | `port/fast3d/gfx_pc.cpp:6059` | D578: disable the room-change exact fallback. | live switch |
| `GE_INTERPREACH_OFF` | `port/fast3d/gfx_pc.cpp:6128` | D583: disable the REACH exact fallback. | live switch | | `GE_INTERPREACH_OFF` | `port/fast3d/gfx_pc.cpp:6128` | D583: disable the REACH exact fallback. | live switch |
| `GE_INTERPOBJ_OFF` | `port/fast3d/gfx_pc.cpp:2626` | D955: in-between presents draw objects at their tick pose again (no per-joint motion blend; counters still run). A group whose joints step more than 120 units in a tick is drawn exact. | live switch |
| `GE_INTERPPROPCAM_OFF` | `port/fast3d/gfx_pc.cpp:2436` | D954: in-between presents draw props at the exact camera again (no room-camera blend on the plain perspective). | live switch | | `GE_INTERPPROPCAM_OFF` | `port/fast3d/gfx_pc.cpp:2436` | D954: in-between presents draw props at the exact camera again (no room-camera blend on the plain perspective). | live switch |
| `GE_PCDUMP_PRESENTS` | `port/src/video.c:1539` | With `GE_PCDUMP`: one capture per present instead of per game frame. | live tooling | | `GE_PCDUMP_PRESENTS` | `port/src/video.c:1539` | With `GE_PCDUMP`: one capture per present instead of per game frame. | live tooling |
| `GE_NOTICKLOCK` | `port/src/libultra.c:1321` | D600: disable the display-locked retrace tick. | live switch | | `GE_NOTICKLOCK` | `port/src/libultra.c:1321` | D600: disable the display-locked retrace tick. | live switch |
+1
View File
@@ -465,6 +465,7 @@ D646,FIXED,"F10 dropdown popup drew over the hint/tooltip panel (popup bounded b
D951,OPEN,"v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes —",OPEN (test-side; 2026-10-13) D951,OPEN,"v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes —",OPEN (test-side; 2026-10-13)
D952,FIXED,"macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started) —","FIXED 2026-10-13 (port-only, gfx_sdl2.cpp; Mac A/B:" D952,FIXED,"macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started) —","FIXED 2026-10-13 (port-only, gfx_sdl2.cpp; Mac A/B:"
D954,FIXED,"Doors and guards flicker at 90 fps, not at 60:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; Windows-measured; Deck check owed)" D954,FIXED,"Doors and guards flicker at 90 fps, not at 60:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; Windows-measured; Deck check owed)"
D955,OPEN,"Objects (props, doors, guards) moved at the 60 Hz tick inside a 90 Hz picture:","IMPLEMENTED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; Windows-measured; Deck feel check owed)"
D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate: D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate:
D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)" D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)"
D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)" D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
465 D951
466 D952
467 D954
468 D955
469 D607
470 D608
471 D609
+20
View File
@@ -876,6 +876,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D951 | **v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes** — full `## D951` entry at file tail | OPEN (test-side; 2026-10-13) | | D951 | **v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes** — full `## D951` entry at file tail | OPEN (test-side; 2026-10-13) |
| D952 | **macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started)** — full `## D952` entry at file tail | FIXED 2026-10-13 (port-only, `gfx_sdl2.cpp`; Mac A/B: unfixed 3/3 hang, fixed 0/5) | | D952 | **macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started)** — full `## D952` entry at file tail | FIXED 2026-10-13 (port-only, `gfx_sdl2.cpp`; Mac A/B: unfixed 3/3 hang, fixed 0/5) |
| D954 | **Doors and guards flicker at 90 fps, not at 60: in-between presents drew props at the exact camera while rooms used the blended one** — full `## D954` entry at file tail | FIXED 2026-10-13 (port-only, `gfx_pc.cpp` + `interpportal.c`; Windows-measured; Deck check owed) | | D954 | **Doors and guards flicker at 90 fps, not at 60: in-between presents drew props at the exact camera while rooms used the blended one** — full `## D954` entry at file tail | FIXED 2026-10-13 (port-only, `gfx_pc.cpp` + `interpportal.c`; Windows-measured; Deck check owed) |
| D955 | **Objects (props, doors, guards) moved at the 60 Hz tick inside a 90 Hz picture: in-between presents now blend object motion per joint** — full `## D955` entry at file tail | IMPLEMENTED 2026-10-13 (port-only, `gfx_pc.cpp` + `interpportal.c`; Windows-measured; Deck feel check owed) |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
@@ -22029,3 +22030,22 @@ FpsCap = 60'`: 20/21 PASS; statue REGRESSION (4.66%, frame 900).
**Verified.** Windows build; Surface run as above: convention check ok, 0 passes without a snapshot, the gun disarm hit 431 times per 10 s of gameplay (the remaining passes had no gun drawn). New 10 s log line `D954 prop camera: ...`. Deck build (release recipe: RelWithDebInfo, gcc-14, rpath `$ORIGIN`, bundled SDL2 2.30.0) deployed to the maintainer's shortcut. **Owed:** maintainer Deck check at 90 fps (Surface doors and guards; also the first-person gun while turning, which must not move). **Verified.** Windows build; Surface run as above: convention check ok, 0 passes without a snapshot, the gun disarm hit 431 times per 10 s of gameplay (the remaining passes had no gun drawn). New 10 s log line `D954 prop camera: ...`. Deck build (release recipe: RelWithDebInfo, gcc-14, rpath `$ORIGIN`, bundled SDL2 2.30.0) deployed to the maintainer's shortcut. **Owed:** maintainer Deck check at 90 fps (Surface doors and guards; also the first-person gun while turning, which must not move).
**Status.** FIXED (Deck confirmation owed). **Status.** FIXED (Deck confirmation owed).
## D955: object motion at 90+ fps — props, doors and guards moved at the 60 Hz tick inside a 90 Hz picture — IMPLEMENTED 2026-10-13 (port layer; maintainer Deck check owed)
**Symptom (maintainer, Deck, 90 fps, D954 build).** "Models/objects kinda lag behind in a noticeable way, almost like tearing except nothing is tearing, those just seem to update not at the right speed." Rooms and the camera are smooth.
**Cause (code).** In-between presents draw every modelview exactly as loaded (D578 Cause 2 fix, Deck A/B 2026-10-09: nearest-transform pairing matched identical models to the wrong instance and ghosted); D954 moves only the camera. So every object kept its tick pose for the whole tick: object motion advanced on 2 of 3 presents at 90 Hz.
**Facts the design rests on (verified in code; subagent review `docs/dev/notes/REVIEW-D955-INTERP-OBJECT-MOTION.md`).** GE models are matrix-skinned, not CPU-skinned: each instance's joint matrices for the tick are in `model->render_pos` (dyn pool; `model.c:2423` `render_pos = mtxlist`), converted in place to fixed point, and the model DLs load them through segment 3 (`model.c:5490` `gSPSegment(3, render_pos)`); the vertices are the model file's shared static arrays. The camera is baked into those matrices (`chr.c:2730` `basemtx = camGetWorldToScreenMtxf()`). Every view matrix is relative to `current_model_pos`, the current ROOM's origin (`bondview2.c:8287`), which jumps at a room change. (GEVR's interpolation notes say chrs are CPU-skinned and that the origin changes every frame; both are wrong.)
**Fix (`port/fast3d/gfx_pc.cpp`, `port/src/interpportal.c`; no game code, no game hooks).** Game thread, with the frame's task (D31 snapshot): the on-screen props (`g_OnScreenPropList`) -> per model instance `{render_pos, numMatrices, Model*, model->obj, group, pos}` plus `current_model_pos`; a chr groups with its held weapons, hat and `prop->child` attachments. Explosions, smoke, the viewer and CPU-resized doors (CLIP_TO_BBOX / eye / iris, the `propobj.c:9555` test) are left out. Renderer, once per game frame: read the tick's joint matrices from the instance blocks, pair with the previous frame by (Model*, model->obj), rebase the previous pose onto the current camera, `D = L_{N-1}^-1 * L_N` with `L = R * P^-1` (so the camera is not blended twice: D954 carries it), gate per joint (step <= `GE_INTERPOBJ_STEP` 120 units, rotation < ~35 deg, scale), and decide per group. In the D954 window of a blend pass, a MODELVIEW LOAD whose source lies in an instance block is replaced by `lerp_rows(MV_{N-1} * D, MV_N, alpha)` (basis lengths kept). Exact for: a room-origin change, a non-consecutive or missing previous frame, a changed model file, a failed gate anywhere in the group, split-screen, menus, 60 fps. A previous pose is kept only for instances this frame's DL actually loaded (an on-screen-list entry that was not drawn has a stale `render_pos`). `GE_INTERPOBJ_OFF=1` disables the blend; the 10 s line `D955 obj: ...` reports frames, instances, groups blended/exact, joint loads blended/keyed, unkeyed loads in the window, gate rejects, max step and a static residual.
**Measured (Windows, 1280x800, FpsCap 90, `GE_INTERP_FORCE=1`, scripted turn/walk/strafe).** Rebase check: props whose `pos` did not change land on themselves after `MV_{N-1} * D` to 1.6e-06 mean / 3.1e-05 max (Surface intro flyby, 13,325 joints). On Dam the same residual reads 0.01-0.18 from two single-joint OBJ models whose residual tracks the turn rate (camera-facing sprites; harmless to blend). Dam gameplay, blend on, per 10 s: 33,817 of 33,952 keyed joint loads blended, groups 2,487 blended / 16 exact, 3 gate rejects (a 127-unit step), 2 room-origin changes caught; ~18k unkeyed loads in the window are the room modelviews (~30 rooms x 617 passes); presents 89.6/s (no cost visible on Windows). Surface's spawn has almost nothing on screen and its intro flyby has every present exact (fast camera), so Dam is the test level.
**Owed.** Maintainer Deck check at 90 fps: guards walking across the view (standing and turning), held gun stays in the hand, hats, a guard dying, shadows, doors opening, the Facility bathroom stall doors and sinks while strafing (the D578 ghost site), crossing a room boundary with a guard in view. Vehicles (tank, train) and a per-type gate split are a later stage.
**Pass 2 (2026-10-13, same evening).** (a) The sky and sky water are pre-projected on the CPU for camera N (`sky.c` G_FLOATVTX_EXT clip-space verts), so on in-between presents the horizon stayed at camera N while the world took the blended camera. Per blend pass the camera-space delta `C = P * R^-1 * Rb * P^-1` (rotation part; the sky is at infinity) is applied to each sky vertex: view point rebuilt from (x_c/sx, y_c/sy, -w), rotated, re-projected; S/T unchanged. Dam: 556-589 passes re-projected per 10 s. (b) Destroyed objects draw world-space matrices under the room projection (`propobj.c` objRender `destroyed` branch), where the camera rebase is wrong: object joints are blended only while the props' corrected plain perspective is loaded (`s_pcam_applied`). (c) The first-person weapon is NOT blended: its loads barely ever come from `hand->weaponModel.render_pos` (counted: under 5 per frame), so it is drawn through another matrix path; follow-up. Dam after (a)+(b): 32,013 of 32,140 keyed joint loads blended, 0 gate rejects, 90.1 presents/s.
Maintainer, Deck, 90 fps (2026-10-13): guards and doors smooth.
**Status.** IMPLEMENTED (Deck check owed).
+4
View File
@@ -2016,3 +2016,7 @@ IA4 = 3-bit intensity + 1-bit alpha (nibble = I<<1 | A). An I2:A2 decode (D266)
## D45. Interpolating GE: rooms carry the camera in the projection, props carry it in the modelview (D954) ## D45. Interpolating GE: rooms carry the camera in the projection, props carry it in the modelview (D954)
GE loads two projections per view: `field_10E0` = perspective * lookat for rooms (modelviews are scale*translation), and the plain perspective `projmatrix` for chrs, doors, props, glass, explosions and the gun (camera baked into each modelview; LOD reads `-m[3][2]` as view depth). An interpolator that blends only the projection moves the rooms and leaves every prop at the exact camera, which reads as props flickering against the world at high frame rates only. Give the plain perspective the room camera blend (`P * R^-1 * Rb`, row vectors, `R = L * P`), and stop before anything camera-attached (the gun inherits the loaded projection; its `gSPPerspNormalize` marks the boundary). Check the matrix order at runtime (`R * P^-1` affine) instead of assuming it. GE loads two projections per view: `field_10E0` = perspective * lookat for rooms (modelviews are scale*translation), and the plain perspective `projmatrix` for chrs, doors, props, glass, explosions and the gun (camera baked into each modelview; LOD reads `-m[3][2]` as view depth). An interpolator that blends only the projection moves the rooms and leaves every prop at the exact camera, which reads as props flickering against the world at high frame rates only. Give the plain perspective the room camera blend (`P * R^-1 * Rb`, row vectors, `R = L * P`), and stop before anything camera-attached (the gun inherits the loaded projection; its `gSPPerspNormalize` marks the boundary). Check the matrix order at runtime (`R * P^-1` affine) instead of assuming it.
## D46. GE models are matrix-skinned; every view matrix is relative to the current room's origin (D955)
Chrs and objects load per-joint matrices from `model->render_pos` through segment 3 (`gSPSegment(3, render_pos)` in `subdraw`); the vertices are the model file's shared static arrays, so per-object identity lives in the matrix block, not in vertex buffers (interpolate matrices, keyed by (Model*, joint); never by draw ordinal, which shifts with LOD, weapons, hats and decals). Those matrices have the camera baked in, and every view matrix (rooms, props, the lookat in `field_10E0`) is relative to `current_model_pos`, the current ROOM's origin, which jumps at a room change: anything that compares matrices across ticks must rebase by the camera delta and treat a room change as a discontinuity.
+413 -2
View File
@@ -2564,6 +2564,300 @@ static void gfx_pcam_disarm(void) {
s_pcam_armed = false; s_pcam_armed = false;
} }
/* D955: object motion at 90+ fps. In-between passes drew every object at its
* tick-N pose (D578 draws modelviews exactly; D954 only moves the camera), so
* props and chrs stepped at the 60 Hz tick inside a 90 Hz picture. GE models
* are matrix-skinned: each instance's joint matrices for the tick sit in
* model->render_pos (dyn pool, nmtx fixed-point Mtx), loaded by the model DLs
* through segment 3; the camera is baked in (basemtx = camGetWorldToScreenMtxf).
* Identity comes from the game-thread snapshot (interpPortalObjInstances): a
* G_MTX MODELVIEW LOAD whose source lies in an instance's block is
* (Model*, joint). Blend, per joint, in camera-N space:
* D = L_{N-1}^-1 * L_N (L_k = R_k * P_k^-1, row vectors, R = L * P)
* MVb = lerp_rows(MV_{N-1} * D, MV_N, alpha)
* so the object takes its blended world pose, and D954's projection then
* carries the blended camera. A group (a chr, its held weapons and hat) blends
* or stays exact as a whole. Exact for: a room-origin change (every MV is
* relative to current_model_pos, which jumps at a room change), a missing or
* non-consecutive previous frame, a model whose file changed, a joint that
* moved further than a tick plausibly allows (teleport, respawn), split-screen
* and anything outside the D954 window (effects, gun, HUD). A previous pose is
* only kept for instances whose matrices this frame's DL actually loaded (an
* on-screen-list entry that was not drawn has a stale render_pos).
* GE_INTERPOBJ_OFF=1 disables it (counters still run). */
struct ObjKey {
const void* model;
const void* modelobj;
bool operator==(const ObjKey& o) const { return model == o.model && modelobj == o.modelobj; }
};
struct ObjKeyHash {
size_t operator()(const ObjKey& k) const { return std::hash<const void*>()(k.model) ^ (std::hash<const void*>()(k.modelobj) << 1); }
};
struct ObjPose {
std::vector<InterpMtx> m; /* decoded joint matrices of the tick */
float pos[3];
};
struct ObjInst {
uintptr_t lo, hi; /* render_pos block */
int nmtx, group, type, rigid;
ObjKey key;
bool hit; /* a load of this frame's DL used it */
bool blend; /* this frame: blend (pair + gate + group ok) */
std::vector<InterpMtx> cur, a; /* tick N; tick N-1 rebased by D */
std::vector<InterpMtx> out; /* this pass's blended matrices */
float pos[3];
float alpha_out; /* alpha `out` was built for */
};
static std::vector<ObjInst> s_obj; /* this frame, sorted by lo */
static std::unordered_map<ObjKey, ObjPose, ObjKeyHash> s_obj_prev;
static float s_obj_Lprev[4][4];
static float s_obj_origin_prev[3];
static unsigned s_obj_serial_prev = 0;
static bool s_obj_have_prev = false;
static bool s_obj_frame = false; /* s_obj holds this frame */
static unsigned s_obj_cnt_frames = 0, s_obj_cnt_noprev = 0, s_obj_cnt_origin = 0, s_obj_cnt_inst = 0,
s_obj_cnt_grp_blend = 0, s_obj_cnt_grp_exact = 0, s_obj_cnt_loads = 0, s_obj_cnt_loads_blend = 0,
s_obj_cnt_unkeyed = 0, s_obj_cnt_gate = 0, s_obj_cnt_static = 0;
static float s_obj_res_sum = 0.0f, s_obj_res_max = 0.0f, s_obj_step_max = 0.0f;
static bool interp_obj_off(void) {
static int off = -1;
if (off < 0) {
off = getenv("GE_INTERPOBJ_OFF") != nullptr ? 1 : 0;
}
return off == 1;
}
/* Could a (rebased) and b be one joint one tick apart? World units; a tank
* or the train moves a few tens of units per tick, a teleport hundreds. */
static bool gfx_obj_plausible(const float a[4][4], const float b[4][4], float* stepOut) {
float dt = 0.0f;
for (int j = 0; j < 3; j++) {
dt += (a[3][j] - b[3][j]) * (a[3][j] - b[3][j]);
}
dt = sqrtf(dt);
*stepOut = dt;
if (dt > GE_ENVF("GE_INTERPOBJ_STEP", 120.0f)) {
return false;
}
for (int i = 0; i < 3; i++) {
float la = 0.0f, lb = 0.0f, d = 0.0f;
for (int j = 0; j < 3; j++) {
la += a[i][j] * a[i][j];
lb += b[i][j] * b[i][j];
d += a[i][j] * b[i][j];
}
if (la < 1e-12f || lb < 1e-12f) {
return false;
}
const float r = sqrtf(la / lb);
if (r < 0.8f || r > 1.25f || d / sqrtf(la * lb) < 0.82f) { /* ~35 deg, scale jump */
return false;
}
}
return true;
}
/* Start of a game frame (before its passes): read the snapshot and this
* tick's matrices, pair with the previous frame, decide per group. */
static void gfx_obj_frame_begin(void) {
s_obj.clear();
s_obj_frame = false;
const IpObjInst* in = nullptr;
float origin[3];
unsigned serial = 0;
const int n = interpPortalObjInstances(&in, origin, &serial);
int32_t pw[16], rw[16];
float P[4][4], R[4][4], Pinv[4][4], L[4][4];
if (n <= 0 || !interpPortalCamMatrices(pw, rw)) {
s_obj_have_prev = false;
return;
}
gfx_decode_mtx_words(pw, P);
gfx_decode_mtx_words(rw, R);
if (!gfx_mat4_inverse(P, Pinv)) {
s_obj_have_prev = false;
return;
}
gfx_matrix_mul(L, R, Pinv);
s_obj_cnt_frames++;
s_obj.resize(n);
for (int i = 0; i < n; i++) {
ObjInst& o = s_obj[i];
o.lo = (uintptr_t)in[i].render_pos;
o.hi = o.lo + (uintptr_t)in[i].nmtx * 64u;
o.nmtx = in[i].nmtx;
o.group = in[i].group;
o.type = in[i].type;
o.rigid = in[i].rigid;
o.key = ObjKey{ in[i].model, in[i].modelobj };
o.hit = false;
o.blend = false;
o.alpha_out = -1.0f;
memcpy(o.pos, in[i].pos, sizeof(o.pos));
o.cur.resize(o.nmtx);
for (int j = 0; j < o.nmtx; j++) {
gfx_decode_mtx_words((const int32_t*)(o.lo + (uintptr_t)j * 64u), o.cur[j].m);
}
}
std::sort(s_obj.begin(), s_obj.end(), [](const ObjInst& x, const ObjInst& y) { return x.lo < y.lo; });
s_obj_cnt_inst += (unsigned)n;
s_obj_frame = true;
const bool sameOrigin = s_obj_have_prev && !memcmp(origin, s_obj_origin_prev, sizeof(origin));
const bool consecutive = s_obj_have_prev && serial == s_obj_serial_prev + 1;
float D[4][4], Lpinv[4][4];
bool haveD = consecutive && sameOrigin && gfx_mat4_inverse(s_obj_Lprev, Lpinv);
if (haveD) {
gfx_matrix_mul(D, Lpinv, L);
}
if (!consecutive) {
s_obj_cnt_noprev++;
} else if (!sameOrigin) {
s_obj_cnt_origin++;
}
/* Pair + gate per instance; a group is exact if any member fails. */
std::unordered_map<int, bool> grpOk;
for (ObjInst& o : s_obj) {
bool ok = false;
if (haveD) {
auto it = s_obj_prev.find(o.key);
if (it != s_obj_prev.end() && (int)it->second.m.size() == o.nmtx) {
ok = true;
o.a.resize(o.nmtx);
const bool still = o.rigid && !memcmp(it->second.pos, o.pos, sizeof(o.pos));
for (int j = 0; j < o.nmtx; j++) {
gfx_matrix_mul(o.a[j].m, it->second.m[j].m, D);
float step;
if (!gfx_obj_plausible(o.a[j].m, o.cur[j].m, &step)) {
ok = false;
s_obj_cnt_gate++;
}
if (step > s_obj_step_max) {
s_obj_step_max = step;
}
if (still) {
/* Stage 0 check: an object that did not move must
* land exactly on itself after the rebase. */
float res = 0.0f, sc = 1.0f;
for (int c = 0; c < 3; c++) {
sc = fmaxf(sc, fabsf(o.cur[j].m[3][c]));
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 3; c++) {
const float e = fabsf(o.a[j].m[r][c] - o.cur[j].m[r][c]) / (r == 3 ? sc : 1.0f);
res = fmaxf(res, e);
}
}
s_obj_res_sum += res;
s_obj_cnt_static++;
if (res > s_obj_res_max) {
s_obj_res_max = res;
}
}
}
}
}
auto g = grpOk.find(o.group);
if (g == grpOk.end()) {
grpOk[o.group] = ok;
} else if (!ok) {
g->second = false;
}
}
for (ObjInst& o : s_obj) {
o.blend = grpOk[o.group] && !interp_obj_off();
}
for (auto& g : grpOk) {
if (g.second) {
s_obj_cnt_grp_blend++;
} else {
s_obj_cnt_grp_exact++;
}
}
memcpy(s_obj_Lprev, L, sizeof(L)); /* committed at frame end */
memcpy(s_obj_origin_prev, origin, sizeof(origin));
s_obj_serial_prev = serial;
}
/* End of a game frame: this frame's drawn instances become the previous. */
static void gfx_obj_frame_end(void) {
if (!s_obj_frame) {
s_obj_prev.clear();
s_obj_have_prev = false;
return;
}
s_obj_prev.clear();
for (ObjInst& o : s_obj) {
if (!o.hit) {
continue;
}
ObjPose& p = s_obj_prev[o.key];
p.m.swap(o.cur);
memcpy(p.pos, o.pos, sizeof(p.pos));
}
s_obj_have_prev = true;
s_obj_frame = false;
}
/* A MODELVIEW LOAD from `addr`: the instance + joint it belongs to, or null. */
static ObjInst* gfx_obj_find(uintptr_t addr, int* joint) {
if (s_obj.empty()) {
return nullptr;
}
size_t lo = 0, hi = s_obj.size();
while (lo < hi) {
const size_t mid = (lo + hi) / 2;
if (s_obj[mid].lo <= addr) {
lo = mid + 1;
} else {
hi = mid;
}
}
if (lo == 0) {
return nullptr;
}
ObjInst& o = s_obj[lo - 1];
if (addr >= o.hi || ((addr - o.lo) & 63u) != 0) {
return nullptr;
}
*joint = (int)((addr - o.lo) / 64u);
return &o;
}
/* The blended joint for this pass (built once per instance per pass alpha). */
static const float (*gfx_obj_blended(ObjInst& o, int joint))[4] {
const float t = s_interp_alpha;
if (o.alpha_out != t) {
o.out.resize(o.nmtx);
for (int k = 0; k < o.nmtx; k++) {
const float (*a)[4] = o.a[k].m;
const float (*b)[4] = o.cur[k].m;
float (*m)[4] = o.out[k].m;
for (int i = 0; i < 4; i++) {
float la = 0, lb = 0, lr = 0;
for (int j = 0; j < 4; j++) {
m[i][j] = a[i][j] + (b[i][j] - a[i][j]) * t;
if (j < 3) {
la += a[i][j] * a[i][j];
lb += b[i][j] * b[i][j];
lr += m[i][j] * m[i][j];
}
}
if (i < 3 && lr > 1e-12f) { /* keep the blended basis length (no cos/2 shrink) */
const float want = sqrtf(la) + (sqrtf(lb) - sqrtf(la)) * t;
const float k2 = want / sqrtf(lr);
for (int j = 0; j < 3; j++) {
m[i][j] *= k2;
}
}
}
}
o.alpha_out = t;
}
return o.out[joint].m;
}
static void gfx_sp_matrix(uint8_t parameters, const int32_t* addr) { static void gfx_sp_matrix(uint8_t parameters, const int32_t* addr) {
float matrix[4][4]; float matrix[4][4];
@@ -2605,6 +2899,28 @@ static void gfx_sp_matrix(uint8_t parameters, const int32_t* addr) {
gfx_interp_matrix(matrix, (parameters & G_MTX_PROJECTION) != 0, (parameters & G_MTX_LOAD) != 0); gfx_interp_matrix(matrix, (parameters & G_MTX_PROJECTION) != 0, (parameters & G_MTX_LOAD) != 0);
} }
/* D955: an object joint (see gfx_obj_frame_begin). */
if (s_obj_frame && !addr_bad && !(parameters & G_MTX_PROJECTION) && (parameters & G_MTX_LOAD)) {
int joint;
ObjInst* o = gfx_obj_find((uintptr_t)addr, &joint);
if (o != nullptr) {
o->hit = true;
/* Only under the props' corrected plain perspective: a destroyed
* object is drawn under the room projection with world-space
* matrices (propobj.c objRender), where the camera rebase would be
* wrong -- exact there. */
if (s_pcam_armed && s_pcam_applied) {
s_obj_cnt_loads++;
if (o->blend) {
memcpy(matrix, gfx_obj_blended(*o, joint), sizeof(matrix));
s_obj_cnt_loads_blend++;
}
}
} else if (s_pcam_armed) {
s_obj_cnt_unkeyed++;
}
}
if (parameters & G_MTX_PROJECTION) { if (parameters & G_MTX_PROJECTION) {
s_pcam_applied = false; s_pcam_applied = false;
if (pcam) { if (pcam) {
@@ -5258,6 +5574,70 @@ static void gfx_dp_set_other_mode(uint32_t h, uint32_t l) {
rdp.other_mode_l = l; rdp.other_mode_l = l;
} }
/* D955 (sky): the sky and sky water are pre-projected on the CPU for camera N
* (sky.c: clip-space (ndcX*w, ndcY*w, 0, w) through G_FLOATVTX_EXT), so on an
* in-between present they stayed at camera N while the world took the
* blended camera: the horizon shuddered against buildings while turning. Per
* blend pass the camera-space delta C = L_N^-1 * L_b = P * R^-1 * Rb * P^-1
* (R = the frame's room projection, Rb = its blend, paired and blended as the
* projection loads are; P = the plain perspective the sky was projected with)
* is applied to each sky vertex: rebuild its view point (x_c/sx, y_c/sy, -w),
* rotate by C's 3x3 (the sky is at infinity: no translation), re-project.
* S/T are plane coordinates and stay as they are. */
static int s_sky_state = 0; /* per pass: 0 unknown, 1 apply, -1 none */
static float s_sky_C[3][3], s_sky_sx = 1.0f, s_sky_sy = 1.0f;
static unsigned s_sky_cnt_pass = 0;
static void gfx_sky_prepare(void) {
s_sky_state = -1;
if (s_interp_mode != INTERP_BLEND || s_interp_alpha >= 1.0f || interp_propcam_off() || s_pcam_conv < 0) {
return;
}
int32_t pw[16], rw[16];
if (!interpPortalCamMatrices(pw, rw)) {
return;
}
float P[4][4], R[4][4], Pinv[4][4], Rinv[4][4];
gfx_decode_mtx_words(pw, P);
gfx_decode_mtx_words(rw, R);
auto it = s_interp_prev.find(gfx_interp_key(0, true));
if (it == s_interp_prev.end() || it->second.empty() || !gfx_mat4_inverse(P, Pinv) || !gfx_mat4_inverse(R, Rinv) ||
fabsf(P[0][0]) < 1e-6f || fabsf(P[1][1]) < 1e-6f) {
return;
}
const InterpMtx* best = nullptr;
float bestd = 0.0f;
for (const InterpMtx& c : it->second) {
const float d = gfx_interp_dist(c.m, R);
if (!best || d < bestd) {
best = &c;
bestd = d;
}
}
if (!best || !gfx_interp_plausible(best->m, R, true)) {
return;
}
float Rb[4][4], t1[4][4], t2[4][4], C[4][4];
const float t = s_interp_alpha;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
Rb[i][j] = best->m[i][j] + (R[i][j] - best->m[i][j]) * t;
}
}
gfx_matrix_mul(t1, P, Rinv);
gfx_matrix_mul(t2, t1, Rb);
gfx_matrix_mul(C, t2, Pinv);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
s_sky_C[i][j] = C[i][j];
}
}
s_sky_sx = P[0][0];
s_sky_sy = P[1][1];
s_sky_state = 1;
s_sky_cnt_pass++;
}
/* D245 (M-201): G_FLOATVTX_EXT loader -- gfx_sp_vertex for pre-transformed /* D245 (M-201): G_FLOATVTX_EXT loader -- gfx_sp_vertex for pre-transformed
* clip-space float vertices with float S/T (see port/include/floatvtx.h). * clip-space float vertices with float S/T (see port/include/floatvtx.h).
* Only the unlit, unfogged path the sky/water fans use is supported. */ * Only the unlit, unfogged path the sky/water fans use is supported. */
@@ -5268,8 +5648,23 @@ static void gfx_sp_vertex_float(size_t n_vertices, size_t dest_index, const Port
for (size_t i = 0; i < n_vertices; i++, dest_index++) { for (size_t i = 0; i < n_vertices; i++, dest_index++) {
const PortFloatVtx* v = &vertices[i]; const PortFloatVtx* v = &vertices[i];
struct LoadedVertex* d = &rsp.loaded_vertices[dest_index]; struct LoadedVertex* d = &rsp.loaded_vertices[dest_index];
float x = gfx_adjust_x_for_aspect_ratio(v->x, v->w); float vx = v->x, vy = v->y, vw = v->w;
float y = v->y, z = v->z, w = v->w; if (s_sky_state == 0) {
gfx_sky_prepare(); /* D955 (sky) */
}
if (s_sky_state > 0 && vw > 1e-6f) {
const float px = vx / s_sky_sx, py = vy / s_sky_sy, pz = -vw;
const float qx = px * s_sky_C[0][0] + py * s_sky_C[1][0] + pz * s_sky_C[2][0];
const float qy = px * s_sky_C[0][1] + py * s_sky_C[1][1] + pz * s_sky_C[2][1];
const float qz = px * s_sky_C[0][2] + py * s_sky_C[1][2] + pz * s_sky_C[2][2];
if (qz < -1e-6f) {
vx = qx * s_sky_sx;
vy = qy * s_sky_sy;
vw = -qz;
}
}
float x = gfx_adjust_x_for_aspect_ratio(vx, vw);
float y = vy, z = v->z, w = vw;
d->u = v->s * (float)rsp.texture_scaling_factor.s / 65536.0f; d->u = v->s * (float)rsp.texture_scaling_factor.s / 65536.0f;
d->v = v->t * (float)rsp.texture_scaling_factor.t / 65536.0f; d->v = v->t * (float)rsp.texture_scaling_factor.t / 65536.0f;
d->color.r = v->r; d->color.r = v->r;
@@ -6179,6 +6574,8 @@ extern "C" int gfx_interp_tick(Gfx* commands, const float* alphas, int n, int ba
s_interp_cnt_pre_soft++; s_interp_cnt_pre_soft++;
} }
s_interp_turn_max = 0.0f; s_interp_turn_max = 0.0f;
interpPortalPassBegin(commands); /* D955: select this frame's snapshot before the passes */
gfx_obj_frame_begin();
int passes = 0; int passes = 0;
bool record = true; /* D583: record this pass's raw matrices (the first drawn) */ bool record = true; /* D583: record this pass's raw matrices (the first drawn) */
bool exact_all = false; /* D583: this pass is the frame's exact image: store it in every slot */ bool exact_all = false; /* D583: this pass is the frame's exact image: store it in every slot */
@@ -6210,6 +6607,7 @@ extern "C" int gfx_interp_tick(Gfx* commands, const float* alphas, int n, int ba
s_interp_rooms_pass.clear(); s_interp_rooms_pass.clear();
s_pcam_armed = false; /* D954 (rsp was restored from the snapshot above) */ s_pcam_armed = false; /* D954 (rsp was restored from the snapshot above) */
s_pcam_applied = false; s_pcam_applied = false;
s_sky_state = 0; /* D955 (sky) */
gfx_frame_body(commands, passes == 0, &overlay, 0, &unused0, &unused1); gfx_frame_body(commands, passes == 0, &overlay, 0, &unused0, &unused1);
s_interp_body_runs++; s_interp_body_runs++;
record = false; record = false;
@@ -6420,6 +6818,17 @@ extern "C" int gfx_interp_tick(Gfx* commands, const float* alphas, int n, int ba
s_pcam_cnt_pass, s_pcam_cnt_load, s_pcam_cnt_pass ? s_pcam_deg_sum / (float)s_pcam_cnt_pass : 0.0f, s_pcam_cnt_pass, s_pcam_cnt_load, s_pcam_cnt_pass ? s_pcam_deg_sum / (float)s_pcam_cnt_pass : 0.0f,
s_pcam_deg_max, s_pcam_cnt_nosnap, s_pcam_cnt_gun); s_pcam_deg_max, s_pcam_cnt_nosnap, s_pcam_cnt_gun);
s_pcam_cnt_pass = s_pcam_cnt_load = s_pcam_cnt_nosnap = s_pcam_cnt_gun = 0; s_pcam_cnt_pass = s_pcam_cnt_load = s_pcam_cnt_nosnap = s_pcam_cnt_gun = 0;
sysLogPrintf(LOG_NOTE, "D955 obj%s: %u frames (%u no prev, %u room-origin change), %u instances, groups %u blended / %u exact, joint loads %u blended of %u keyed, %u unkeyed in window, %u gate rejects, max step %.1f, static residual mean %.2g max %.2g (%u)",
interp_obj_off() ? " (OFF)" : "", s_obj_cnt_frames, s_obj_cnt_noprev, s_obj_cnt_origin, s_obj_cnt_inst,
s_obj_cnt_grp_blend, s_obj_cnt_grp_exact, s_obj_cnt_loads_blend, s_obj_cnt_loads, s_obj_cnt_unkeyed,
s_obj_cnt_gate, s_obj_step_max, s_obj_cnt_static ? s_obj_res_sum / (float)s_obj_cnt_static : 0.0f,
s_obj_res_max, s_obj_cnt_static);
s_obj_cnt_frames = s_obj_cnt_noprev = s_obj_cnt_origin = s_obj_cnt_inst = s_obj_cnt_grp_blend = 0;
s_obj_cnt_grp_exact = s_obj_cnt_loads = s_obj_cnt_loads_blend = s_obj_cnt_unkeyed = s_obj_cnt_gate = 0;
s_obj_cnt_static = 0;
sysLogPrintf(LOG_NOTE, "D955 sky: %u passes re-projected", s_sky_cnt_pass);
s_sky_cnt_pass = 0;
s_obj_res_sum = s_obj_res_max = s_obj_step_max = 0.0f;
s_pcam_deg_sum = s_pcam_deg_max = 0.0f; s_pcam_deg_sum = s_pcam_deg_max = 0.0f;
s_interp_cnt_ip_exact = s_interp_cnt_ip_soft = s_interp_cnt_late_exact = s_interp_cnt_late_soft = 0; s_interp_cnt_ip_exact = s_interp_cnt_ip_soft = s_interp_cnt_late_exact = s_interp_cnt_late_soft = 0;
if (s_interp_hid_entering > 0) { if (s_interp_hid_entering > 0) {
@@ -6432,6 +6841,8 @@ extern "C" int gfx_interp_tick(Gfx* commands, const float* alphas, int n, int ba
s_interp_record = false; s_interp_record = false;
s_pcam_armed = false; /* D954 */ s_pcam_armed = false; /* D954 */
s_pcam_applied = false; s_pcam_applied = false;
s_sky_state = -1; /* D955 (sky): outside interpolation, never */
gfx_obj_frame_end(); /* D955 */
s_interp_prev.swap(s_interp_cur); s_interp_prev.swap(s_interp_cur);
s_interp_have_prev = true; s_interp_have_prev = true;
if (s_interp_rooms_prev_valid && s_interp_rooms_pass != s_interp_rooms_prev) { if (s_interp_rooms_prev_valid && s_interp_rooms_pass != s_interp_rooms_prev) {
+19
View File
@@ -60,6 +60,25 @@ unsigned interpPortalExtraCount(void);
* (rooms) as fixed-point Mtx words; 0 = unavailable (split-screen, menus). */ * (rooms) as fixed-point Mtx words; 0 = unavailable (split-screen, menus). */
int interpPortalCamMatrices(int32_t proj[16], int32_t room[16]); int interpPortalCamMatrices(int32_t proj[16], int32_t room[16]);
/* D955: one on-screen model instance of the drawn frame (game-thread snapshot).
* render_pos = the tick's joint matrix block (nmtx fixed-point Mtx, 64 bytes
* each; the DL loads them through segment 3). group = instances that must
* blend or fall back together (a chr, its held weapons and hat). */
typedef struct IpObjInst {
const void *render_pos;
const void *model;
const void *modelobj;
float pos[3];
int nmtx;
int group;
int type; /* PROP_TYPE_* */
int rigid; /* OBJ / DOOR / WEAPON prop (not a chr or its attachment) */
} IpObjInst;
/* D955: the drawn frame's instances; *origin = current_model_pos (the room
* origin every view matrix is relative to), *serial = snapshot serial. 0 =
* none (split-screen, menus, no snapshot). */
int interpPortalObjInstances(const IpObjInst **out, float origin[3], unsigned *serial);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+109
View File
@@ -90,6 +90,8 @@ static int rp_env(const char *name, int *cache)
/* Frame snapshot (game thread) */ /* Frame snapshot (game thread) */
/* ------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------- */
#define IP_MAXOBJ 384
typedef struct { typedef struct {
const void *dl; /* key: the frame's display list */ const void *dl; /* key: the frame's display list */
int valid; int valid;
@@ -136,6 +138,12 @@ typedef struct {
int cam_ok; int cam_ok;
s32 cam_proj[16]; s32 cam_proj[16];
s32 cam_room[16]; s32 cam_room[16];
/* D955: on-screen model instances (only when cam_ok). */
int obj_n;
IpObjInst obj[IP_MAXOBJ];
float origin[3];
unsigned obj_serial;
} IpSnap; } IpSnap;
static IpSnap s_ring[RP_SLOTS]; static IpSnap s_ring[RP_SLOTS];
@@ -143,6 +151,7 @@ static int s_ringNext;
static const IpSnap *S; /* snapshot of the frame being drawn (render thread) */ static const IpSnap *S; /* snapshot of the frame being drawn (render thread) */
static const IpSnap *SC; /* D954: same frame, camera matrices (valid or not for the replay) */ static const IpSnap *SC; /* D954: same frame, camera matrices (valid or not for the replay) */
static unsigned s_snapSerial; static unsigned s_snapSerial;
static unsigned s_objSerial; /* D955: one per sent frame (cam snapshot) */
typedef struct IpSchedTask { /* mirror of OSScTask (src/sched.h) up to `list` */ typedef struct IpSchedTask { /* mirror of OSScTask (src/sched.h) up to `list` */
void *next; void *next;
@@ -153,6 +162,89 @@ typedef struct IpSchedTask { /* mirror of OSScTask (src/sched.h) up to `l
} IpSchedTask; } IpSchedTask;
extern s32 getPlayerCount(void); extern s32 getPlayerCount(void);
extern PropRecord *g_OnScreenPropList[];
/* D955: add one model instance to the snapshot. */
static void ip_obj_add(IpSnap *s, Model *m, PropRecord *prop, int group, int group_is_rigid)
{
IpObjInst *o;
int i;
if (m == NULL || m->render_pos == NULL || m->obj == NULL || m->obj->numMatrices <= 0) {
return;
}
for (i = 0; i < s->obj_n; i++) {
if (s->obj[i].model == m) { return; } /* already listed (a child drawn on its own too) */
}
if (s->obj_n >= IP_MAXOBJ) { return; }
o = &s->obj[s->obj_n++];
o->render_pos = m->render_pos;
o->model = m;
o->modelobj = m->obj;
o->nmtx = m->obj->numMatrices;
o->group = group;
o->type = prop->type;
o->rigid = (prop->type == PROP_TYPE_OBJ || prop->type == PROP_TYPE_DOOR || prop->type == PROP_TYPE_WEAPON) && group_is_rigid;
o->pos[0] = prop->pos.f[0];
o->pos[1] = prop->pos.f[1];
o->pos[2] = prop->pos.f[2];
}
/* D955: props attached to `prop` (prop->child chain, e.g. objects carried
* by an object or chr), into the same group; two levels deep. */
static void ip_obj_children(IpSnap *s, PropRecord *prop, int group, int depth)
{
PropRecord *c;
if (depth > 2) { return; }
for (c = prop->child; c != NULL; c = c->next) {
if ((c->type == PROP_TYPE_OBJ || c->type == PROP_TYPE_WEAPON || c->type == PROP_TYPE_DOOR) && c->obj != NULL) {
ip_obj_add(s, c->obj->model, c, group, 0);
}
ip_obj_children(s, c, group, depth + 1);
}
}
/* D955: the frame's on-screen props, as the render step left them. CHRs bring
* their held weapons and hat (one group); OBJ/WEAPON/DOOR are their own group.
* Explosions, smoke and the viewer are left out (geometry rebuilt every tick),
* and so are doors whose vertices the CPU resizes for the tick (the same test
* as propobj.c's CLIP_TO_BBOX / eye / iris door branch): drawn exact. */
static void ip_snapshot_objs(IpSnap *s)
{
int i, k, group = 0;
struct player *p = g_CurrentPlayer;
s->obj_n = 0;
s->origin[0] = p->current_model_pos.f[0];
s->origin[1] = p->current_model_pos.f[1];
s->origin[2] = p->current_model_pos.f[2];
for (i = 0; i < 800 && g_OnScreenPropList[i] != NULL; i++) {
PropRecord *prop = g_OnScreenPropList[i];
group++;
if (prop->type == PROP_TYPE_CHR && prop->chr != NULL) {
ChrRecord *chr = prop->chr;
ip_obj_add(s, chr->model, prop, group, 0);
for (k = 0; k < 2; k++) {
PropRecord *w = chr->weapons_held[k];
if (w != NULL && w->obj != NULL) { ip_obj_add(s, w->obj->model, w, group, 0); }
}
if (chr->handle_positiondata_hat != NULL && chr->handle_positiondata_hat->obj != NULL) {
ip_obj_add(s, chr->handle_positiondata_hat->obj->model, chr->handle_positiondata_hat, group, 0);
}
ip_obj_children(s, prop, group, 0);
} else if ((prop->type == PROP_TYPE_OBJ || prop->type == PROP_TYPE_WEAPON || prop->type == PROP_TYPE_DOOR) &&
prop->obj != NULL) {
ObjectRecord *obj = prop->obj;
if (prop->type == PROP_TYPE_DOOR) {
DoorRecord *door = (DoorRecord *) obj;
if ((door->doorFlags & DOORFLAG_CLIP_TO_BBOX) || door->doorType == DOORTYPE_EYE ||
door->doorType == DOORTYPE_IRIS) {
continue;
}
}
ip_obj_add(s, obj->model, prop, group, 1);
ip_obj_children(s, prop, group, 0);
}
}
}
/* D954: game thread, with the frame's graphics task. */ /* D954: game thread, with the frame's graphics task. */
static void ip_snapshot_cam(IpSnap *s, const void *dl) static void ip_snapshot_cam(IpSnap *s, const void *dl)
@@ -161,12 +253,15 @@ static void ip_snapshot_cam(IpSnap *s, const void *dl)
_Static_assert(sizeof(Mtx) == sizeof(s->cam_proj), "D954: Mtx must be 16 fixed-point words"); _Static_assert(sizeof(Mtx) == sizeof(s->cam_proj), "D954: Mtx must be 16 fixed-point words");
s->dl = dl; s->dl = dl;
s->cam_ok = 0; s->cam_ok = 0;
s->obj_n = 0;
s->obj_serial = ++s_objSerial;
if (p == NULL || p->projmatrix == NULL || p->field_10E0 == NULL || getPlayerCount() != 1) { if (p == NULL || p->projmatrix == NULL || p->field_10E0 == NULL || getPlayerCount() != 1) {
return; return;
} }
memcpy(s->cam_proj, p->projmatrix, sizeof(s->cam_proj)); memcpy(s->cam_proj, p->projmatrix, sizeof(s->cam_proj));
memcpy(s->cam_room, p->field_10E0, sizeof(s->cam_room)); memcpy(s->cam_room, p->field_10E0, sizeof(s->cam_room));
s->cam_ok = 1; s->cam_ok = 1;
ip_snapshot_objs(s);
} }
static void ip_snapshot(IpSnap *s, const void *dl) static void ip_snapshot(IpSnap *s, const void *dl)
@@ -257,6 +352,20 @@ void interpPortalOnSend(void *mq, void *msg)
ip_snapshot(&s_ring[i], (const void *) t->list.t.data_ptr); ip_snapshot(&s_ring[i], (const void *) t->list.t.data_ptr);
} }
/* D955: the frame's on-screen model instances (see IpSnap.obj). */
int interpPortalObjInstances(const IpObjInst **out, float origin[3], unsigned *serial)
{
if (SC == NULL || !SC->cam_ok) {
return 0;
}
*out = SC->obj;
origin[0] = SC->origin[0];
origin[1] = SC->origin[1];
origin[2] = SC->origin[2];
*serial = SC->obj_serial;
return SC->obj_n;
}
/* D954: the frame's two projections (see IpSnap.cam_*); 0 when unavailable. */ /* D954: the frame's two projections (see IpSnap.cam_*); 0 when unavailable. */
int interpPortalCamMatrices(int32_t proj[16], int32_t room[16]) int interpPortalCamMatrices(int32_t proj[16], int32_t room[16])
{ {