diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index d5ae9d66..eaa5fa62 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -104,7 +104,7 @@ method finding that applies to the Decoder's harness as much as this one: | # | ask | why the port cannot answer it | |---|---|---| -| **H1** | **Does a held direction REPEAT in the menus, and at what rate?** Initial delay and repeat interval, for the d-pad and for the left stick separately if they differ. | Behavioural. The port now does **one step per deflection** — `authored`, and deliberately the conservative half: it cannot run away and it invents no rate. If the game repeats, a human will feel it as *"I have to flick it again"*. | +| **H1** | 🟢 **PARTLY ANSWERED 2026-09-01 and the port has adopted the half that landed.** The game **digitises the left stick to four direction bits at 61 % deflection** and never sees a velocity (`input-button-numbering-is-remapped.md`). `Gamepad.ENTER` moves **0.5 → 0.61**: 0.5 was a *floor* (Godot's `ui_*` action deadzone), 0.61 is the game's own threshold, and between them Godot reports a direction the real game does not. Asserted at the device level in `tools/port/verify-input`. **Still open: does a held direction REPEAT, and at what rate** — one step per deflection remains authored, and the digitise-to-bits mechanism corroborates that a rate cannot come from this layer. | 🔴 **The 0.11 hysteresis gap stays AUTHORED** — nothing measured says the game has hysteresis at all. ⚠️ **A human chose 0.5 and 0.61 changes how the stick feels**; revert that one constant if it reads as needing too much push. ✅ And I never consumed the mislabelled pad bit table — checked by grep over `port/`, `authored/` and `tools/port/`, not remembered. | | **H2** | ✅ **ANSWERED 2026-09-01, and the port's own premise was wrong.** The Decoder found **no post-process pass** on either splash (`auto/frame-blend-draw-path` `3cc1b51`). The blur is **in the asset**: every logo ships a pre-blurred copy ~21×20 px larger and concentric, drawn alone first and crossfaded out as the sharp logo fades in — which is the human's *"blurred to clear/sharp, in about a second"*, with no shader. | 🔴 **"The port applies no blur at all" was FALSE and this page said it.** The port draws all seven quads of the developer splash, the three companions included, and always has. Verified by a frozen sweep, 3 units a step: companions alone at u=3–15, all seven at u=18–42, sharp only from u=45. Writing down what the renderer was *believed* to do instead of running it is what produced the claim. What remains is the **extent**, and it is the same unit question as H3 — asked there, once. See [`plate-arrival-halves.md`](plate-arrival-halves.md). | | **H3** | ✅ **CLOSED 2026-09-01. ALL FOUR NAMED CAUSES ARE DEAD AND THE HUMAN'S OBSERVATION IS NOT — recorded that way deliberately rather than left green.** The rate is **56.8 units per guest second**, measured, control at 1.15 %, two elements agreeing at one clock (`units-per-second-measured.md`). 30 and 120 both excluded. At 56.8 the plate's t=236 lands at **4.15 s** against the port's 3.93 s — the port is fractionally **early**. | Eliminated in order: `rest.t` (the arrival is a declared keyframe), the clock origin (85/85 filmed frames share one clock), the anchor (answered t=160, and `clock: "shared"` survives it), the unit constant (56.8). 🔴 **`units = 2 × frames` is dead as a route** — the same animation takes 21 labels in one capture and 33 in another. ✅ Audited: this port never used it; `boot.gd` integrates `delta * units_per_second`, so the retirement cost a *justification* in `authored/timing.json` and not a behaviour, and that file's second leg (12 declared units against a 0.14–0.30 s black plateau = 40–86 units/s) has no frames in the chain. **60 units/s is KEPT** — 56.8 is 5.6 % away against ~5 % quantisation and the Decoder did not ask for a move. ⚠️ Reach is the **title**; the splashes are a different `GamePart`. What the human saw is now unattributed — see [`plate-arrival-halves.md`](plate-arrival-halves.md) for the two remaining candidates, of which the strongest is that **Ⓐ was unbound on the play-test build so that human could not skip the 137 s intro**, and the run they judged is not the run any of these measurements describes. | diff --git a/docs/port/verify-screen-blend-divergence.md b/docs/port/verify-screen-blend-divergence.md index a4615730..8b15134e 100644 --- a/docs/port/verify-screen-blend-divergence.md +++ b/docs/port/verify-screen-blend-divergence.md @@ -255,3 +255,72 @@ express, and the allowance cannot go stale against the map it is computed from. * That the port's additive set is complete. It covers three screens; nobody has logged the register on the rest. * Anything about the four undiagnosed rows. + + +--- + +# 🔴 Re-validated on the GPU, and my prediction failed: these numbers are rasteriser-specific + +The human activated a hardware GPU on 2026-09-01. **Every number on this page +above was measured under `llvmpipe`**, so they needed re-deriving before anyone +builds on them. + +## Pre-registered (R2) + +> Both renderers blend in encoded 8-bit space, so if the port's drawing is +> rasteriser-independent the diffs should be **identical, or within 1 level**. +> Anything materially different means a published conclusion here was +> GPU-specific. + +## It failed + +| screen | llvmpipe mean | **GPU mean** | change | max (llvmpipe → GPU) | +|---|---|---|---|---| +| `title` | 0.4431 | **0.5936** | **+34 %** | 41 → 41 | +| `main_menu` | 3.9363 | **4.1449** | +5.3 % | 97 → 97 | +| `extras` | 6.7422 | **6.9757** | +3.5 % | 113 → 113 | +| `title_jp` | 2.7715 | **2.9448** | +6.3 % | 233 → 233 | +| `main_menu_jp` | 0.7885 | **1.0157** | **+29 %** | 26 → 27 | +| `extras_jp` | 0.6592 | **0.8906** | **+35 %** | 26 → 26 | +| `build_12` / `build_15` | 0.0368 | **0.0454** | +23 % | 17 → 17 | + +**Every mean rose, by 3–35 %.** So the diffs are *not* rasteriser-independent +and the prediction was wrong. + +## What survives, and what does not + +🔴 **The maxima are unchanged** — 41, 97, 113, 233, 17 identical, and 26 → 27 on +one row. The large differences are exactly where they were. + +That is the shape of a **rounding population growing, not content moving**: +llvmpipe and the NVIDIA rasteriser round the last bit of a blend differently, so +the ≤1-level tier grows while the elements that genuinely differ do not move at +all. Consistent with both still blending in encoded space — which the control on +this page established for Godot generally, not for one rasteriser. + +**What survives:** + +* the **additive diagnosis**, because it rests on an *ordering*, and the ordering + holds on the GPU: `extras` 6.98 > `main_menu` 4.14 > `extras_jp` 0.89 and + `main_menu_jp` 1.02 > `title` 0.59 — nine additive elements, then five, then + none; +* the **`pgloading_loop5` localisation**, which is a bounding box; +* the **`build_00`/`build_01` agree** result — still 0 pixels over the bar; +* the **derived allowance**: the failing set is the same four rows. + +**What does not, and is now labelled:** + +* the histogram (*53 % within 1 level, 16 844 over 40*) was llvmpipe-specific and + the ≤1 tier is larger on the GPU; +* every absolute **mean** quoted above this section; +* the **RMSE-vs-capture** pair (3151.96 / 3769.61) was llvmpipe. The *ordering* + claim — the port is nearer than the reference — has not been re-derived on the + GPU and is not claimed here until it is. + +## The rule this earns + +**A renderer comparison carries its rasteriser as a hidden parameter.** Nothing +in this corpus recorded which one produced a diff, and for eight months there was +only one so it never mattered. Any diff quoted from here on should say what drew +it — the same discipline `TEMPORAL-VERIFICATION.md` already demands for capture +rate, applied to the thing that rasterises rather than the thing that clocks. diff --git a/port/scripts/gamepad.gd b/port/scripts/gamepad.gd index b2abbca3..eade6bca 100644 --- a/port/scripts/gamepad.gd +++ b/port/scripts/gamepad.gd @@ -55,11 +55,33 @@ extends RefCounted ## human will notice as "I have to flick it again", and the fix is a measured ## repeat interval — not a guessed one. Logged for the Decoder as `pad-repeat`. -## Godot's action deadzone for the `ui_*` actions is 0.50, so the latch must not -## arm below it — the action itself would not read as pressed and the step would -## be swallowed anyway, leaving the latch armed against a press that never -## happened. -const ENTER := 0.5 +## ✅ DECODED 2026-09-01, and it replaces an authored value. +## +## This was **0.5**, chosen as a *floor* rather than as a value: Godot's action +## deadzone for the `ui_*` actions is 0.50, so the latch must not arm below it — +## the action itself would not read as pressed and the step would be swallowed +## anyway, leaving the latch armed against a press that never happened. That +## reasoning still holds and 0.61 is comfortably above the floor. +## +## The game's own threshold is now measured: it **digitises the left stick to +## four direction bits at 61 % deflection**, so it never sees a velocity at all +## (`docs/re/input-button-numbering-is-remapped.md` and the corrected +## `input-pad-read-path.md`). Between 0.50 and 0.61 Godot reports `ui_down` +## pressed and the real game reports nothing; at 0.5 this port stepped there. +## +## 📌 The mechanism also corroborates the human's fix rather than merely +## agreeing with it: a control that digitises to bits cannot express a rate, so +## "one step per deflection" is what the hardware layer *can* produce, not a +## conservative guess that happened to look right. +## +## ⚠️ **The 0.11 gap to `RELEASE` stays AUTHORED.** Nothing measured says the +## game has hysteresis at all, let alone how wide. Only the arm threshold moved. +## +## 🔴 **This changes how the stick feels and a human chose the old number.** +## Asserted at the device level below and in `tools/port/verify-input`, but a +## feel-test is the real check: revert this one constant to 0.5 if 0.61 reads as +## a stick that needs pushing too far. +const ENTER := 0.61 ## Release lower than it arms. Without the gap a stick resting near 0.5 chatters ## across the boundary and re-arms on noise, which is the original bug wearing a diff --git a/tools/port/verify-input b/tools/port/verify-input index afa90e7e..ef2e5a1e 100755 --- a/tools/port/verify-input +++ b/tools/port/verify-input @@ -60,7 +60,7 @@ var ran := 0 ## removes it. A check whose subject cannot be removed is skipped there and ## counted, not silently dropped -- a control that quietly tests four of nine ## things reports the same green line as one that tests all nine. -func ok(name: String, subject: String, cond: bool, detail: String = "") -> void: +func ok(name: String, subject: String, cond: bool, detail: String = "", control_row: String = "the stick row (6 -> 1)") -> void: if mode == "control" and subject == "godot": print(" %-44s -- not controllable (Godot's own binding)" % name) return @@ -72,8 +72,12 @@ func ok(name: String, subject: String, cond: bool, detail: String = "") -> void: # the same code path, reduces 6 stick events to 1. That row is the # positive control for this one, and naming it is the honest move; # inverting it would have been a green line that meant nothing. - print(" %-44s -- negative; positive control is the stick row (6 -> 1)" - % name) + # 🔴 The control row was HARDCODED here and a second negative arrived. + # A negative that names someone else's control is not controlled; it is + # borrowing a green line. `control_row` now defaults to the original + # text so that row is unchanged, and any new negative must say what + # actually backs it. + print(" %-44s -- negative; positive control is %s" % [name, control_row]) return ran += 1 var want: bool = cond if mode != "control" else not cond @@ -165,6 +169,26 @@ func _init() -> void: nav([0.9, 0.45, 0.9, 0.45, 0.9]) == 1, "%d step(s)" % nav([0.9, 0.45, 0.9, 0.45, 0.9])) + # ✅ THE GAME'''S OWN THRESHOLD, ASSERTED AT THE DEVICE LEVEL. The game + # digitises the stick to four direction bits at 61 % deflection, so a + # deflection between Godot'''s 0.50 action deadzone and that 0.61 is a + # direction the real game never sees. At the old ENTER = 0.5 this port + # stepped there. Negative first, then the positive control on the SAME run + # shape -- a negative alone would also pass if the latch were simply broken. + # 🔴 THIS ROW WAS "latch" AND THE CONTROL CAUGHT IT IMMEDIATELY. Removing + # the latch does not remove the THRESHOLD -- the unlatched path also tests + # `>= Gamepad.ENTER`, so 0.55 counts 0 either way and the row could never + # invert. The harness said so in one run: "a check did not invert -- it is + # not testing what it claims to test". It is a negative, and its positive + # control is the row below it: the same shape at 0.70 does step. + ok("0.55 is below the game 61 % threshold, must not step", "negative", + nav([0.55, 0.55, 0.55]) == 0, + "%d step(s)" % nav([0.55, 0.55, 0.55]), + "the 0.70 row on the same shape") + ok("...and its control: 0.70 on the same shape DOES step", "latch", + nav([0.70, 0.70, 0.70]) == 1, + "%d step(s)" % nav([0.70, 0.70, 0.70])) + # A button already IS an edge; latching it would swallow the second of two # quick taps. var pad := Gamepad.new()