diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 34d138d3..1700f1de 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -273,7 +273,7 @@ D393,FIXED,Controller B/Y both crouch; no in-game use button —,FIXED + user li D394,PARTIAL,Xbox 1.1 Jinx controller parity —,PARTIAL (port 1.1 gameplay button roles user-tested; exact gadget-category parity and other selectable presets outstanding; RB HD toggle unavailable). D395,PARTIAL,Controller trapped in keyboard/mouse-only binding capture —,"PARTIAL (both options UIs block pad-initiated capture; pad B can cancel an active modal; NTSC build linked, controller-only retest owed)." D396,PARTIAL,"F10 pad B/A used as slider decrement/increment, not back/select —","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,OPEN,SELECT FILE background + intro gun-barrel show an interlaced/comb look —,"OPEN, re-scoped + root-causing (no fix yet):" +D397,RESOLVED,SELECT FILE background + intro gun-barrel show an interlaced/comb look —,RESOLVED (2026-09-28): D398,FIXED,PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).,"SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there)." D399,PARTIAL,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).,"LANDED (port-only, D343 class):" D400,PARTIAL,"File-select ""PC Options"" label:","LANDED (port-only, frontoptions.c):" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 4668688b..bdb99bc4 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -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) diff --git a/docs/porting-notes.md b/docs/porting-notes.md index d47234d3..05598fdd 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1295,6 +1295,57 @@ GE on N64 renders gameplay at ~20–30 fps, so `g_ClockTimer` (= `speedgraphfram - **Repro correctly:** force the granularity with `GE_D318B_CLK=N` (TEMP probe, lockstep N ticks/frame) and A/B N=1 vs N=2. Wall-clock runs on a slow host (WSLg renders ~5 fps → mixed clk 1–6) and `GE_DETERM=1` (advances per VI-retrace request, also mixed clk) both give uncalibrated results. - **Where else to look:** any `for/while (numticks)` accumulator followed by a single threshold/floor consumer; D193 (AI locomotion rate) and D243 (cutscene `numticks` bursts) touched the same axis from the other side. +## D15. A texture-rectangle idiom that is sub-1-native-pixel tall relies on the RDP's floor-based fixed-point scanline stepping — now replicated in `gfx_draw_rectangle` (D397, RESOLVED 2026-09-28) + +GE's front-end vertical-gradient trick (`titleRenderFolderMenuBackgroundLines`, +`src/game/title2.c`) draws a tall image as 299 separate 1-scanline-tall +`gSPTextureRectangle`s, one per source row, each tinted by a per-row +interpolated `gDPSetPrimColor`. The lower-right Y coordinate uses the +universal N64 idiom `((y+1)<<2)-1`, which in U10.2 units is **3, not 4** — a +0.75-native-pixel-tall rect, not 1.0. This is correct *on real hardware* +specifically because the RDP's scan converter steps by truncating +(flooring) the fixed-point Y to an integer scanline before testing coverage, +so `y*4+3` still falls in scanline `y` and nothing else — the "-1" is a +hardware-truncation idiom, not a literal float height. + +`gfx_pc.cpp`'s `gfx_draw_rectangle` does not replicate that truncation: it +converts the U10.2 corners straight to continuous NDC floats +(`ulyf/lryf = ±(y / (4·HALF_SCREEN_HEIGHT)) + 1`) and hands them to the GPU's +own continuous rasterizer, which uses pixel-center/coverage rules, not +floor-stepped scanlines. A rect that is 0.75 (native) pixels tall, stacked +299 times with each row offset by exactly 1.0 native pixel from the last, +does not reliably rasterize as "exactly the one scanline the N64 idiom +intended" under a continuous rasterizer — whether a given row rounds to 0 or +1 device pixel depends on where its fractional edges land relative to the +device pixel grid, and that phase drifts row-to-row (worse under any +non-integer native→window viewport scale, `gfx_adjust_viewport_or_scissor`'s +`ratioX`/`ratioY`). Stacked over ~300 rows filling the screen, sporadic +per-row rounding reads as a periodic horizontal comb/interlace — this is the +leading mechanism for D397's "interlaced" file-select background and intro +gun-barrel, and is a **first-render, not re-entry-triggered** artifact +(unlike D182), because it is a rasterization property of the geometry itself, +not stale state. + +**Tell:** any GE front-end/menu draw that fakes a large image or gradient by +stacking many independent 1-scanline (or otherwise sub-1-native-pixel-tall) +`gSPTextureRectangle`s rather than one multi-row texture — the `(<<2)-1` +lower-right idiom is the signature to grep for. A single such rect is +invisible (sub-pixel rounding on one edge); many stacked ones make the +rounding pattern visible as banding. + +**Fix (landed 2026-09-28, RESOLVED):** `gfx_draw_rectangle` (`gfx_pc.cpp`) now replicates the RDP truncation. After the sub-pixel offset is applied, it floors 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 converting to NDC: `top_scan = uly>>2`, `bot_scan = (lry-1)>>2`, then `uly = top_scan<<2`, `lry = (bot_scan+1)<<2`. This is a **no-op for whole-pixel rects** (their edges are already `4k`/`4k−1`, and the `(<<2)−1` bottom idiom already yields `bot_scan` = the intended last row), so only sub-pixel-tall strips change. **Confirmed by a falsifiable 3-scale PCDUMP test:** the comb period is fixed at ≈3 *native* rows across 640/1024/1280 (window-period = 3×scale) — the beat of 0.75-px strips at a 1.0-px pitch — exactly the fixed-point-flooring signature, ruling out the scale-invariant LERP/CC factor. Verified by playtest: gun-barrel + file-select gradient render clean, comb gone. + +**Not the cause (checked and ruled out for D397):** the CC/LERP scaling of +`ENV_ALPHA` as `value/255.0f` (`gfx_pc.cpp` around the `G_CCMUX_ENV_ALPHA` +case) is byte-identical to the Perfect Dark PC port's (`gfx_pc.cpp` in the +local `pd_port` checkout) — same formula, no special-cased "5-bit RDP +coefficient" scaling in either port. The real RDP color combiner's A/B/C/D +inputs are 8-bit values (PRIM/ENV alpha come from an 8-bit `gDPSetEnvColor` +field); there is no coarser-precision quirk to correct here. A uniform +darkening from a nonzero `ENV_ALPHA` LERP factor is very likely faithful +(possibly authored/original-dev-intentional tinting), not a bug — do not +"fix" it without an N64/1964-GEPD reference screenshot proving otherwise. + ## E. Process / method notes - **Never `exit()` while another thread may be inside the GL driver (D344).** diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 8dadcfc7..eb8423d7 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -560,36 +560,6 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad vs_buf[vs_len] = '\0'; fs_buf[fs_len] = '\0'; -#ifdef PORT - /* D397 TEMP probe (env-gated): dump the generated texel formula per - * shader so the file-select gunbarrel combine can be inspected. */ - { - static int d397_on = -1; - if (d397_on < 0) d397_on = getenv("GE_D397SH") != NULL; - if (d397_on) { - fprintf(stderr, - "D397SH: id0=%016llx id1=%08x 2cyc=%d alpha=%d noise=%d edge=%d blur=%d grey=%d " - "c0r=(%u,%u,%u,%u) c0a=(%u,%u,%u,%u) c1r=(%u,%u,%u,%u) c1a=(%u,%u,%u,%u) " - "single(%d,%d/%d,%d) mult(%d,%d/%d,%d) mix(%d,%d/%d,%d)\n", - (unsigned long long)shader_id0, (unsigned)shader_id1, - (int)cc_features.opt_2cyc, (int)cc_features.opt_alpha, (int)cc_features.opt_noise, - (int)cc_features.opt_texture_edge, (int)cc_features.opt_blur, (int)cc_features.opt_grayscale, - cc_features.c[0][0][0], cc_features.c[0][0][1], cc_features.c[0][0][2], cc_features.c[0][0][3], - cc_features.c[0][1][0], cc_features.c[0][1][1], cc_features.c[0][1][2], cc_features.c[0][1][3], - cc_features.c[1][0][0], cc_features.c[1][0][1], cc_features.c[1][0][2], cc_features.c[1][0][3], - cc_features.c[1][1][0], cc_features.c[1][1][1], cc_features.c[1][1][2], cc_features.c[1][1][3], - (int)cc_features.do_single[0][0], (int)cc_features.do_single[0][1], - (int)cc_features.do_single[1][0], (int)cc_features.do_single[1][1], - (int)cc_features.do_multiply[0][0], (int)cc_features.do_multiply[0][1], - (int)cc_features.do_multiply[1][0], (int)cc_features.do_multiply[1][1], - (int)cc_features.do_mix[0][0], (int)cc_features.do_mix[0][1], - (int)cc_features.do_mix[1][0], (int)cc_features.do_mix[1][1]); - const char* tf = strstr(fs_buf, "texel = "); - if (tf) fprintf(stderr, "D397SHF: %s;\n", tf); - } - } -#endif - const GLchar* sources[2] = { vs_buf, fs_buf }; const GLint lengths[2] = { (GLint)vs_len, (GLint)fs_len }; GLint success; diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 413ab529..f0e09f6a 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -550,24 +550,6 @@ static void gfx_generate_cc(struct ColorCombiner* comb, const ColorCombinerKey& comb->used_textures[1] = used_textures[1]; // comb->prg = gfx_lookup_or_create_shader_program(shader_id0, shader_id1); memcpy(comb->shader_input_mapping, shader_input_mapping, sizeof(shader_input_mapping)); -#ifdef PORT - /* D397 TEMP probe (env-gated, remove before merge): dump which runtime - * G_CCMUX inputs this combine feeds as vInput1..N so the file-select - * LERP factor source can be settled against the measured census. */ - if (getenv("GE_D397SH")) { - fprintf(stderr, - "D397SHM: id0=%016llx cmode=0x%016llx map0=[%u %u %u %u %u %u %u] map1=[%u %u %u %u %u %u %u]\n", - (unsigned long long)comb->shader_id0, (unsigned long long)key.combine_mode, - (unsigned)comb->shader_input_mapping[0][0], (unsigned)comb->shader_input_mapping[0][1], - (unsigned)comb->shader_input_mapping[0][2], (unsigned)comb->shader_input_mapping[0][3], - (unsigned)comb->shader_input_mapping[0][4], (unsigned)comb->shader_input_mapping[0][5], - (unsigned)comb->shader_input_mapping[0][6], - (unsigned)comb->shader_input_mapping[1][0], (unsigned)comb->shader_input_mapping[1][1], - (unsigned)comb->shader_input_mapping[1][2], (unsigned)comb->shader_input_mapping[1][3], - (unsigned)comb->shader_input_mapping[1][4], (unsigned)comb->shader_input_mapping[1][5], - (unsigned)comb->shader_input_mapping[1][6]); - } -#endif } static struct ColorCombiner* gfx_lookup_or_create_color_combiner(const ColorCombinerKey& key) { @@ -3471,6 +3453,28 @@ static void gfx_draw_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lr uly += rdp.subpixel_ofs_y; lry += rdp.subpixel_ofs_y; + // D397: the N64 RDP floors the U10.2 top/bottom edges of a texture rect to + // whole scanlines before its per-scanline coverage test, so a rect covers + // scanlines [floor(uly/4), floor((lry-1)/4]]. A rect whose height is not a + // whole number of native pixels therefore rasterizes to a whole number of + // scanlines on hardware at every window scale. The gun-barrel / file-select + // gradient is built from 0.75px-tall row strips (title2.c: uly=(i+12)<<2, + // lry=((i+13)<<2)-1 -> 0.75px tall at 1px pitch); on the RDP each strip is + // exactly one scanline. The continuous-NDC conversion below instead passes + // the fractional bottom edge straight to the GPU, whose coverage test + // aliases the 0.75px pitch into a visible "comb" (H2: period fixed in + // native rows, ~3 = the 0.75px-at-1px-pitch beat, window period = 3*scale). + // Snap the top/bottom edges to the RDP scanline range so a sub-pixel strip + // rasterizes to exactly the scanline it targets. No-op for whole-pixel + // rects (edges already multiples of 4) and for the copy-mode +1<<2 edge + // (already a whole scanline), so normal fills are unchanged. + if (lry > uly) { + const int32_t top_scan = uly >> 2; + const int32_t bot_scan = (lry - 1) >> 2; + uly = top_scan << 2; + lry = (bot_scan + 1) << 2; + } + // U10.2 coordinates float ulxf = ulx; float ulyf = uly;