docs: the P1 diff, and the one question it raised
`main_menu` -- the milestone's gate -- agrees with the reference renderer to within 3/255 on every channel of every pixel, RMSE 0.38 %, no pixel above 4 %. Nine of the twelve screens are at or under that; two are byte-identical. Three exceed it, and each gets a cause rather than a wider tolerance: * `title` (max 6): every disagreement is INSIDE A TIE -- the derived order and the CLI's measured-off-the-game order differ only among elements with identical layer keys. That is exactly the residual HANDOFF Q3 documents and the export already declares in `unresolved: paint_order_ties`. Cost: 904 px at 4-6/255 in one glow band. The port keeps the stable sort; fitting it to one screen's capture would be tuning. * `title_jp` (max 154): `ptlogo_eff2` at 125 % is the only drawn element in the whole export at a scale that is not a whole multiple of 100 %, and `title_jp` is the only screen over 6/255. Same fact twice. `ui_layout::blit` samples the source at the destination pixel's top-left corner; a GPU samples at its centre, and at 125 % those disagree on one column in five. I think the CLI is the one that is wrong -- corner-sampled nearest is a half-pixel bias toward the top-left that no rasteriser produces. But that is a reading, not a measurement: it needs a framebuffer capture of the Japanese title screen, so it is filed in BLOCKED.md as a question. The port is NOT changing to match, because matching would mean reproducing a half-pixel offset on purpose to make a number smaller. * `extras` (max 4): two pixels. Also reconciled: the pivot question predicted a P1 diff could not distinguish the declared pivot from half the texture, because both renderers use the declared one. That held. Recorded so the agreement is not later mistaken for evidence -- and P2 will not settle it either. BLOCKED.md's `/reborn` HEAD updated to bc6354d, with a note that the mount is read-only by design and `git -C /reborn pull` fails rather than being skipped.
This commit is contained in:
21
README.md
21
README.md
@@ -42,8 +42,27 @@ assuming a value is on the disc.
|
||||
| `port/` | the Godot 4 project |
|
||||
| `authored/` | decisions that are **not** on the disc, each with its reason |
|
||||
| `export/` | generated, gitignored, never hand-edited |
|
||||
| `tools/` | verification harnesses that hold the port to the reference renderer |
|
||||
| `docs/` | the mission, the format spec, the agent's loop prompt |
|
||||
|
||||
## Verifying
|
||||
|
||||
`sylpheed-cli screen render` -- built from the same `sylpheed-formats` revision
|
||||
the exporter is pinned to -- is the reference renderer. `tools/verify-screen`
|
||||
draws every exported screen both ways and reports the largest per-channel
|
||||
difference in the frame:
|
||||
|
||||
```
|
||||
tools/verify-screen # every screen in the manifest
|
||||
tools/verify-screen main_menu # one of them
|
||||
```
|
||||
|
||||
Where the two disagree, one of them is wrong; `docs/DECISIONS.md` says which and
|
||||
why, rather than tuning the port until the number goes down.
|
||||
|
||||
## Status
|
||||
|
||||
Pre-P0. Nothing runs yet.
|
||||
**P1.** The exporter writes `GP_TITLE`'s twelve screen builds and their sprites,
|
||||
and the Godot project draws any of them statically at 1280x720 from that tree
|
||||
alone. `main_menu` matches the reference renderer to within 3/255 on every
|
||||
channel of every pixel. Next: P2, keyframe animation.
|
||||
|
||||
@@ -9,7 +9,9 @@ a decoded one a month from now. Where a milestone can proceed with a placeholder
|
||||
the placeholder goes in `authored/` with a `why` naming the question it stands in
|
||||
for, so it is deleted rather than forgotten when the answer arrives.
|
||||
|
||||
Last reconciled against HANDOFF.md on **2026-08-28**, at `/reborn` HEAD `e81dcad`.
|
||||
Last reconciled against HANDOFF.md on **2026-08-28**, at `/reborn` HEAD `bc6354d`.
|
||||
(`/reborn` is mounted read-only, so `git -C /reborn pull` fails by design; the
|
||||
mount is refreshed outside this container and HEAD is read, not fetched.)
|
||||
|
||||
## Still open — these block work
|
||||
|
||||
@@ -46,6 +48,28 @@ than exported:
|
||||
Not blocking anything today; raised because the port found them and a guess here
|
||||
would be believed later.
|
||||
|
||||
### Does the game sample a scaled sprite at the pixel corner or the pixel centre?
|
||||
|
||||
Found at P1, by the only screen it could have been found on. `title_jp`'s
|
||||
`ptlogo_eff2` is the **single drawn element in the whole export** at a scale that
|
||||
is not a whole multiple of 100 % (125 %), and `title_jp` is the only one of the
|
||||
twelve screens whose Godot-vs-CLI diff exceeds 6/255.
|
||||
|
||||
The two renderers pick different source texels at a non-integer ratio.
|
||||
`sylpheed_formats::ui_layout::blit` samples at the destination pixel's **top-left
|
||||
corner** (`sxi = col * sw / dw`); a GPU samples at its **centre**
|
||||
(`floor((col+0.5)*sw/dw)`). At 125 % they disagree on one column in five — ~30
|
||||
pixels above 100/255, strung along thin diagonal edges. At every whole multiple
|
||||
of 100 % they agree exactly, which is why the other eleven screens are clean.
|
||||
|
||||
The port has **not** changed to match: matching would mean reproducing a half-
|
||||
pixel bias on purpose to make a number smaller. The question for the RE agent,
|
||||
when it is cheap: **a framebuffer capture of the Japanese title screen** would
|
||||
settle it outright, and it is the kind of thing a capture answers in one look.
|
||||
|
||||
Cost of being wrong either way: a one-texel edge on one glow, on a screen the
|
||||
English boot path never shows. This is filed, not urgent.
|
||||
|
||||
### The pivot is not half the texture on `GP_TITLE`
|
||||
|
||||
`sylpheed-formats`'s `ui_layout::Element::pivot_x` is documented as "for a `.t32`
|
||||
@@ -68,3 +92,8 @@ differ by up to 59 px on `ptlogo_back2`, which is visible. Until then the port
|
||||
follows the decoders and uses the declared pivot, which is also what
|
||||
`sylpheed-cli screen render` does — so a P1 diff cannot distinguish them, and
|
||||
agreement between the two is not evidence.
|
||||
|
||||
**P1 has now been run and that prediction held.** The port and the CLI agree on
|
||||
every scaled element across all twelve screens; the question is untouched by it.
|
||||
It will stay untouched by P2 as well, since P2 animates the same two renderers'
|
||||
shared assumption. Only a capture answers this.
|
||||
|
||||
@@ -114,3 +114,161 @@ The disc has no time slot on the last keyframe of a group. A file that carries
|
||||
one there has invented it. `check` rejects it — this is the one place where the
|
||||
temptation to emit a plausible number is strongest and the resulting error is
|
||||
completely invisible.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Godot draws the screen, 2026-08-28
|
||||
|
||||
### The Godot side reads the manifest, not a path
|
||||
|
||||
`ExportTree` is the only class that knows where `export/` is: `SYLPHEED_EXPORT`
|
||||
if set, otherwise `<project>/../export`. Screens are addressed by their manifest
|
||||
**name** (`main_menu`), never by a file path, so the runtime never encodes the
|
||||
archive's subdirectory and a re-export that moves a file does not break it. It
|
||||
also checks `format` on both the manifest and each screen, and refuses a tree it
|
||||
was not built to read rather than half-drawing one.
|
||||
|
||||
Textures are read as bytes and decoded with `load_png_from_buffer` at runtime.
|
||||
They are deliberately **not** Godot-imported resources: `export/` is gitignored
|
||||
and regenerated wholesale, and a `.import` sidecar per sprite would be derived
|
||||
state living next to derived state, invalidated on every re-export.
|
||||
|
||||
### One CanvasItem draws the whole screen
|
||||
|
||||
`ScreenView._draw` walks `paint_order` and draws each element itself, rather
|
||||
than making a node per element and leaning on `z_index`. The export's
|
||||
`paint_order` is already back-to-front, so honouring it is a loop; expressing
|
||||
the same order through sixteen nodes' z-indices would hide the one thing that is
|
||||
still unresolved about it — the **ties** — behind Godot's own sibling rules,
|
||||
where a change in the export would silently become a change in Godot's tree
|
||||
order instead of a visible change in the draw sequence.
|
||||
|
||||
### P1 draws `rest` and nothing else
|
||||
|
||||
Every element is drawn at its resting pose. No keyframe interpolation: that is
|
||||
P2, and it depends on the keyframe time unit, which is **measured** rather than
|
||||
decoded. A milestone whose gate is a pixel diff must not have a measured
|
||||
constant inside it, or the diff stops being evidence about the port.
|
||||
|
||||
For the same reason `focused_id` is empty at P1. Initial focus was measured as
|
||||
unstable boot to boot (HANDOFF Q5), so choosing one is an authored decision and
|
||||
it belongs to P5, where a human is pressing keys.
|
||||
|
||||
### Nearest-neighbour, and why that is not a preference
|
||||
|
||||
`TEXTURE_FILTER_NEAREST`. The export is a 1:1 copy of the disc's own texels and
|
||||
elements draw at up to 500 %; a bilinear filter invents detail the disc does not
|
||||
have. It is also what the reference renderer does — `ui_layout::blit` maps
|
||||
destination to source by integer division — so a filter difference cannot
|
||||
masquerade as a placement difference in the diff.
|
||||
|
||||
### The capture is the SubViewport, not the window
|
||||
|
||||
The screen is drawn into a `SubViewport` sized to the export's own `design`
|
||||
rectangle and shown through a container that scales it to the window. The first
|
||||
attempt captured `get_viewport()` and got **1235×695**: there is a window manager
|
||||
on the Xvfb display and its title bar had eaten 45×25 px of a screen the export
|
||||
declares as 1280×720. A gate that compares a rescaled 1235×695 capture against a
|
||||
1280×720 composite measures the compositor.
|
||||
|
||||
So `--capture` grabs the SubViewport texture: exactly the design rectangle,
|
||||
independent of the window, directly comparable with `screen render` with no crop
|
||||
and no resample. The windowed run is still worth doing — it is what proves a
|
||||
human sees the screen — but it is not what the numbers come from.
|
||||
|
||||
## P1 gate — the diff, and what it found
|
||||
|
||||
`tools/verify-screen` renders every screen in the manifest both ways and reports
|
||||
the largest per-channel difference anywhere in the frame. Both renderers are held
|
||||
to the same inputs: the reference CLI built by `build-reference-cli` from the
|
||||
revision the exporter is **pinned** to (not `/reborn/target/`, which is a live
|
||||
mount that moves mid-iteration), `--black` because the screen carries its own
|
||||
background, and `--primitives --animated` because those are what make the CLI
|
||||
draw the same element set the port draws at rest.
|
||||
|
||||
| screen | build | max per-channel Δ | |
|
||||
|---|---|---|---|
|
||||
| `main_menu` | 5 | **3** | the P0/P1 gate screen |
|
||||
| `main_menu_jp` | 8 | 3 | |
|
||||
| `extras` / `extras_jp` | 6 / 9 | 4 / 3 | |
|
||||
| `press_start` / `press_start_jp` | 2 / 3 | 1 | |
|
||||
| `build_00` / `build_01` | 0 / 1 | 3 | |
|
||||
| `build_10` / `build_11` | 10 / 11 | **0** | byte-identical |
|
||||
| `title` | 4 | 6 | paint-order tie, below |
|
||||
| `title_jp` | 7 | 154 | sampling phase, below |
|
||||
|
||||
`main_menu` — the milestone's own gate — agrees to **≤3/255 on every channel of
|
||||
every pixel**, RMSE 0.38 %, with **no** pixel differing by more than 4 %. 3/255
|
||||
is what integer-truncating compositing in the CLI and float rounding on a GPU
|
||||
differ by; there is no structural disagreement anywhere in the frame.
|
||||
|
||||
Three screens exceed that, and each has a named cause rather than a threshold.
|
||||
|
||||
### `title`: a tie in the paint order — neither renderer is wrong
|
||||
|
||||
Build 4 is the one screen where the CLI uses a paint order **measured off the
|
||||
running game** instead of deriving it. Compared against the order this port
|
||||
exports, every single disagreement is **inside a tie** — the two orders differ
|
||||
only among elements carrying *identical* layer keys (`0x8083`, the `back2` glow
|
||||
group, and `0x80a0`):
|
||||
|
||||
```
|
||||
derived : … 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, …
|
||||
measured: … 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, …
|
||||
```
|
||||
|
||||
That is exactly the residual HANDOFF Q3 documents and this export already
|
||||
declares in `unresolved: ["paint_order_ties"]`. It is worth stating what it
|
||||
costs: **904 px** in the glow band at (445,117)–(1195,313), all of them 4–6/255.
|
||||
The port keeps the stable sort, per HANDOFF's own recommendation. Nothing to fix,
|
||||
and nothing to tune — a "fix" here would be fitting the port to one screen's
|
||||
capture.
|
||||
|
||||
Two of the reordered indices (`0x80a0`) are `kind & 0x4` template instances that
|
||||
both renderers skip, so the only real reorder outside the glow group is
|
||||
`ptlogo2` against `ptlogo_tm`, which do not overlap.
|
||||
|
||||
### `title_jp`: nearest-neighbour sampling phase — the CLI is the one I would call wrong
|
||||
|
||||
`title_jp` is the **only** screen in the export with a drawn element at a scale
|
||||
that is not a whole multiple of 100 %: `ptlogo_eff2` at 125 %. It is also the
|
||||
only screen with a difference above 6/255. The two facts are the same fact.
|
||||
|
||||
At a non-integer ratio the two renderers pick different source texels:
|
||||
|
||||
* `ui_layout::blit` samples the source at the destination pixel's **top-left
|
||||
corner** — `sxi = col * sw / dw`.
|
||||
* A GPU samples at the destination pixel's **centre** — `floor((col+0.5)·sw/dw)`.
|
||||
|
||||
At 125 % those disagree on one column in five, which is why the differing pixels
|
||||
are ~30 above 100/255 strung along thin diagonal edges rather than a shifted
|
||||
region. At every whole multiple of 100 % they agree exactly, which is why the
|
||||
other eleven screens are clean.
|
||||
|
||||
**Which is wrong:** the CLI, I think. Corner-sampled nearest is a half-
|
||||
destination-pixel bias toward the top-left that no rasteriser produces, and the
|
||||
Xenon GPU that drew this screen sampled at pixel centres. But I have no
|
||||
framebuffer capture of `title_jp` and the disagreement is sub-pixel on one glow,
|
||||
so this is a reading, not a measurement — recorded in `docs/BLOCKED.md` rather
|
||||
than acted on. **The port is not changing to match**, because matching the CLI
|
||||
here would mean deliberately reproducing a half-pixel offset in order to make a
|
||||
number smaller.
|
||||
|
||||
### `extras`: two pixels
|
||||
|
||||
Two pixels at 4/255. Rounding.
|
||||
|
||||
### What the diff cannot tell us
|
||||
|
||||
The pivot question in `docs/BLOCKED.md` predicted that a P1 diff could not
|
||||
distinguish "anchor scale to the declared pivot" from "anchor to half the
|
||||
texture", because both renderers use the declared pivot. That prediction held:
|
||||
the port and the CLI agree on every scaled element, and that agreement is **not
|
||||
evidence** about which anchor the game uses. It stays open.
|
||||
|
||||
### `pteff05.t32` and `pteff04.t32` have no sprite, and that is correct
|
||||
|
||||
Both renderers skip them. The bundle declares them and carries **zero** RATC
|
||||
children for either, so there is no texture on the disc to export — this is a
|
||||
property of the disc, not a gap in the exporter, and `ScreenView` reports it as
|
||||
`no sprite in the export` rather than dropping it silently.
|
||||
|
||||
Reference in New Issue
Block a user