D176(a): emit screen-space sky geometry on PC (Path B)

skyRenderTri/skyRenderFull emit the sky as a verbatim N64 RDP triangle
command chopped into G_RDPHALF_* immediates, which fast3d deliberately
no-ops -> Surface/Statue/Streets skies render solid black.

Behind #ifdef PORT, substitute a screen-space gSPVertex + gSP1Triangle/
gSP2Triangles emit built from the already-projected SkyRelated38 verts
(new helper skyPortRenderPoly, ortho matrices via dynAllocate*). N64 path
unchanged under #else. Pure RSP-idiom substitution, no behavioural change.

Links 243/243. Render check owed on a display: -level_22 frame ~360.

findings.md D176(a) M-46; porting-notes.mdD.
This commit is contained in:
James Dansereau committed 2026-09-08 22:29:28 -04:00
1 parent b7bb08db65
commit 9a6017a784
3 files changed
+168

No files matched your search

+59
View File
@@ -5090,6 +5090,65 @@ M-42 clean headless black-sky repro.
`-level_29` — confirm also black, proving it is the shared emit path and not a
Surface-only asset.
### D176(a) — M-46: Path B implemented (`src/game/sky.c`, `#ifdef PORT`)
Path B is in. `src/game/sky.c` only; N64 path kept byte-for-byte under `#else`.
**What changed.** A new file-scope `#ifdef PORT` helper `skyPortRenderPoly(gdl,
SkyRelated38 **v, nverts)` and an early `#ifdef PORT` return in both
`skyRenderTri` (3 verts, `arg1..arg3`) and `skyRenderFull` (4 verts,
`arg1..arg4`), placed right after the `sp444/sp488 == 0` degenerate-area guard
and `1.0f / area` so all shared early-outs still run. The helper:
- `dynAllocateVertices(n)` + two `dynAllocateMatrix()` from the vtx pool
(same lifetime idiom as the bondview2 watch-gauge ortho overlay and
explosion.c billboards).
- `guOrtho(proj, l, l+w, t+h, t, -32768, 32768, 1)` where `l/t/w/h =
getPlayer_c_screen{left,top,width,height}()` — maps the already-projected
pixel coords 1:1 into clip space, Y flipped (screen-down → NDC-up); identity
modelview. `gSPMatrix` LOAD PROJECTION then MODELVIEW. `bg.c` reloads the
perspective projection at the top of `bgLevelRender` (lines 638/689/…), so
leaving ortho loaded after `skyRender` is safe.
- Per vertex: `ob = {unk28, unk2c, unk30} * 0.25` (the `*4` subpixel scale the
RDP path used); `tc = {unk20, unk24} * 32` (standard N64 S10.5); `cn = rgba`
from the struct.
- `gSPClearGeometryMode(G_LIGHTING|G_CULL_BOTH|G_FOG)` +
`gSPSetGeometryMode(G_SHADE|G_SHADING_SMOOTH)`, then `gSPVertex` +
`gSP1Triangle(0,1,2)` (tri) / `gSP2Triangles(0,1,3, 0,3,2)` (quad, winding
irrelevant with culling off). Combiner (`SHADE,ENV,TEXEL0,ENV`), env colour
and `texSelect(&skywaterimages[SkyImageId/WaterImageId], 1,0,2)` are already
emitted by the caller immediately before — the helper does not touch them.
**Verified.** `src/game/sky.c` compiles clean under the real PC build flags
(`gcc.exe … -DPORT=1 -std=gnu11 -Wall`, exit 0, no new warnings) with the
worktree's `build-pc/` CMake configuration. Full link + run NOT done: the
worktree has no `data/` ROM so assets can't be extracted here. Every symbol the
helper uses (`dynAllocate*`, `guOrtho`, `guMtxIdent`, `gSPVertex`,
`gSP{1,2}Triangle{,s}`, `osVirtualToPhysical`) is already linked by the compiled
set, so link is expected clean.
**Owed to a human (cannot headless-verify):**
1. Eyeball `-level_22` (Statue, night) frame ~360, bare boot no input — the
M-42 black-sky repro. Expect a rendered moonlit sky in the upper half.
Also spot-check `-level_36` (Surface), `-level_29` (Streets).
2. **Untested risk — texture scale.** `tc = unk20/unk24 * 32` is the
conventional guess; `unk20/unk24` come out of `sub_GAME_7F097388` as
`unk0c * (65535/65536)` with `unk0c ≈ worldX*0.1` for clouds. If the sky
texture is tiled wrong / smeared, the fix is localised to the two `tc[]`
lines in `skyPortRenderPoly` (multiplier, or an explicit `gSPTexture`
scaling override) — no other code moves.
3. **Untested risk — ortho Z / depth.** `ob[2] = unk30*0.25` in `[0,8192]`
under an `[-32768,32768]` ortho; sky renders first into a cleared buffer so
depth ordering should not matter, but if the sky Z-fights or is culled,
drop `ob[2]` to 0.
4. Seam/precision check vs the N64 RDP subpixel edge-walk (the Path A
motivation) — only relevant once it renders at all.
**Confidence:** MODERATE. The emit-path substitution is sound and matches the
established screen-space-overlay idiom; compile is clean. The two numeric
unknowns (texture scale, and whether fast3d's viewport transform composes with
this ortho exactly as the projection math intends) need one visual check.
---
## D177 — Ladders non-functional: `count`/`rooms` land in the high half of a widened pointer (M-36)
+8
View File
@@ -451,6 +451,14 @@ through a converter or a runtime bswap fixup reads scrambled.
logic). 2D pixel space is `viGetX()` x `viGetY()`. Return NULL when the
overlay is closed so the frame is byte-for-byte unchanged.
- **Verbatim RDP triangle commands chopped into `gImmp1(G_RDPHALF_1/_CONT/_2)`
pairs** (GE's `skyRenderTri`/`skyRenderFull` hand-build an edge-walked
`G_TRI_FILL`/`G_TRI_SHADE_TXTR` for the modified RSP ucode to reassemble) are
silently dropped by fast3d — it has no RDP triangle rasteriser. Substitute a
normal `gSPVertex` + `gSP*Triangle` batch behind `#ifdef PORT`: if the game
code already screen-space-projected the verts, re-emit them under a
`guOrtho(l,r,b,t,…)` + identity modelview matching the projection's pixel
extents, `#else` keeps the stream verbatim (D176(a), M-46).
- Z buffer cleared by pointing the color image at it + fill-rect → does
nothing in fast3d; must emit `G_CLEAR_DEPTH_EXT` (D105).
- LOD / detail mip tiles: fast3d fabricates a crop when detail textures
+101
View File
@@ -14,6 +14,9 @@
#include "othermodemicrocode.h"
#include "fr.h"
#include "image_bank.h"
#ifdef PORT
#include "dyn.h"
#endif
#define SKYABS(val) (val >= 0.0f ? (val) : -(val))
@@ -1474,6 +1477,78 @@ bool skyVerticesAreTheSame(SkyRelated38 *arg0, SkyRelated38 *arg1)
return sqrtf((f0 * f0) + (f1 * f1)) < 1.0f ? TRUE : FALSE;
}
#ifdef PORT
/*
* D176(a) Path B (M-46) -- black-sky fix. Full rationale in
* docs/dev/findings.md section F, "D176(a)".
*
* skyRenderTri / skyRenderFull normally build their geometry as a verbatim
* N64 RDP triangle command, chopped into 32-bit halves and carried as
* gImmp1(G_RDPHALF_1 / _CONT / _2, word) pairs for GE's modified RSP ucode
* (rsp/graphics/gmain.s) to reassemble and DMA to the RDP as an edge-walked
* G_TRI_FILL / G_TRI_SHADE_TXTR. The PC software RSP (port/fast3d) has no RDP
* triangle rasteriser and deliberately drops G_RDPHALF_* (gfx_pc.cpp), so
* every cloud-sky level rendered a solid black upper half.
*
* The SkyRelated38 verts handed to these functions are already fully
* screen-space projected by sub_GAME_7F097388:
* unk28 = screenX * 4, unk2c = screenY * 4, unk30 = screenZ (0..0x7fff),
* unk34 = 1/w, unk20 / unk24 = S / T, r/g/b/a = per-vertex colour.
* Under PORT we skip the RDPHALF encode entirely and re-emit the same
* triangle(s) as an ordinary gSPVertex + gSP*Triangle batch under a
* pixel-space ortho projection the software RSP can consume. The combiner
* (SHADE,ENV,TEXEL0,ENV), env colour and texSelect(&skywaterimages[...])
* are all already emitted by the caller. This is a pure N64-RSP-idiom
* substitution (same class as the G_TRI4 / dynamic-lighting PORT branches
* elsewhere in src/): the N64 (#else) path below is byte-for-byte unchanged.
*/
static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts)
{
Vtx *vtx = dynAllocateVertices(nverts < 3 ? 3 : nverts);
Mtx *proj = dynAllocateMatrix();
Mtx *mv = dynAllocateMatrix();
f32 l = getPlayer_c_screenleft();
f32 t = getPlayer_c_screentop();
f32 r = l + getPlayer_c_screenwidth();
f32 b = t + getPlayer_c_screenheight();
s32 i;
guOrtho(proj, l, r, b, t, -32768.0f, 32768.0f, 1.0f);
guMtxIdent(mv);
for (i = 0; i < nverts; i++)
{
vtx[i].v.ob[0] = (s16) (v[i]->unk28 * 0.25f);
vtx[i].v.ob[1] = (s16) (v[i]->unk2c * 0.25f);
vtx[i].v.ob[2] = (s16) (v[i]->unk30 * 0.25f);
vtx[i].v.flag = 0;
vtx[i].v.tc[0] = (s16) (v[i]->unk20 * 32.0f);
vtx[i].v.tc[1] = (s16) (v[i]->unk24 * 32.0f);
vtx[i].v.cn[0] = (u8) v[i]->r;
vtx[i].v.cn[1] = (u8) v[i]->g;
vtx[i].v.cn[2] = (u8) v[i]->b;
vtx[i].v.cn[3] = (u8) v[i]->a;
}
gSPMatrix(gdl++, osVirtualToPhysical(proj), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_PROJECTION);
gSPMatrix(gdl++, osVirtualToPhysical(mv), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW);
gSPClearGeometryMode(gdl++, G_LIGHTING | G_CULL_BOTH | G_FOG);
gSPSetGeometryMode(gdl++, G_SHADE | G_SHADING_SMOOTH);
gSPVertex(gdl++, osVirtualToPhysical(vtx), nverts, 0);
if (nverts >= 4)
{
gSP2Triangles(gdl++, 0, 1, 3, 0, 3, 2, 0, 0);
}
else
{
gSP1Triangle(gdl++, 0, 1, 2, 0);
}
return gdl;
}
#endif
/*
* Address: 0x7F097818
*/
@@ -1619,6 +1694,18 @@ Gfx *skyRenderTri(Gfx *gdl, SkyRelated38 *arg1, SkyRelated38 *arg2, SkyRelated38
sp440 = 1.0f / sp444;
#ifdef PORT
{
/* D176(a) Path B: re-emit as screen-space geometry (see skyPortRenderPoly).
* skyRenderTri is only ever called textured in this file; `textured` is
* honoured only in that the caller has set the textured combiner. */
SkyRelated38 *portv[3];
portv[0] = arg1;
portv[1] = arg2;
portv[2] = arg3;
return skyPortRenderPoly(gdl, portv, 3);
}
#else
sp484 = arg1;
sp480 = arg2;
sp47c = arg3;
@@ -1966,6 +2053,7 @@ Gfx *skyRenderTri(Gfx *gdl, SkyRelated38 *arg1, SkyRelated38 *arg2, SkyRelated38
gImmp1(gdl++, G_RDPHALF_1, (spb8 & 0x0000ffff) << 16 | (spb4 & 0x0000ffff));
gImmp1(gdl++, G_RDPHALF_2, (spb0 & 0x0000ffff) << 16 | (spac & 0x0000ffff));
}
#endif
return gdl;
}
@@ -2120,6 +2208,18 @@ Gfx *skyRenderFull(Gfx *gdl, SkyRelated38 *arg1, SkyRelated38 *arg2, SkyRelated3
sp484 = 1.0f / sp488;
#ifdef PORT
{
/* D176(a) Path B: re-emit the sky quad as screen-space geometry
* (see skyPortRenderPoly). arg1..arg4 are TL,TR,BL,BR corners. */
SkyRelated38 *portv[4];
portv[0] = arg1;
portv[1] = arg2;
portv[2] = arg3;
portv[3] = arg4;
return skyPortRenderPoly(gdl, portv, 4);
}
#else
sp4cc = arg1;
sp4c8 = arg2;
sp4c4 = arg3;
@@ -2484,6 +2584,7 @@ Gfx *skyRenderFull(Gfx *gdl, SkyRelated38 *arg1, SkyRelated38 *arg2, SkyRelated3
gImmp1(gdl++, G_RDPHALF_1, (spb0 & 0x0000ffff) << 16 | (spac & 0x0000ffff));
gImmp1(gdl++, G_RDPHALF_2, (spa8 & 0x0000ffff) << 16 | (spa4 & 0x0000ffff));
}
#endif
return gdl;
}