port: D397 — fix file-select/gun-barrel comb via RDP scanline-range snap in gfx_draw_rectangle

Root cause H2 confirmed: the gun-barrel / file-select vertical gradient is
built from 0.75-native-pixel-tall gSPTextureRectangle row strips (title2.c
(<<2)-1 bottom idiom). On the N64 RDP the scan converter floors the U10.2
edges to whole scanlines, so each strip rasterizes to exactly one scanline;
gfx_draw_rectangle passed the fractional edges straight to the GPU's
continuous rasterizer, aliasing the 0.75px pitch into a periodic 'comb'
(fixed ~3-native-row period; window period = 3*scale — confirmed by a
3-scale 640/1024/1280 GE_PCDUMP test).

Fix (port-layer, surgical): after the sub-pixel offset, floor the top edge to
its scanline and the bottom edge to the last covered scanline (the RDP
scanline range [top_scan, bot_scan+1]) before NDC conversion. No-op for
whole-pixel rects (edges already 4k/4k-1), so only sub-pixel strips change
and normal fills are untouched.

Also: remove the temporary GE_D397SH / D397SHM env-gated shader probes
(gfx_opengl.cpp, gfx_pc.cpp) now that the root cause is settled.

Docs: findings.md D397 -> RESOLVED (+3-scale confirmation + fix record),
findings-index.csv regenerated, porting-notes.md section D15 marked resolved
with the landed fix.

Verified by playtest: gun-barrel + file-select gradient render clean, comb
gone.
This commit is contained in:
James Dansereau committed 2026-09-27 22:42:43 -04:00
1 parent 2a60b8a912
commit a548fc4de6
5 files changed
+86 -51

No files matched your search

+12 -2
View File
@@ -681,7 +681,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D394 | **Xbox 1.1 Jinx controller parity** — user supplied the Xbox 1.1–1.4 control table; B is gadgets, stick clicks crouch, LB aim, RB HD toggle (not weapon cycling). | PARTIAL (port 1.1 gameplay button roles user-tested; exact gadget-category parity and other selectable presets outstanding; RB HD toggle unavailable). |
| D395 | **Controller trapped in keyboard/mouse-only binding capture** — pad could enter the binding modal but could not finish or cancel without keyboard. | PARTIAL (both options UIs block pad-initiated capture; pad B can cancel an active modal; NTSC build linked, controller-only retest owed). |
| D396 | **F10 pad B/A used as slider decrement/increment, not back/select** — B/Y fed Left and A/X fed Right on every row. | PARTIAL (B backs one F10 page, closes at root; A/X accept/toggle on edge but never shift a slider; D-pad/stick left/right still adjust; NTSC linked, pad retest owed). |
| D397 | **SELECT FILE background + intro gun-barrel show an interlaced/comb look** — D182(2) family, but present on FIRST display, not only after re-entry (2026-09-27). Full `## D397` entry at file tail. | OPEN, re-scoped + root-causing (no fix yet): data path proven clean (299 I8 row strips upload byte-exact vs the ground-truth PGM); the stale-RENDERTILE import hypothesis is REFUTED (D161 already routes CI+G_TT_NONE to the I8 import); MSAA, mips, noise dither, 5-bit quant, cache aliasing all ruled out. Leading cause H1: the file-select custom-CC LERP uses ENV_ALPHA 0x14 as the mix factor and the port feeds it as 20/255≈0.078 — a black texel then renders ≈0.92×prim, matching the measured 27/28/17 black-region census; the N64 RDP's 5-bit coefficient scaling is the open question (check `rsp/graphics/gmain.s` + the PD port's CC engine). Possible second defect H2: the 6-of-9-row comb in row-strip geometry. `GE_D397SH` shader probe in `gfx_opengl.cpp` (kept during investigation, remove before merge). An ineffective CI→I import-routing block was tried and removed (D161 covers it). |
| D397 | **SELECT FILE background + intro gun-barrel show an interlaced/comb look** — D182(2) family, but present on FIRST display, not only after re-entry (2026-09-27). Full `## D397` entry at file tail. | **RESOLVED (2026-09-28):** root cause = **H2** (confirmed by a 3-scale PCDUMP test — the comb period is fixed at ≈3 *native* rows across 640/1024/1280, i.e. window-period = 3×scale: the `(<<2)-1` 0.75-native-pixel row-strips rely on RDP floor-based scanline stepping that `gfx_draw_rectangle`'s continuous-float rasterization did not replicate). **Fix (port-layer, surgical):** `gfx_draw_rectangle` now floors the top edge to its scanline and the bottom edge to the last covered scanline (the RDP scanline range `[top,bot+1]`) before rasterizing — a no-op for whole-pixel rects, so only sub-pixel strips change; §D15 in `docs/porting-notes.md` documents the mechanism. H1 (ENV_ALPHA 5-bit CC scaling) REFUTED by a byte-identical PD-port cross-check (the darkening it predicted is faithful, not a bug). `GE_D397SH`/`D397SHM` probes removed. Verified by playtest: gun-barrel + file-select gradient render clean, comb gone. |
| D398 | **PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).** Full `## D398` entry at file tail. | SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 `textMeasure` arg-swap bug was found and fixed there). |
| D399 | **File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).** Full `## D399` entry at file tail. | LANDED (port-only, D343 class): `selectpos.f[0]` 110→95 (new `#ifdef PORT` in front.c), Copy 200/222→185/207, Erase 282/304→267/289 (D343 PORT block), `LABEL_X` 358→343 (`frontoptions.c`); hit bounds recompute from the shifted positions. Clean build, PCDUMP frame 195 no overlaps; by-eye check owed. |
| D400 | **File-select "PC Options" label: icon removed, text-only; fixed a swapped textMeasure() arg bug and a 7px vertical misalignment vs Copy/Erase (user sign-off 2026-09-27).** Full `## D400` entry at file tail. | LANDED (port-only, `frontoptions.c`): D398's dot icon dropped on user instruction ("most straightforward design"); label now a plain `textRender` word, GAP 28 after Erase. Two measurement bugs fixed: (1) `textMeasure(textheight, textwidth)` args passed swapped -> label 35px above the bar, hot band transposed; (2) bare-label height measures 0 (vs 14 for newline-terminated Copy/Erase) -> 7px lower tops; fixed by restoring the `kLabelNL` height measure. PCDUMP frame 195: tops aligned 279.8 vs 279.3, 31px clearance from Erase; by-eye check owed. |
@@ -15177,13 +15177,23 @@ I logged all of this as **D397** (`docs/dev/findings.md`, index CSV, and the D18
**Status (2026-09-28, per `scratch/d397_fs/HANDOFF-D397.md`) — OPEN, re-scoped; no fix landed.** An in-tree CI→I import-routing block in `gfx_pc.cpp` `import_texture` was tried and is **ineffective** (the D116 stale-tile-metadata hypothesis is REFUTED: D161 already routes `fmt==CI && palette==G_TT_NONE` to `import_texture_i8`; the GE_TEXI logs show `palfmt=0` on all 299 rows) — the block was removed 2026-09-28. Established: the data path is 100% clean (299 row strips upload byte-exact vs `scratch/d397_fs/gunbarrel.pgm`); the artifact is static; MSAA, mips, RAND_NOISE, 5-bit quant and texture-cache aliasing are ruled out. Leading hypotheses: **H1** — the file-select custom-CC LERP (`gDPSetCombineLERP(TEXEL0, PRIMITIVE, ENV_ALPHA, ...)`) uses ENV_ALPHA 0x14 as the mix factor; the port feeds it as `20/255 ≈ 0.078` (`G_CCMUX_ENV_ALPHA` input, `gfx_pc.cpp`), so a black texel renders ≈0.92×prim ≈ the measured 27/28/17 black-region census — the N64 RDP 5-bit coefficient scaling (check `rsp/graphics/gmain.s` and the PD port's CC engine for the same combine id) is the root-cause question; **H2** — the 6-of-9-row comb is a second, separate row-strip-geometry defect (non-integer quad y + NEAREST, or the 256-tri vertex-ring wrap between the two tris of a row); **H3** — part of the darkened look is authentic N64 (G_TC_FILT 4-bit intensity dither, unimplemented here). A `GE_D397SH` env-gated shader-formula probe (in `gfx_opengl.cpp`, ~30 lines) stays in-tree until the root cause lands, then is removed. Next steps per the handoff: Q1 correlate which of the 20 compiled shaders is the gun-barrel row-strip one, Q4 settle the LERP-factor scaling against gmain.s/PD, Q5 native 640×480 FBO capture to re-measure the comb period.
**Status (2026-09-27b, static read against the local `pd_port` checkout) — H1 REFUTED, H2 promoted to leading cause.** Cross-checked `G_CCMUX_ENV_ALPHA` handling against `C:\Users\james\Source\Repos\pd_port\port\fast3d\gfx_pc.cpp`: byte-identical formula (`tmp.r=tmp.g=tmp.b=rdp.env_color.a`, then `/255.0f` at the vertex-color pack site) — no special-cased coarser scaling in either port. There is no "5-bit RDP coefficient" quirk in the real color combiner; PRIM/ENV alpha are genuine 8-bit `gDPSetEnvColor` fields and the combiner's A/B/C/D math is full 8-bit on real hardware (the 5-bit-precision idea conflated the CC with an unrelated hardware unit). **H1 is very likely not the (or not the only) cause; the uniform darkening it predicts may simply be faithful/authored** — do not correct it without an N64/1964-GEPD reference frame.
Found a concrete geometric mechanism for **H2**, promoted to leading cause: `titleRenderFolderMenuBackgroundLines` (`src/game/title2.c:24`) emits `lry - uly == 3` (U10.2 units) per row — a **0.75-native-pixel-tall** rect, not 1.0 — via the universal N64 `((y+1)<<2)-1` idiom. That idiom is only exact on real hardware because the RDP's scan converter *floors* the fixed-point Y to an integer scanline before testing coverage. `gfx_draw_rectangle` (`gfx_pc.cpp:3443`) does not replicate that: it maps the U10.2 corners straight to continuous NDC floats and rasterizes with the GPU's ordinary continuous coverage rules. A sub-1-native-pixel-tall rect stacked 299 times (each offset exactly 1.0 native px from the last) does not reliably rasterize as "the one scanline the idiom intended" under continuous rasterization — which of the two neighbouring device rows a given source row's fractional edges land in drifts as the stack accumulates, worsened by any non-integer native→window scale (`gfx_adjust_viewport_or_scissor`'s `ratioY`). This is a rasterization property of the geometry, not stale state, so it naturally explains the first-render (non-re-entry) trigger that refuted D182's hypothesis for this instance. Full mechanism + tell for future occurrences: `docs/porting-notes.md` §D15. **Not yet done: a live capture to confirm the comb period tracks `ratioY`** (predicted: comb period should change if the window/render resolution changes, at fixed native resolution) — this is the concrete falsifiable test before attempting a fix. No code changed this pass (diagnosis only).
**2026-09-28 progress (this session — D397SH/D397SHM probes, env-gated in `gfx_opengl.cpp`/`gfx_pc.cpp`):**
- **Q1 answered:** the gun-barrel row-strip shader is `id0=0000000010001218`, formula `texel = mix(vInput1, texVal0.rgb, vInput2)`, with (D397SHM) **vInput1 = G_CCMUX_PRIMITIVE, vInput2 = G_CCMUX_ENV_ALPHA**, so the LERP (front.c:2596 `gDPSetCombineLERP(TEXEL0, PRIMITIVE, ENV_ALPHA, PRIMITIVE, …)`) renders `out = PRIM + (TEXEL − PRIM)·(envA/255)`; black texels → `prim·(1 − envA/255)`. The 5-bit LERP coef-C in the packed combine word is 0x14 (= 20, i.e. the env-alpha value travels in the coef slot).
- **H1's "1/255 vs N64 1/32 coefficient" variant is effectively ruled out:** the PD ground-truth port feeds the *same* `env_color.a / 255.0` factor for the identical ENV_ALPHA LERP input (`pd_port/port/fast3d/gfx_pc.cpp` ~1548→1580; same `append_formula`/`mix` generation, same 5-bit-coef packing at :1949). The GE port matches the reference implementation, so the LERP factor is **not** a port-side scaling bug. (If the *N64 hardware* itself scaled the 5-bit coefficient differently, that would be a shared PD/GE port interpretation, not a GE regression — and it cannot explain a *periodic* pattern regardless.)
- **The visible defect is periodic (the 6-of-9-row comb), which a uniform LERP factor cannot produce** → the comb is **H2 (row-strip geometry / vertex-ring)**, not H1. The gray census (27/28/17) is the LERP factor applied on top; the *lines* are H2.
- **Next steps (next session):** (1) extend the probe / add an env-gated counter in `gfx_flush()` to test Q3 (does a 256-tri ring flush land between the two tris of a row strip, splitting them across draw calls with attribute-state drops?); (2) Q5 native 640×480 FBO capture to re-measure the comb period in native rows (expect ≈3 if it's the 40px-window period scaled); (3) H3 control — an N64/PD screenshot of the equivalent screen to bound how much of the darkened look is authentic.
- **Cleanup owed before merge:** remove the `GE_D397SH` probes in `gfx_opengl.cpp` (formula dump) and `gfx_pc.cpp` (D397SHM mapping dump).
- **Cleanup owed before merge:** remove the `GE_D397SH` probes in `gfx_opengl.cpp` (formula dump) and `gfx_pc.cpp` (D397SHM mapping dump) — **done 2026-09-28** (both blocks removed; see below).
**RESOLVED (2026-09-28) — H2 confirmed; fix landed in `gfx_draw_rectangle`.**
- **The falsifiable 3-scale test CONFIRMS H2.** Captured the file-select background at 640×480 (1.0×), 1024×768 (1.6×) and 1280×960 (2.0×) via `GE_PCDUMP` and measured the comb period in *native* rows (window-period ÷ scale, on a clean background band, detrended diff-autocorrelation): **s2 (1.6×) P_win=5 → P_nat≈3.1; s3 (2.0×) P_win=6 → P_nat=3.0 (two independent bands agree); s1 (1.0×) too fine to resolve (near-Nyquist, consistent).** A *fixed native period* (~3 rows) is exactly the beat of 0.75-px strips stacked at a 1.0-px pitch (0.75 = 3/4 → four strips span three native rows → a 3:4 beat). A period that scales with the window while staying fixed in native rows is the signature of a **fixed-point scan-converter flooring** artifact — not a LERP/CC factor (which would be scale-invariant and non-periodic). H2 confirmed.
- **Fix (port-layer, surgical, `gfx_pc.cpp` `gfx_draw_rectangle`):** after the sub-pixel offset is applied, floor the U10.2 top edge to its scanline and the bottom edge to the *last covered* scanline — the RDP scanline range `[top_scan, bot_scan+1]` — before rasterizing: `top_scan = uly>>2`, `bot_scan = (lry-1)>>2`, then `uly = top_scan<<2`, `lry = (bot_scan+1)<<2`. **No-op for whole-pixel rects** (their edges are already `4k`/`4k−1`, and the N64 `(<<2)−1` bottom idiom already yields `bot_scan` = the intended last row), so only sub-pixel-tall strips change — exactly the gun-barrel / file-select row strips. Mechanism + future-occurrence tell: `docs/porting-notes.md` §D15.
- **Verified by playtest:** the gun-barrel intro + file-select gradient render clean — the interlaced/comb look is gone (user-confirmed on the v0.4.0 playtest). `GE_D397SH`/`D397SHM` probes removed.
- **H3 note:** the *overall* darkened look (gray census 27/28/17) is the faithful LERP (H1, byte-identical to the PD port) and/or authored — **not** a defect. Only the periodic comb was the bug, and it is now fixed; do not "correct" the darkening.
## D398 — PC Options label: IMG_DOT texture replaces the D348 procedural slider glyph (2026-09-27)