Commit Graph

1059 Commits

Author SHA1 Message Date
sylph-decoder
7737df6ba1 tools: a screen oracle that matches CONTENT, with movie frames as its controls
The statistics oracle (green/white/mean) cannot reject the class it exists to
reject. A frame of ADV.wmv with a bright green laser reads green 0.0018 /
white 0.086 / mean (53,67,76) -- the title's numbers -- and a probe built on it
tapped (A) into the movie, then waited 120 s for a menu that was never coming.

screen_match correlates against committed captures instead. Controls run before
it was ever used live: 8/8, and the negatives are COMMITTED movie frames rather
than scratch grabs -- an earlier list pointed at two scratch files and a later
run of the same probe overwrote one, failing the control for the wrong reason.

Two paths, both controlled. The exact path costs 1503 ms/frame, which is fine
offline and catastrophic in a live loop; fast=True decimates 4x for 38-75 ms and
agrees with the exact path to +/-0.005 on all eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
2026-08-29 11:50:18 +00:00
sylph-decoder
7e20853592 re: withdraw the no-disc banner -- the disc is mounted, and both instruments that said otherwise were blind to it
The container was replaced at 11:07:38 UTC, 25 minutes after b9aca6a wrote
"the decoder container has no disc". /disc is a real read-only bind mount
(device 2050 against /'s 92), 6.2 GB, 74 entries under dat/, and
`sylpheed-cli screen list` returns 12 GP_TITLE builds.

The reusable half is the instruments. `find / -xdev` cannot cross into a bind
mount on another device, so its "no ISO, no default.xex, no GP_TITLE.pak"
is what it returns whether or not the disc is there. `sylph-doctor` only ever
looks under /work and never consults $SYLPHEED_DISC, so it still reports
"no ISO" against a disc that works. Two instruments, one blind spot, read as
corroboration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
2026-08-29 11:12:36 +00:00
sylph-decoder
3a87a26235 game: fill in the player's-eye navigation map from the committed oracle frames
Sections 1 to 3 were almost entirely open questions. Everything a capture in
docs/re/captures/ actually shows is now written down from the chair: what is on
each screen, what the cursor does, and what each footer offers.

Boot: the publisher plate is SQUARE ENIX, the developer plate is GAME ARTS /
SETA / studio anima, both still pictures the game draws rather than video, then
the cinematic -- one A skips it, 57 s to the title against 193 s without.

Main menu: the five labels and where each goes, the wrap rule, and the caveat
that initial focus varied across four boots. Carries the footer warning from the
measurement in the same push.

Submenus: NEW GAME's DIFFICULTY and SELECT DATA; LOAD GAME's slot carousel,
Details panel and its five-button footer; the six tutorial lessons in two
groups; OPTIONS' four categories; EXTRAS' three items; and MISSION SELECT with
the locked-list explanation for the cursor that would not move.

What stays open is marked open, and it is now the residue no capture answers
rather than the residue nobody looked at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UxPvE5cz7zekXBKi7Xw2r
2026-08-29 10:43:12 +00:00
sylph-decoder
f6d49da612 re: two menu facts re-measured from the committed captures, no disc needed
Refutation attempt, per the adversarial duty. Target: this page's own row "B on
the main menu goes to the title". Chosen because it is one of only two Q5 rows
with an empty evidence cell, and because it is the only exit from the main menu,
so the port will build on it.

Whole-frame colour test for the pad-glyph discs. The main menu carries ZERO
red-B pixels anywhere in the frame, on two independent captures, while the same
unchanged detector finds 514 on EXTRAS and 518 on DIFFICULTY. The control passes
twice: the A glyph reads 438/438/440/438 across all four screens, so it is one
asset at one size and a B of that family could not have slipped under a
threshold. The main menu's legend is "Select / OK"; every submenu adds "Back".

The claim SURVIVES -- a legend is not behaviour, and an absent glyph cannot
refute an observed press -- but it is downgraded to amber. The observation is
uncited and single, it is now the only Q5 row the game's own text contradicts,
and there is a named confound: the title-side screens auto-return after ~8-10 s
idle, which looks exactly like what was described. Reading 0x828A690C while
pressing B would separate them in one run; that run needs a disc this container
does not have.

Second finding, same method. MISSION SELECT's "sixteen d-pad presses never left
Stage 01" was a LOCKED stage list, not a broken one. The labels have three
brightnesses, not two -- locked 104, unlocked 183, focused 254 -- and the
all-story-unlocked capture is the control that separates the lower two while
holding row 1 at an identical 254. On that save the cursor reaches Stage16 at the
bottom of a scrolled list. The list is 16 long and shows 8 at a time.

Regenerator committed beside the finding; it reads only files already in git.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UxPvE5cz7zekXBKi7Xw2r
2026-08-29 10:43:01 +00:00
sylph-decoder
b9aca6a2d9 re: the decoder container has no disc -- root-caused to the volume migration
find / -xdev turns up no ISO, no default.xex and no GP_TITLE.pak; /exchange is
empty; SYLPHEED_DISC is unset; sylph-doctor agrees. Everything else in the
container is healthy -- xenia_canary is built, :98 is up, screenshot works,
Vulkan enumerates. There is simply no game to boot.

The cause is in the launcher. Before 06676d3, sylph-agent bind-mounted the
human's working tree at /work, and the ISO and sylph_extract/ live in that tree,
so the disc arrived incidentally with the repository mount. 06676d3 replaced
that with `-v sylpheed-decoder-repo:/work` -- correct for the collision class it
was written for -- and nothing was added to replace the disc. sylph-decoder
still forwards SYLPH_ISO, but as a bare environment variable naming a host path
that does not exist inside the container. sylph-port mounts the disc explicitly,
so the one container that owns the disc and the oracle is the one without them.

This shuts the oracle, every sylpheed-cli call that names a pak, the disc-gated
tests, and -- because the XEX is on the disc -- the static PPC route too. It
does not touch the committed corpus, which is what this iteration worked from.

A second, smaller casualty of the same migration: no git identity is configured
anywhere, so the first commit in a fresh container fails outright. Both are
recorded with their fixes; neither is worked around, since the launcher runs on
the host and this container cannot restart itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UxPvE5cz7zekXBKi7Xw2r
2026-08-29 10:42:47 +00:00
MechaCat02
06676d3dc0 containers: each agent clones the monorepo into its own volume
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 7m48s
CI / WASM — Web (push) Failing after 7m16s
CI / Formatting (push) Failing after 1m15s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The last structural fix for the collision class that has bitten three times. Both
containers now clone the repository into their OWN named volume instead of
bind-mounting a human's working tree, so an agent's local git config cannot
capture a human's commits, a credential helper cannot leak a container-only path
onto the host, and a `git add -A` cannot sweep another party's in-flight files.

Cloned once at startup and never auto-pulled: pulling under a running agent
moves files out from under whatever it is mid-edit, which is the same bug again.

Accepted knowingly: Claude Code keys per-project memory off the working
directory, so moving off the host path starts that memory empty. The corpus in
docs/ is the memory that matters and it travels with the clone.

Other changes:
* docker/agent -> docker/decoder; the launcher is sylph-decoder. Roles, not
  "the agent", now that there is more than one.
* /reborn is gone -- one repository now, so the port reads HANDOFF from its own
  checkout rather than through a live read-only mount of someone else's tree.
* Canary mounts separately at /canary; it stays a fork tracking upstream.
* A shared `sylpheed-exchange` volume at /exchange, with tools/ on PATH so
  `share` is available in both.
* The decoder's credential file gets the .host-copy treatment the port already
  had -- `credential.helper=store` rewrites by rename-over-target, which is
  EBUSY on a bind mount and reports a fatal that is not one.
* Budget split deliberately: decoder 5 cpu / 6 GB, port 3 / 4, leaving room for
  the planned Referee. "Half the host" was right when there was one agent.

Prompts move to docs/agents/ and are rewritten around the protocol: the oracle
is the running game, dynamic RE stays with the decoder, each iteration must
attempt to refute one claim of the other, and neither may verify its way out of
its own role.
2026-08-29 11:48:30 +02:00
MechaCat02
a8815f2826 agents: the team protocol, the share tool, and a player's-eye navigation doc
Some checks failed
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
**navigation.md rewritten from the player's chair.** It was written from the
inside out -- GamePart ids, pak names, sprite names -- which is how WE find
things, not what the game shows anyone. Now it describes what is on screen, what
you press and what happens, with internals as footnotes. Most rows are open on
purpose: it exists to be filled in by playing, and the in-game tutorials are the
resource for the flight half.

**tools/share** gives transient files provenance without giving them history.
Three kinds of thing were travelling down one channel with opposite needs: code
and decoded knowledge want permanence, cited evidence wants permanence, and
"look at this PNG" wants no history at all. The third kind bloats a repository
forever; passing it by message is worse, because the receiver gets bytes with no
idea which build produced them. `share put` records who, when, what, the sender's
commit, and whether their tree was dirty -- because a capture taken from a
modified tree cannot be reproduced from the sha, and the receiver deserves to
know that before building an argument on it.

**docs/agents/PROTOCOL.md** is the contract. The parts that matter:

Dynamic RE stays with the Decoder -- most of what is open is behavioural and
cannot be answered from the file. What the planned Referee adds is different:
bias enters at what you CHOOSE to capture, so a corpus captured to a fixed
protocol by someone with no hypothesis is worth more than one captured to settle
an argument.

A message may point, ask, prioritise and challenge. It may not change scope,
redefine ground truth, or carry a finding instead of writing it down -- including
a message claiming to relay the human, because a relayed instruction has no
evidence attached and this project has watched a wrong belief travel further and
faster than its correction.

Adversarial duty is explicit: every iteration, try to refute one claim of another
agent and record the attempt either way. Run your own instrument through a
control first. Disagreements go to the human with both positions, not to
whoever is more certain.

And no agent may verify its way out of its own role: the Port has no oracle, the
Decoder builds nothing, the Referee interprets nothing.
2026-08-29 11:42:29 +02:00
MechaCat02
65cefa74c3 monorepo: one repository for the decoders, the port and the corpus
Some checks failed
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
Merges the Godot port into the reverse-engineering repository, preserving both
histories -- 1019 commits of corpus plus the port's 31, brought in by subtree
merge and then moved into place so git can follow each file across the rename.

The reason is not tidiness. The two-repo split forced the exporter to depend on
the decoders by pinned revision, and that created a whole class of failure that
now disappears: a sha reachable only from a topic branch, orphaned by a
squash-merge, breaking a fresh checkout silently at build time. It also forced a
live read-only mount of one agent's working tree into another's container, which
is why a contract file could move mid-iteration. With a path dependency, a
decoder change and the exporter change it requires land in the same commit or
not at all.

Canary stays separate: it is a fork tracking upstream.

New structure for the long term:

  docs/game/     how the game is NAVIGATED -- menus, modals, prompts, alerts,
                 and in-game flight. Written so nobody rediscovers it. Mostly
                 open questions on purpose; the in-game tutorials are the
                 resource for the flight half.
  docs/port/MODDING.md
                 modding as a constraint on the exporter TODAY, not a later
                 feature: one logical asset in one file (the disc splits nearly
                 everything, and resolving that is the exporter's job), names a
                 person recognises, PNG/OGG/OGV/JSON only, base-and-overrides so
                 re-exporting is always safe, provenance in every file.
  data/base + data/mods
                 generated tree and drop-in overrides, both gitignored
  exchange/      transient inter-agent files, deliberately outside history
  docs/agents/   the team protocol

Both the README and the navigation doc lead with the correction that cost the
most: the oracle is the real game under Xenia Canary. Reborn's renderer is a
hypothesis under test, it has been wrong, and treating it as ground truth
propagated into three documents and both agents before a human caught it.

Scripted modding stays possible without being built: no screen name is hardcoded
in GDScript and there is no native code in port/, which is what Godot Mod Loader
needs to be able to substitute behaviour later.
2026-08-29 11:34:46 +02:00
MechaCat02
8c33f86a20 merge the Godot port's history into the monorepo
Brought in with a subtree merge rather than a copy, so the port's 31 commits
survive as history rather than arriving as one anonymous import. Landed under
godot-import/ and moved into the final layout in the next commit, which keeps
git's rename detection able to follow each file across the move.
2026-08-29 11:32:01 +02:00
MechaCat02
ff0937fa8a port: pin the 5.1 downmix matrix explicitly -- human decision
The RE agent escalated this rather than picking, correctly: folding centre
into L/R changes how dialogue sits against the music, which is an aesthetic
judgement about the game and not a container detail.

Decided: explicit ITU fold, centre at -3 dB, recorded in the manifest with the
rest of the transcode command. Pinned rather than defaulted because a default is
a decision nobody made -- invisible in the output and free to change between
ffmpeg versions.
2026-08-29 11:12:38 +02:00
Sylpheed port agent
8d7373b16c video: state the 5.1 downmix instead of inheriting it, and write atomically
A real defect in the P4 output, found by the human on ADV.wmv and widened by the
RE agent to the whole disc: the disc ships 28 movies in 5.1 WMA Pro (every
cutscene, INCLUDING both movies this port needs) and 69 already in stereo. A
bare `-ac 2` therefore does two different things and records neither -- stereo
passes through, and 5.1 is folded by ffmpeg's DEFAULT matrix. How loudly
centre-channel dialogue sits against the music is a content decision, and it was
being made by accident and could move under an ffmpeg upgrade.

Now stated: ITU-R BS.775, LFE dropped, normalised by 1/(1+2*sqrt(1/2)) = 0.4142.
It appears in the recorded command, so the manifest determines the output.

MEASURED rather than chosen by taste, and the measurement is the interesting
part: the explicit matrix and ffmpeg's inherited default differ by a residual of
-91 dB -- about one LSB at 16-bit -- with peak and mean agreeing to 0.1 dB. So
ffmpeg's default IS this matrix, and the audio does not change; what changes is
that the manifest now says which matrix. The UNnormalised textbook form was
measured too and clips at 0.0 dBFS, which is why the scaling is there.

The filter is applied only to 6-channel sources, probed per file with ffprobe,
so a stereo source is never run through a matrix referencing channels it lacks.

Also: encode to a temp name and rename on success. ffprobe read a mid-write
.ogv as 33 s against a 137 s source -- no error, no warning, the exact shape of
catastrophic truncation. The filesystem is shared with the RE agent, so that is
a race, not an edge case, and a half-written file must never be visible under
its final name.

tools/verify-video-audio answers the second of the three questions
docs/AUDIO-VERIFICATION.md separates: does GODOT route the audio. An
AudioEffectRecord on the Master bus writes Godot's own mixed output to a WAV
from a headless run, so "no sound card" was never the obstacle I claimed. It
deliberately checks non-silence and level only -- a difference-signal RMS
against the source is inconclusive without cross-correlation alignment and an
agreed downmix, and would produce a confident wrong number.
2026-08-29 09:04:47 +00:00
Sylpheed port agent
700094bef1 FORMAT v3: rotation, and the focus record -- the ring the port could not reach
Pin bumped to the TAG formats-pin-2026-08-29 (7eeae30), applying the policy the
previous commit wrote. What I wanted from it: `UiBuild` gained a public
`records` map. Without it a leaf was unreachable through the public API --
parse_build sorted T8aD children into `sprites` and `.rat` children into a
PRIVATE map -- so the focus ring, which lives inside ptbtn0Nf.rat, a record the
parent bundle declares NO element for, could not be located at all. My exporter
was writing 19 of build 5's 21 sprites and I could not see why.

v3 carries two new things.

ROTATION. `rotation_deg`, decoded at keyframe +12, and the game DRAWS it --
confirmed twice by the RE agent on different screens with different elements:
the title's ptloop sweeps declare +30/-45 and a GPU capture submits them at
+30.26/-45.28, and the focus ring ramps 0 -> 360 with everything else constant,
caught mid-spin in a capture. Rotation is about the DECLARED PIVOT, measured.
The comparison renderer does not draw it yet, so a rotation disagreement means
sylpheed-cli is behind, not that the port is wrong. Sign is still an assumption.

THE FOCUS RECORD. A focused button is not a sprite swap: ptbtn0Nf.rat declares
the spinning ring AND the bright label, and since the parent declares no element
for the record, the leaf is the only source of placement for both. v2's single
focus_sprite could not carry the ring at all and drew the highlight 7 px
off-centre by inheriting the base position. That -7,-7 is load-bearing: the f
label is 13 px larger per axis and -7 keeps the two concentric.

Checked against the game, not against the other renderer: rendering main_menu
with OPTIONS focused changes the region x 504..703, y 399..448. The RE agent
measured the same difference in the live capture at x 505..703, y 397..446 --
independently, from the other side. Ring, label and underline all land; the only
visible residual is the ring's spin PHASE, which is exactly the one thing
neither of us has resolved (its second keyframe is untimed, and the screen-level
rule for that is not established to apply inside a leaf). Listed in `unresolved`
rather than invented.

verify-screen is unchanged at 16/16 -- rotation has no effect at rest on these
screens, as predicted.
2026-08-29 09:04:47 +00:00
MechaCat02
c0a5725934 port: pin by tag, and how to verify audio with no sound card
**The pin.** Pin a TAG, never a bare sha. A sha reachable only from an auto/*
branch is orphaned when that branch is deleted or -- worse -- squash-merged,
because squash creates new commits: main looks like it contains the work while
the pin becomes unreachable and this project stops building for a fresh
checkout, silently, at their build. formats-pin-2026-08-29 exists for the
current state.

Also says plainly why NOT to float to a branch, which was the tempting fix:
Cargo resolves a git dependency once and writes the sha into Cargo.lock, so
floating gives staleness you cannot see instead of staleness you can read.
push-work now pushes --follow-tags so annotated tags travel with the branch.

**Audio.** docs/AUDIO-VERIFICATION.md separates three questions that were being
asked as one: is the transcode faithful (no engine, no device -- a file-vs-file
difference measurement), does Godot route it (AudioEffectRecord on the Master
bus writes a WAV from a headless run), and what does the GAME play (a PulseAudio
null sink, which needs a rebuild).

It leads with the three ways the fidelity measurement lies, because all three
were hit on the first attempt and each produces a confident wrong number rather
than an error: unaligned subtraction, mismatched channel layouts, and probing a
file another process is still writing.

The 5.1 disc fact deliberately is NOT copied here -- it lives in the RE corpus
at docs/re/structures/movie-audio-channels.md and is linked, so there is one
copy to keep true rather than two that drift. Same reason HANDOFF is a summary
with links.

The downmix itself stays flagged as an unmade decision, not quietly resolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:00:54 +02:00
Sylpheed RE agent
26b5afeaa3 agent: tag the decoder states the port pins, and stop leaking credential config
Two fixes to push-work and the policy that goes with them.

**Tags.** The port's exporter depends on sylpheed-formats BY REVISION, so a
commit of ours is part of its build -- and the commit it pinned lived on one
auto/* branch and nowhere else. Deleting that branch orphans it; squash-merging
it is worse, because squash creates NEW commits, so main appears to contain the
work while the pinned sha becomes unreachable and the port stops building for a
fresh checkout. Silently, at their build, long after the breakage.

push-work now pushes with --follow-tags, which publishes annotated tags
reachable from the pushed commits, and MISSION.md says to tag whatever the port
needs. formats-pin-2026-08-29 at 7eeae30 is the first, created after the fact.

**Credentials.** This script set credential.helper as --local config, which
PERSISTS in the repository. The repo is a bind mount the host also uses, so the
host's git inherited a path that exists only inside the container and every host
push failed with "unable to get credential storage lock". Now applied with -c to
the single push. Same fix already landed on the port side; this copy still had
the bug and would have re-leaked on the next push.

Committed with a pathspec so the agent's in-flight work is untouched.
2026-08-29 10:59:14 +02:00
Sylpheed RE agent
3491d30066 re(media): the disc ships movies in TWO audio profiles, and 28 of them are 5.1
Probed all 97 movies. 28 are wmapro 48 kHz 6-channel 5.1 -- ADV.wmv and every
S*.wmv story cutscene; the other 69 are wmav2 48 kHz stereo, every RT*.wmv and
hokyu_*.wmv. The split is cinematics vs in-mission radio chatter.

Both movies the menu milestone needs, ADV.wmv (the boot/attract intro) and
S00A.wmv (the new-game intro), are in the SURROUND group.

Why it matters: one ffmpeg command over dat/movie/ produces two different kinds
of result and records neither. The 69 stereo files pass through unchanged; the 28
surround files get downmixed 5.1 -> stereo by ffmpeg's DEFAULT matrix, folding
centre-channel dialogue into L/R at a weighting nobody chose and which is not
stable across ffmpeg versions. That is a content decision inherited by accident,
so it should be stated explicitly and recorded beside the command.

Credit where due: found by the human while checking a transcode, verified
independently here and widened from one file to the whole disc.

Two METHOD entries from the same episode, both about measurement rather than
format: don't probe a file another process is still writing (a half-written
transcode reported 33 s against a 137 s source, no error, nearly a filed bug),
and a difference-signal RMS is meaningless before cross-correlation alignment
(-34.2 dB against a -25.3 dB source looks like failure and is inconclusive).
2026-08-29 08:57:56 +00:00
Sylpheed RE agent
7eeae3006a re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
Three things, all from parsing ptbtn0Nf.rat as a build.

1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg
ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in
place, the same shape as the GP_BUNK example already recorded.

2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the
ring's bright head sits in a completely different angular position from the
sprite's own -- caught mid-spin. This is a SECOND independent confirmation that
rotation_deg is drawn, now on a different screen and a different element from the
ptloop sweeps, and it raises rotation's priority: it is not a title-only concern
that sits off-screen at rest, it is the main menu's focus marker.
NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control
refuses that precision -- rotating the sprite by a known 30/90/180/270 and
re-measuring gives errors up to 19.8 deg. What survives the error bar is that a
<=20 deg error cannot manufacture a ~250 deg displacement.

3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said
to use the leaf only for elements the parent does not declare. Right for a BASE
record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at
all (zero of build 5's 16), so the f record's placement comes from its leaf for
BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f
sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs
542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused
region is x 505..703, and the leaf predicts a right edge near 707 where the
parent reading predicts 714.

Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf).
Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild
could not locate a leaf's bytes at all -- which is exactly what blocked the port
from reaching the ring.
2026-08-29 08:46:53 +00:00
Sylpheed port agent
2fbcd59920 docs: retract "reference renderer" -- sylpheed-cli is not the oracle
A framing correction from the human, and it runs through everything I have
written, so it is a retraction rather than a silent edit.

Reborn "was/is just a GUI explorer and extraction CLI for verifying the decoding
of the various files. It may very well be wrong." The oracle is the Xenia Canary
capture and the game.

So verify-screen is a CONSISTENCY check between two decoders that share their
assumptions, plus a regression detector -- not a correctness check, and
agreement in it is not evidence of correctness. Its header now says so, it calls
the CLI the COMPARISON renderer, and DIFFERS means "we moved apart, find out
which of us moved".

The uncomfortable part, recorded because it is the actual failure mode: this
file already contained the sentence "two renderers reading one field through one
decoder agreeing is not evidence that the field is right", written after the
ptframe1 case -- and I then went on quoting 3/255 against sylpheed-cli as though
it meant the port was right. Having the principle written down did not stop me
leaning on the agreement.

Three times both renderers agreed and both were wrong, each caught only by a
capture: pteff05 (menu screens had no background), scale-0 (drawn full size
instead of collapsed), rest() (the menu bracket missing).

Correctness moves to the captures -- nine of them, indexed at
docs/re/captures/ORACLE-CAPTURES.md, covering all five screens in scope. Three
cautions travel with them: not gamma-neutral (there is a floor, don't chase it),
geometry IS sound (a positional disagreement is real), and each is one moment of
a still-animating screen.

verify-screen keeps running over all 16 screens every iteration. It is still
worth having -- total, cheap, and it catches a divergence introduced on the RE
side. It is just not a grade.
2026-08-29 08:45:48 +00:00
Sylpheed port agent
579c8096c1 port: P4 -- the intro video plays inside the boot sequence
The exporter transcodes ADV.wmv and S00A.wmv to Ogg Theora and records the exact
ffmpeg command in the manifest, per MISSION §6, so a modder who dislikes the
quality re-runs one line rather than reverse-engineering what was done.

Quality was MEASURED, not judged: SSIM against the decoded source over a 10 s
sample is 0.9863 / 0.9896 / 0.9924 at -q:v 6 / 8 / 10, and at 200 % zoom on the
reel's hardest case -- fine serif text and soft gradients over near-black, where
Theora breaks first -- q8 is indistinguishable. So MISSION §6's permitted
FFmpeg-GDExtension fallback is NOT needed and is NOT being proposed. No new
runtime dependency.

-ac 2 because the source is 6-channel WMA Pro; that downmix is a decision, so it
lives in the recorded command rather than in prose.

Encoding is cached on a .cmd sidecar holding the command and the source size --
any change to either re-encodes. export/ is still regenerated wholesale; this is
derived state validating derived state, not a hand-edit, and without it every
re-export pays ~4 minutes to produce a byte-identical file.

The player renders INTO the design SubViewport. Parenting it to the Boot node
played the movie to the window instead, and every captured frame came out black
-- which is worth more than a capture-bug note: a movie outside the 1280x720
design space is outside the coordinate system every screen is expressed in.

(A) skips a movie, because Q9 measured that (title at 57 s vs a 193 s baseline).

NOT VERIFIED, and stated as such: audible playback. This container has no audio
device and Godot falls back to the dummy driver. The Vorbis stream exists, is
2-channel and decodes; whether Godot emits it is unconfirmed.
2026-08-29 08:44:25 +00:00
Sylpheed RE agent
d110cf38c7 media: expose se_wave_riff -- the menu's SE cues, assembled where the format lives
The port is forbidden from reimplementing media assembly and Static.slb is
exactly that case: no RIFF, no seek chunk, no XACT container, just a packed run
of whole 2048-byte XMA1 packets, so a wave is defined only by (offset, packet
count) and the header has to be synthesized. That step now happens once, in the
crate that owns the format, instead of in each consumer.

`slb::xma1_wave_riff` wraps raw packets; `media::se_wave_riff` looks the bank up
and reads just the packets asked for. Both reuse the existing synth_xma1_fmt /
build_riff, which are already byte-identical to what tools/re-capture/
slb_extract_wave.py writes -- so this is exposure, not a second implementation.

It reads a TARGETED range rather than the whole bank, and that is load-bearing:
Static.slb is the ONE entry of sound.pak's 9 519 whose declared extent runs past
the end of the extracted segments -- by exactly 616 768 B -- so reading it whole
fails outright on this extraction. Every cue we need is in the first few hundred
KB. Recorded rather than worked around silently.

Verified as an artifact, not a compile: all three cues decode through ffmpeg to
mono 48 kHz PCM at 0.533 / 0.344 / 1.016 s, non-silent (rms 2085 / 2985 / 4327,
peaks 29813 / 16973 / 32767). The refusal path is exercised in the same run --
an impossible packet count is rejected rather than returning a short stream,
because a truncated XMA decodes to plausible-sounding garbage.

Also adds docs/re/captures/ORACLE-CAPTURES.md: an index of the nine canary
framebuffer captures already in this repo, and a plain statement that THEY are
the reference and `screen render` is not.
2026-08-29 08:41:42 +00:00
Sylpheed RE agent
b21c8e4118 re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an
eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new
reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c
declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 --
so ui_layout::parse_build reads it unchanged.

The control is the base record, whose position is known independently: the parent
screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone
returns ptbtn01.t32 at (542,162). It reproduces all five buttons.

Positions are absolute design-space top-left. The ring rests at (500, 156/236/
316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest,
identical in the Japanese bundle. The bright label is a uniform (-7,-7).

Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the
parent's rather than being relative to it, and the two copies are not always
byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what
compose honours, so the leaf is the source only for elements the parent does not
declare, which is exactly the ring. And `screen render --focus` is blind to the
ring for the same reason the port's exporter was: el.focused is name-based on
top-level elements and neither walks into the leaf.
2026-08-29 08:36:53 +00:00
Sylpheed port agent
b632602517 docs: P3 -- the boot runs unattended, and four answers taken as given
Gate: `godot --path port -- --boot --film=/tmp/boot` runs publisher_logo ->
developer_logos (4.65 s) -> title (8.57 s), holding on the title after 13.05 s,
each screen fading in, holding, and fading through black into the next.

verify-screen now covers all 16 screens and passes `--all` to the reference
renderer: the exporter addresses by pak entry index, which is the numbering
`--all` uses, and without it `--build 10` would land on entry 12. The four new
splash bundles come in at max 1-2/255. The three known differences are unchanged.

Also recorded, taken from the RE agent rather than re-derived:

* Focus stays "replace" -- over-vs-instead is unobservable (the focused sprite
  covers the base at 100 % of base-visible pixels; the two compositions differ by
  RMSE 1.1, under the gamma floor). The port guessed right for the wrong reason.
  The real gap is that ptbtn0Nf.rat declares TWO sprites -- a glowing ring, then
  the label -- where ptbtn0N.rat declares one. P5's, and the ring's placement is
  not decoded, so it will be authored from the capture and marked as such.
* RMSE against captures has a FLOOR: capture ~ 255*(render/255)^g, g ~ 1.49 menu
  and EXTRAS, 1.34 title, and it is a ramp the GAME installed, not a capture-path
  artefact to subtract. Narrow reach (fitted on mostly-dark patches), so the port
  will not extrapolate it and will not apply it to rendered output. It is a
  comparison constant, not a rendering one.
* Rotation is escalated to a human and the port has NOT acted. The RE half is
  answered -- about the declared pivot, measured -- and it has zero effect on the
  five screens at rest.
2026-08-29 08:27:41 +00:00
Sylpheed port agent
0a026074d3 port: play the exit ramp, and run the boot sequence unattended
P3. The exit is the group playing ITSELF out, not a black rect over a frozen
screen -- and that was settled by a test that discriminates rather than by
plausibility. Under the black-rect model every region is scaled by the same
1-alpha, so the button/background brightness RATIO would hold constant through
the fade; measured, it falls 6.495 -> 5.574 -> 3.105 -> 2.125 -> 1.935.

Implemented by giving the final untimed keyframe a SYNTHETIC time,
exit_ramp_units after the last timed one, then interpolating it like any other
frame. One code path: arriving and leaving differ only in how far `t` is allowed
to run, not in kind. `holding` is what the sequencer clears to send a screen away.

The sequencer waits on nothing the disc does not carry. A screen holds until its
own group has arrived, then plays out; `dwell` in flow.json is deliberately empty
because each screen's dwell IS its keyframe group (publisher wordmark 3.92 s,
developer logos 3.17 s, both read from the disc). Any extra hold would be a
number nobody measured.

The last screen keeps holding -- nothing is taking the title's place, and a boot
that ends by fading to black looks like a boot that crashed.

flow.json reproduces an OBSERVATION and says so in its header: Q6 closed with a
negative, the order is on the disc nowhere, a transition is a call with a name
argument chosen by code. The intro video's place in the real boot is named as a
gap rather than the order being quietly rewritten to hide it.
2026-08-29 08:27:41 +00:00
Sylpheed port agent
0d2bf1f241 export: reach the splash by authored entry index, and key names by entry
There were TWO splash screens and this port had neither. Entries 11/14 are the
developer logos; entries 10/13 are the SQUARE ENIX publisher wordmark, the first
thing the boot shows, which nothing in this project had noticed.

They have no .rat layout child so `is_build` cannot see them, and the RE agent
established that no CONTENT rule can either: design size fails (every extra
composable bundle sampled is 1280x720, same as every screen) and element count
fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD, the splash
halves are 3 and 7 -- the ranges overlap).

So `screen_builds` is `is_build` plus an authored allow-list of ENTRY INDICES,
each carrying a `why` that says it is a locator and not a claim. An allow-list
rather than a loosened predicate because this is safe in GP_TITLE and would not
be in general: there, widening adds exactly four bundles and all four are real
screens, zero fragments.

screen_names.json is rekeyed from enumeration ordinal to pak entry. Widening
renumbers ordinals, and a name that moves when the enumeration rule changes is
not a name -- the file always called the entry the stronger locator, and it is
now the only stable one. The two unnamed plates therefore renamed build_10/11 ->
build_12/15; they were always locators, and now they locate the right thing.

All 16 export and validate.
2026-08-29 08:27:41 +00:00
Sylpheed RE agent
9ca1eb50fd re(ui): answer four of the port's five asks -- splash, fade-out, focus, gamma
1 SPLASH ADDRESSING (was blocking P3). No content predicate exists: design size
fails (every extra composable bundle sampled is 1280x720, like every screen) and
element count fails (fragments run 2..15, the splash halves are 3 and 7). But
GP_TITLE needs none -- `--all` adds exactly four bundles there and all four are
real screens, with the --all index equal to the pak entry index 1:1. And there
are TWO splash screens: 11/14 are the developer logos, 10/13 are the SQUARE ENIX
publisher wordmark, which the port did not have and which the boot shows first.

2 FADE-OUT (was blocking P3). It is (a), and it is bigger than the fade quad.
Every element ends on exactly ONE untimed keyframe, which rules out (b); that
block is where the screen plays out -- quad to a=255, buttons/labels/glows to
a=0, frames hold. (c) is refuted by a null test that discriminates: a black quad
alone holds the button/background brightness ratio constant, and through the
fade it falls 6.50 -> 1.94, 3.4x monotonic.

3 FOCUS (saves P5 rework). Over-vs-instead is unobservable -- the focused sprite
covers the base at 100.0% of base-visible pixels on three pairs once aligned
(true offset (7,7); the centre alignment reads a misleading 78-84%), and
compositing both ways differs by RMSE 1.1 inside the button rect. The real defect
is the focus record's SECOND element: ptbtn0Nf.rat declares ptbtneff01.t32 (a
42x46 glowing ring, focus only) plus the bright label, where the base declares
one sprite. That ring is the marker the port draws nowhere.

5 GAMMA. The capture is not neutral: capture ~ 255*(render/255)^g, g ~ 1.34-1.49,
and the chain says it is a ramp the GAME installed, not a capture artefact. So
RMSE against captures has a floor. Reach stated: the flat patches are all dark
(render ~0-60), so midtones and highlights are unconstrained.

4 ROTATION is a human's call and is recorded in MISSION, not acted on -- the port
rotating while the reference renderer does not would make verify-screen report a
large diff meaning "the port is right". The RE half is answered: rotation is
about the declared pivot, measured against a GPU capture.

The focus record's +20 element-count word is marked 🟡 not  -- read on GP_TITLE's
ten button records only; the disc-wide check is written and still running.
2026-08-29 08:19:23 +00:00
Sylpheed port agent
3588a0270d docs: record what the port needs from the RE agent, ranked by cost to the port
Five items, written down here rather than only in a message, because a request
that lives in one conversation dies with the container. Two are blocking P3 (how
to recognise the splash bundle, and what the ~0.4 s fade-out actually measures),
one is cheap and prevents P5 rework (focus drawn over vs instead of the base --
the port picked replace without evidence, and the oracle says it picked wrong),
one needs a joint decision rather than a unilateral one (whether the port should
draw the newly-decoded rotation and thereby become deliberately more correct than
the renderer it verifies against), and one calibrates whether RMSE against
captures has a floor at all.
2026-08-29 08:04:38 +00:00
Sylpheed port agent
f6fccee11c deps: bump the pin to f817dd5 -- the menu screens get their background
WHAT I WANTED FROM IT: 56cc7ac, "a RATC child's name is stated, not inferred".
A child was named by scanning backwards for the last printable run of bytes
before its magic. For `pteff05.t32` the three trailing payload bytes are
38 41 58 = `8AX`, which beat the real name, so the child registered under a name
no element declares and resolved to no sprite.

`pteff05.t32` is the full-resolution background of all five menu screens.

Every menu render this port has produced has been missing its background, and
P1 wrote that up as a fact about the disc -- "the bundle carries zero RATC
children for either, so there is no texture on the disc to export". That is
retracted in docs/DECISIONS.md rather than edited away.

The re-export is contained: six new sprites and nothing else. pteff05 on
main_menu/extras and their JP twins, pteff04 on both titles. Each gains a
`sprite` line and moves `layer_source` from "implied" to "sprite" -- the layer
key is now read from the file instead of the decoders' table of keys measured
off the running game, which is the ratchet turning the right way.

pteff05.png is 1280x720; ptbase.png, which had been carrying the background
alone, is 640x360 drawn at 200 %.

Measured against the live capture rather than the other renderer: settled
main_menu RMSE 8.05 % -> 5.92 %. The reference renderer was missing the same
element for the same reason, so no renderer-vs-renderer diff could have found
this -- the third time a capture has caught something both renderers agreed on.
2026-08-29 08:04:06 +00:00
Sylpheed RE agent
9a0ca0d71f docs(method): time the disc mesh suite -- 22 minutes of silence is not a hang
Two runs were killed this session for looking stuck. Measured: 1 318 s serial,
no output while it runs. Also records that `build-reborn test` forces
--workspace and silently ignores a `-p`, so scoping needs the cargo
passthrough (`build-reborn t -p <crate>`).
2026-08-29 07:49:18 +00:00
Sylpheed RE agent
f817dd5939 re(ui): the 60 nameless RATC children are frames, not children -- .tan decoded
Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC
children carry no `opt ` block, and it was not established whether they lack one
or sit past our 128-byte window.

Neither. They are not children. `examples/ratc_optless_children.rs` re-runs
`ratc::parse`'s own guards over the disc and reports which one fired: all 60 are
"tag beyond the window", none is rejected by length, gap or charset, none is
child #0, and all 60 live in six bundles of one archive. Within a bundle the
distances back to the nearest tag are an exact arithmetic progression, step
60 600 -- ten different records finding the SAME tag, because there is only one.

Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a
single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block
declares the resource; its payload is a run of T8aD frames.

Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names
exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367,
`.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire
population with nothing left over. The negative is closed, not narrowed.

Consequence recorded but deliberately not fixed: `ratc::parse` over-reports
there, listing a `.tan`'s frames as anonymous children. Nothing in the menu
milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled
out -- so no screen the port draws changes.

Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc`
under default test parallelism (SIGKILL, no assertion; 8/8 pass with
--test-threads=1).
2026-08-29 07:39:09 +00:00
Sylpheed RE agent
56cc7acfc3 re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background
`ratc::parse` named each child by scanning backwards for the last printable run
of bytes before its magic. The format states the name explicitly instead, in an
`opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same
block `ui_layout::opt_link` already read for a button's focus link.

The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every
one the same failure: the 3 trailing payload bytes are themselves printable and
beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the
full-resolution background of all five menu screens registered under a name no
element declares, resolved to no sprite, and `compose` dropped it through an
early `continue` that -- unlike the two arms above it -- records nothing. The
screen lost its background and `screen render` still reported "all resolved".

`8AX` was never a name. Docs that treated it as one are corrected here.

Disc-wide, and the control is the 17 918 the scan already got right: the `opt `
reading reproduces every one of them. Effect on the five screens is the
signature of the same art at twice the resolution -- mean brightness unmoved,
high-frequency detail x1.15..x1.30 -- which is what the separately-measured
`ui-8ax-fullres-background` result said the game draws.

Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on
the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC
sibling references now resolve 10 148 of 10 148.

Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning
the `8AX` case byte for byte and the no-block fallback), and every disc-gated
integration suite in sylpheed-formats/sylpheed-cli.
2026-08-29 07:26:56 +00:00
Sylpheed port agent
4a5fa7f1cb merge main: container and push-work fixes
Bringing the infrastructure fixes from main onto this branch so the two do not
drift. main touched docker/{entrypoint.sh,sylph-port,bin/push-work} and
docs/loop-task.md; this branch's only docker/ change is bin/build-reference-cli,
so there is no overlap and nothing to reconcile by hand.
2026-08-29 06:14:43 +00:00
Sylpheed RE agent
49f109deb1 agent: the loop must never stop itself
A run ended with a clean exit 0 while the display title read "Loop interval
optimization", leaving four files uncommitted in the tree. Nothing crashed --
the loop was ended, and ending the loop ends the run: the container exits and
there is no next iteration.

The prompt said the agent did not NEED to arm a wakeup. It never said not to
stop one, and an agent that reads about a pacing control will reasonably try to
use it. Now explicit: do not call ScheduleWakeup at all, and if the cadence is
wrong, say so and leave it to a human -- the interval is set outside the prompt.

Recorded with the date and the symptom, because "the container exited cleanly"
looks like a finished job rather than a self-inflicted stop.
2026-08-29 08:12:42 +02:00
Sylpheed RE agent
7a4e4333f4 docs: withdraw yesterday's "paint order is a sequence" -- wrong source
Last iteration I claimed the splash's measured_paint_order [0,2,4,6,1,3,5]
records, between its glow and logo halves, the temporal order they were
seen in rather than depth -- because the halves never share a frame.

The no-overlap measurement is right (glows f94-115, logos f116-211). The
inference is wrong, on two independent grounds:

  * Wrong source. That vector is not a read of the draw capture. It is a
    read of the live screen object's CHILD ARRAY -- ui-screen-runtime.md
    records it literally as "paint order (child slots)". A child list has
    a definite order whether or not its children are ever drawn together,
    so co-occurrence does not bear on it. The capture was the CHECK.
  * The order is in the file anyway. paint_order_audit on GP_TITLE entry
    11: derived == measured, 0 inverted pairs, 0 same-layer-key ties. The
    glows and logos carry distinct T8aD keys (0xa100 < 0xa110), so the
    file orders the halves statically, no capture involved.

I asked the question that started this iteration -- do the title and menu
orders have the same problem -- and the answer is that none of the three
does, for the same reason.

What survives is narrower and now recorded with numbers: how much of each
order its capture actually cross-checks. The title capture is stable (8
draws / 12 quads / 5 textures, identical in all five captured frames
across two logs) and confirms 7 of 24 positions; the menu capture is not
(texture 0x11C30000 present in frame 0, gone by frame 3); the splash
capture cannot cross-check its middle at all.

A counting trap worth the tool: count QUADS, not draws. The menu's draw 9
is indices=24 -- six quads batched from one texture. Counting draws reads
9 where 16 are on screen, and an earlier pass of this analysis briefly
"found" three quads for six declarations that way and concluded elements
were missing. They were batched.

METHOD: check what a "measured" value was measured FROM before reasoning
about its limits. The co-occurrence rule is real, and it is specific to
orders read from draw captures.
2026-08-29 05:19:44 +00:00
Sylpheed RE agent
2532c056be docs: the splash .prm measured -- and its paint order is a sequence, not depth
ui-prm-primitives recorded that where a .prm paints on a screen without a
measured order is unsolved. For the developer splash it is now measured.

Every frame opens with two full-screen draws before any sprite. The
second is untextured in all 212 frames with a constant vertex colour of
FF000000 -- opaque black -- matching palogo_eff0.prm's declaration
exactly: kind 0x10, pivot (640,360) -> 1280x720, one keyframe, a = 255.
So the splash backdrop is an opaque black full-screen quad from the
bundle itself, painted behind every sprite, which is why a splash render
needs --black rather than the default backdrop.

Not a general rule, and said so: the measured main-menu order puts
pteff02.prm at position 4 and pteff00.prm LAST, the latter being the
screen-transition fade.

And a correction to an existing row. measured_paint_order returns
[0, 2, 4, 6, 1, 3, 5] for the splash, described as "the .prm, then all
three glows, then the three logos". But the glows and the logos never
appear in the same frame -- 0 overlapping frames in 235 -- and two
elements that never co-occur have no observable relative depth. Between
those halves the vector records the order they were SEEN IN, not a
front-to-back relationship.

That does not make the render wrong, and element 0 is a real depth
observation since the .prm co-occurs with everything. But the type of the
claim matters: reading the vector as depth invites compositing all seven
elements at once, which is exactly what does not reproduce the screen.

METHOD: two things that never co-occur have no observable relative order;
when recording an order, note which pairs actually appeared together.
2026-08-29 05:06:37 +00:00
Sylpheed RE agent
7988f52ce7 re(ui): deliver the measured splash sequence the port has to author
The activation decision is code, and MISSION already says the port
authors the sequence -- so the useful move is to hand over the sequence
measured rather than chase the code. From the 235-frame draw capture, at
1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s, both settled in Q1):

  publisher: SQUARE ENIX logo    f1-90     90 frames   3.00 s+   at 0.00
  (gap, nothing drawn)           f91-93     3 frames   0.10 s    at 3.00
  developer: both glows          f94-115   22 frames   0.73 s    at 3.10
  developer: both logos          f116-211  96 frames   3.20 s    at 3.83

Three limits, stated with the numbers rather than after them. The capture
opens with palogo_sqex already at a=255, so the publisher phase began
before the window and 3.00 s is a FLOOR -- every "starts at" is relative
to the capture, not to boot. palogo_anima and palogo_anima_eff get 0
draws in all 214 frames, so a third pair's phase is not in this
measurement. And it is one capture, one run: the glow->logo switch being
a single frame boundary with no overlap is a strong shape, but each
duration is one sample.

What is solid is the part that matters: the 0.73 s and 3.20 s phases are
each within 2% of their element's declared span, so the durations are the
bundle's own and only the ordering is authored. That is the difference
between a port transcribing timing and inventing it.
2026-08-29 05:00:54 +00:00
Sylpheed RE agent
7156591654 re(ui): re-establish selective activation by killing the alternative statically
Last iteration I withdrew "a bundle is a palette" because the evidence
did not choose between selective activation within one bundle and two
compositions shown in sequence. The alternative can be killed from the
disc, which I had not tried.

Hypothesis 2 needs a bundle declaring the GLOWS WITHOUT THE LOGOS. There
is none. Every GP_TITLE entry carrying palogo elements:

  10, 13 (publisher twins)  palogo_eff0.prm, palogo_sqex, palogo_sqex_eff
  11, 14 (developer twins)  palogo_eff0.prm + all three logos + all three glows

Four entries, and each developer entry declares the complete set of six.
So whichever bundle was active across frames 94-211 -- entry 11, entry
14, or both in turn -- it declared the logos and the glows, while the
game drew two sprites at a time in disjoint phases.

Therefore only a subset of a bundle's elements is drawn at any moment,
whatever the bundle-loading story is. The conclusion no longer depends on
resolving how many bundles are involved, which is why the texture-base
test's failure stopped mattering.

So the claim is reinstated -- this time by eliminating the alternative
rather than by assuming it away. What worked was not a better capture but
asking what the competing hypothesis would REQUIRE on the disc and
finding it absent.

METHOD: a hypothesis that predicts an artefact can be killed by looking
for the artefact, which is often far cheaper than measuring the behaviour.
2026-08-29 04:56:13 +00:00
Sylpheed RE agent
2a086051b3 re(ui): withdraw the mechanism -- "palette" was an explanation, not a finding
Last iteration I wrote that a bundle is a palette whose elements are
selectively activated. The disjoint glow/logo phases have two
explanations and I asserted one:

  1. one bundle, some elements run then others;
  2. two bundle-loads shown in sequence (entries 11 and 14 are twins
     declaring identical sprites).

The draw log's tex[base=...] looked like it would separate them. It does
not, and the control is in the same table:

  publisher splash  f1-90     0x11C30000, 0x10000000
  glows             f94-115   0x11C30000, 0x10000000
  logos             f116-211  0x11C30000, 0x10000000

The publisher splash is certainly a DIFFERENT bundle from the developer
splash, and it uses the same base. So 0x11C30000 is a reused upload slot,
not a bundle identity, and the test cannot choose between the two
hypotheses.

Survives: a bundle's declared elements are not what gets drawn.
palogo_anima and palogo_gamearts carry byte-identical keyframe times and
in the same run one is drawn 95 frames and the other none -- and
whichever twin was active declares both. The phases are strictly disjoint
(0 overlapping frames in 235).

Withdrawn: the mechanism. The practical consequence is unchanged --
compositing every element of a bundle does not reproduce what the game
shows over time -- but the why is not established and I stated it as
though it were.

What would separate them: a per-draw capture recording the bundle each
draw came from, or a file-IO log showing whether a second RATC entry is
read between frames 115 and 116.

METHOD: a shared resource address does not identify the resource's owner;
and state the mechanism as a separate claim from the observation, or the
weaker one inherits the stronger one's evidence.
2026-08-29 04:51:34 +00:00
Sylpheed RE agent
baed44a9ea re(ui): the sequencing survives refutation -- and a bundle is a palette
Two checks on last iteration's "sequential, not simultaneous" reading.

First, the phases really are disjoint. If glows and logos ever shared a
frame the claim would be wrong. Across all 235 captured frames the count
of frames containing both is ZERO, and the switch is a single clean
boundary -- f110-115 draw 1280x720 + 262x108 + 525x90, f116 onward
1280x720 + 243x86 + 499x72. Two sprites either side, no transition frame.

Second, and larger: a third of the bundle is never drawn. Entry 11
declares three logo/glow pairs and only two appear.

  palogo_gamearts / _eff    95 / 22 frames
  palogo_seta     / _eff    95 / 22 frames
  palogo_anima    / _eff    never

palogo_anima declares the SAME keyframe times as palogo_gamearts. Two
elements with byte-identical data, 95 frames and 0 frames in one run.
Reach: the capture covers frames 1-214, so this is "never in the window".

So a bundle is a palette, not a script. Its elements say what to draw and
for how long; which of them run, and when each starts, is decided outside
the placement data. That is the same conclusion the boot-order work
reached from the other end -- the driver is code, not data -- now with a
per-element measurement behind it.

For the port, concretely: compositing every element of a bundle does not
reproduce what the game shows over time. It is the right thing for a
static screen that settles, and it is not a timeline.

METHOD: two elements with identical data and different outcomes is the
strongest possible evidence that the decision is elsewhere.
2026-08-29 04:47:05 +00:00
Sylpheed RE agent
7fb6bdfad8 re(ui): a group's duration is in the data, its start time is not
Tested whether the splash timeline, played, reproduces the capture --
the last gap in the animation model. Half of it does.

Durations match. At 2 units/frame under the shifted reading, from the
235-frame draw capture of the developer splash:

  glows  drawn f94-115  (22 frames = 44 units)  declared ~0..45  = 45   97.8%
  logos  drawn f116-211 (96 frames = 192 units) declared 15..210 = 195  98.5%

Each element is on screen for its declared span to within 2%.

Starts do not. Every glow declares the same times 15,30,45 and every
logo the same 15,30,190,194,206,210, so on one clock they would overlap
almost entirely -- and they do not overlap at all. The glows run 94-115
and the logos 116-211, strictly sequential, the logos starting the frame
after the glows end. Fitting one origin needs f0 ~ 93.5 for gamearts_eff
and ~103 for gamearts, about 19 units apart, and aligning one throws the
other off by ~9 frames at both ends.

The obvious candidate is refuted. parse_placements reads each group
header as (element index, keyframe count) plus one undecoded LEAD-IN
WORD -- exactly where a per-group start offset would live. It is
0x00000000 for all seven elements, glows and logos alike.

Reach: not the keyframe times (identical within each family), not that
word (zero), not declaration order (which interleaves logos and glows
where the observed sequencing groups them), not the RATC child order.
What remains is that the sequencing is code-driven, which agrees with
what the boot-order work concluded independently.

For the port: a group says how long an element animates and what it does,
not when it starts relative to its neighbours. The observed order on the
developer splash -- both glows, then both logos -- is measured for one
screen, not a decoded rule, so the sequencing has to be authored.

METHOD: when a model reproduces durations but not positions, the missing
piece is an origin, not a rate.
2026-08-29 04:42:55 +00:00
Sylpheed RE agent
1f2b469f7e re(ui): a static composite is only meaningful for a screen that settles
The model's sharpest prediction, tested with its control. The draw log
says that on the developer splash the _eff glows are drawn on frames
94-115 and the logos on 116-211, so at the moment the reference capture
was taken EVERY glow is already finished -- including the two that have
plateaus and which rest_plateau therefore renders visible. Suppressing
them should help on the splashes and hurt where a screen genuinely
settles.

  publisher splash   +0.9604 -> +0.9982   +0.0377
  developer splash   +0.9659 -> +0.9980   +0.0321
  title    (control) +0.9500 -> +0.9480   -0.0020
  main menu(control) +0.9460 -> +0.8544   -0.0916
  EXTRAS   (control) +0.9440 -> +0.8370   -0.1070

Both splashes jump to about 0.998; all three persistent screens get
worse. The control is what makes this a finding rather than a
coincidence: the same edit helps exactly where the model says it should
and hurts exactly where it says it should not.

So rest_plateau is not over-drawing in general -- it over-draws on
TRANSIENT screens. A plateau mid-animation means the element is held at
that point in the timeline, not that it is on screen once the screen has
settled. Where a screen settles, the held pose IS the settled pose and
the rule is measurably right.

And that answers the question left open several iterations ago -- what
"rest" means for a transient element. It does not mean anything: the
splashes never rest. A static composite of them can match a chosen frame,
and about 0.998 is what these captures' frame is worth, but the format
does not answer a question the screen never poses.

For the port: play the timeline for the two splashes, which the settled
keyframe timing now supports, and composite statically for title, main
menu and EXTRAS.

METHOD: an edit that improves one set of cases is only interesting once
you have shown it damages the cases where it should.
2026-08-29 04:37:59 +00:00
Sylpheed RE agent
458585d136 re(ui): the structural case for last -- 2 293 of 2 305, checked disc-wide
The weakness in the rest-rule finding was that `last` had been SCORED on
only two elements. It cannot be scored on more -- only two ambiguous
elements sit on a screen with a live capture -- but the entry -> hold ->
exit model makes a prediction that can be checked on all 2 305: what does
each element's FINAL keyframe look like?

  final keyframe invisible (a = 0)          1 618   transient: gone at rest
  final keyframe visible, at max alpha        675   faded in and stopped
  final keyframe visible, BELOW max alpha      12   genuinely unclear

Of the 687 that end visible, 472 have monotonically non-decreasing alpha
-- a plain fade-in that stops, [0, 255] over two keyframes in the
commonest case (pjex_eff.rat, pghud_speed_cut.t32) -- and another 203 end
at their maximum after dipping.

So `last` is structurally defensible for 2 293 of 2 305 (99.5 %), against
a dwell rule that returns a mid-movement frame by construction. Observed
correct for 2, structural for 675, model-consistent for 1 618, unclear
for 12.

The assumption carrying the 1 618 is stated rather than buried: that a
plateau-less element's animation has finished by the time the screen is
settled. The draw log establishes exactly this for the two splash glows
(drawn frames 94-115, logos 116-211) and establishes nothing for the
rest.

Default still unchanged. The case is now observational, structural and
model-based rather than two data points, but it would move 1 896 elements
and the decision belongs with whoever owns the renderer.
2026-08-29 04:34:21 +00:00
Sylpheed RE agent
4012d5b555 re(ui): why rest_plateau is right -- and last is right only for a transient
The shifted keyframe-time reading looked like it implied something
simple: the final pose is reached at a definite time and nothing follows,
so rest should just be the last keyframe and the plateau heuristic could
go. Tested by applying it to EVERY element:

  title              +0.9500 -> +0.6819   -0.2681
  main menu          +0.9460 -> +0.6416   -0.3044
  EXTRAS             +0.9440 -> +0.5745   -0.3695
  publisher splash   +0.9600 -> blank (zero variance, corr undefined)
  developer splash   +0.9643 -> blank

Refuted, and the failure supplies the model. A group is entry -> hold ->
exit, and the exit is the screen's DISMISSAL. While a screen is displayed
it has not reached its last keyframe; it is sitting at the hold. So
rest_plateau is the correct primary rule, and the last keyframe is the
post-exit state -- correct only once the screen is gone, which is why
applying it everywhere blanks the splashes.

This does not contradict the shifted reading. That reading says when each
pose is reached; it says nothing about the group being played to
completion while the screen is still up. The step between them was mine.

And it explains why last wins for the two plateau-less elements: an
element with no hold is a transient, it flashes and is over, and at any
settled moment it is gone -- which is its last keyframe. The draw capture
says the same independently: on the developer splash the _eff glows draw
on frames 94-115 and the logos on 116-211, so the glows are already
finished when the logos are up.

Three independent observables -- animation timing, static composites, and
the per-frame draw log -- now agree on one rule: plateau where there is
one, last keyframe where there is not.

METHOD: a blank render is a NaN correlation, not a low score, and that
NaN was the strongest form of the result; and when a model predicts
something the measurement refuses, suspect the step you supplied between
them.
2026-08-29 04:28:19 +00:00
Sylpheed RE agent
515456c59a re(ui): quantify what changing the rest rule would do disc-wide
The open question was whether "last keyframe" holds beyond the two
elements I could score against a capture. It cannot be scored disc-wide --
only two ambiguous elements sit on a screen with a live capture -- but the
blast radius can be measured, and it argues the same way.

  genuinely ambiguous elements                       2 305
    the two rules AGREE on                             409  (17.7 %)
    they DIFFER on                                   1 896  (82.3 %)

  dwell (current): invisible pose 1 711 (74.2 %), zero-scale 195 (8.5 %)
  last           : invisible pose 1 618 (70.2 %), zero-scale  43 (1.9 %)

Two things follow. It is not a marginal choice: the rules disagree on 82%
of the affected elements, so "either is fine" is not available. And the
current rule produces 4.5x more degenerate poses -- a zero-scale pose is
collapsed to nothing, i.e. an element's PRE-ROLL before it has grown in,
which is definitionally not a rest. 195 elements currently rest at a frame
they are only passing through, against 43 under last.

That is an argument from the data's own structure rather than from the two
captures, and it points the same direction.

Kept honest: it is indirect. Fewer degenerate results is not the same as
more correct results, and last still returns an invisible pose 70% of the
time -- right for a transient element, wrong for a persistent one. The
default stays put; the numbers are in HANDOFF for whoever decides.
2026-08-29 04:24:40 +00:00
Sylpheed RE agent
93e9b185ea re(ui): the rest fallback fires on 2 elements, and "last keyframe" wins there
Scored candidate rest-pose rules by rendering and correlating instead of
arguing, and both results correct something I had published.

First, the exposure. The guessing fallback is reached only by an element
that is plateau-less AND multi-keyframe -- a single-keyframe element
short-circuits at `match len { 1 => first }`. Per screen:

  title (4)               24 elements   2 plateau-less   0 reach the fallback
  main menu (5)           16            5                0
  EXTRAS (6)              18            5                0
  publisher splash (10)    3            2                1
  developer splash (11)    7            2                1

So on the three screens the port cares most about, rest() never guesses.
That is why three different rules render builds 4/5/6 to identical
correlations -- the code is unreachable there, which I nearly read as
"the choice does not matter".

Second, where it does fire, the last keyframe is markedly better:

  publisher splash   dwell +0.9600   last +0.9982   maxalpha +0.9600
  developer splash   dwell +0.9643   last +0.9758   maxalpha +0.9643

That refutes my own earlier refutation. I had killed the last-keyframe
rule by arguing it makes palogo_anima_eff invisible while its two
siblings stay lit, which looked like an artefact. The capture says
otherwise: making it invisible is what improves the match. The sibling
symmetry was my expectation, not evidence.

Caveat kept in front: both captures are single frames of a transient
animation, so this fixes which pose matches THOSE frames, not which is
canonically at rest. Default unchanged -- better on both screens where it
fires and identical on the other three, but it would move 2 305 elements
disc-wide on two measurements. Reachable via SYLPHEED_REST_RULE=last.

Also confirmed: all 195 zero-scale rest poses are inside the corrected
2 305 ambiguous population; none is a single-keyframe element.

METHOD: score a rule where it can differ, or you measure nothing; and an
argument from symmetry is a prediction, not a refutation.
2026-08-29 04:20:27 +00:00
Sylpheed RE agent
0265da31a1 re(ui): refute my own fix for rest(), and correct the defect rate by 65%
Two corrections from one experiment.

A keyframe group is entry -> hold -> exit, and the exit ends invisible:
on the five port screens the final keyframe is invisible for 21/24
(title), 8/16 (main menu), 12/18 (EXTRAS), 2/3 and 6/7 (splashes). So the
screen as seen is the HOLD, which is why rest_plateau is the right
primary rule and why "rest = last keyframe" would empty every screen.

That suggested a fix: an element with no hold has no representative pose,
so draw nothing rather than guess an endpoint. Tested through compose's
visible mask and correlated against the live captures:

  title       +0.9500 -> +0.6839   -0.2661
  main menu   +0.9460 -> +0.9037   -0.0423
  EXTRAS      +0.9440 -> +0.9094   -0.0346

Refuted on all three, and the reason invalidates a number I published. An
element with a SINGLE keyframe has no adjacent pair, so the plateau test
marks it plateau-less -- but its one pose is unambiguously its rest.
Suppressing those removes backgrounds and full-screen layers, which is
the title's -0.27.

  no plateau (as published)      3 807  (24.57 %)
    ... single-keyframe          1 502  trivially at rest, not a guess
    genuinely ambiguous          2 305  (14.88 %)

So rest() guesses for 2 305 elements, not 3 807 -- the figure I gave the
port overstated the defect by 65%. Corrected in HANDOFF and the page.

METHOD: a predicate over adjacent PAIRS silently misclassifies a
one-element list; and acting on a claim is a better test of it than
re-reading it -- this flaw survived a census, a write-up and a handoff
row, and died the moment the rule was used to change a rendering.
2026-08-29 04:15:58 +00:00
Sylpheed RE agent
60285a6ead re(ui): replicate the keyframe-time shift -- three elements, two screens
The case for reading +36 as "the time the NEXT pose is reached" rested on
one element's fade-out shape, then on one element's hold duration. Both
splash halves supply more, and they agree.

  element            screen              observed hold  as decoded  shifted
  palogo_gamearts    developer splash        83 f          8 f       80 f
  palogo_seta        developer splash        83 f          6 f       80 f
  palogo_sqex        publisher splash       >=77 f *       6 f      102 f

  * the capture opens mid-hold at frame 1, so 77 is a floor.

The readings predict opposite structures. For palogo_gamearts, as decoded:
hold 8f, in 80f, hold 2f, out 6f, out 2f -- an eighty-frame FADE-IN and a
two-frame hold. Shifted: in 8f, hold 80f, out 2f, out 6f, out 2f. The
capture shows an 83-frame hold and no fade-in at all.

The elements that cannot discriminate are not contradicted:
palogo_gamearts_eff observed in 7f / hold 7f / out 8f, and both readings
give 8f phases -- with four blocks the shift only relabels which phase is
which. So the glows, which is where Q1's linear law was measured, say
nothing either way rather than arguing against.

The decoder's default is still unchanged, and the reason is now
articulated rather than assumed. The single thing opposing the shift is
rest() on ptlogo_eff3, where the shifted reading makes the longest-dwell
fallback return the bloom's 200% peak. That fallback is unsound whenever
it runs -- it returns an endpoint of a movement, neither of which is held
-- and checked: the shift does not fix it either. So the objection was
never evidence about the times. Timing had three discriminating
measurements; pose selection had a heuristic guessing.

For the port: animation timing should use the shift; static composites are
unaffected and the five screens' correlations stand. Classified measured,
not decoded -- three elements in one screen family, not a disc-wide check.
2026-08-29 04:09:36 +00:00
Sylpheed RE agent
fb6376a548 docs: one page saying how good the five screens actually are
The answers for the port's five screens were spread across a dozen
documents and none of them said how good the result IS. Measured:

  screen              build  drawn   corr vs capture  alignment
  title                 4    15/24      +0.9500       dy=0 dx=0
  main menu             5    11/16      +0.9460       dy=0 dx=0
  EXTRAS                6    13/18      +0.9440       dy=0 dx=0
  publisher splash     10     2/3       +0.9600       dy=0 dx=0
  developer splash     11     6/7       +0.9643       dy=0 dx=0

Every one aligns at exactly zero offset over a +/-2 px search in both
axes, so placement and scale are right and the residual is tone and
detail rather than geometry.

The drawn/total counts are not slack. Each undrawn element has a reason
already documented: kind & 0x4 ghost instances (4 on the title), .prm
primitives off by default (2 per menu, 1 per splash), loop* animations
off by default and off-screen at rest (2 per menu screen), and the 8AX
name mismatch (1 per menu screen) whose art reaches the screen anyway via
ptbase. 4+2+2+1 = 9, 2+2+1 = 5, 1. Nothing unexplained.

The residual is ranked for a consumer: tone first (gamma 1.34-1.49, the
game's own display ramp), then 8AX resolution, then one drawable
paint-order tie on EXTRAS alone, then rotation-decoded-but-not-rendered
which does not affect these five at rest.

Reach stated: these are static composites at the resting pose against
single frames, so nothing here speaks to animation, and a whole-frame
correlation is a sanity figure rather than a per-element check.
2026-08-29 04:04:15 +00:00
Sylpheed RE agent
0efd692b4c docs: the suite is heavy, not hung -- correcting my own "cannot terminate"
Last iteration I wrote that build-reborn test cannot finish in a working
session, from having watched it run 3h26m. That was the stronger claim
and I made it without measuring the work.

Timing `mesh info` on each of the 166 .xpr containers with a 25 s cap:

  files scanned                166
  exceeding 25 s                19   Hangar, 17 Stage_*, ptc_pack
  Stage_S02 to completion      144 s, rc = 0

Nothing hangs. Nineteen heavy containers at roughly two minutes each is
about 45-60 minutes for one pass, before the 147 fast ones. The 3h26m
observed was that hour of work running at a load average of 9-14 --
inflated by the two duplicate runs I had left going, which did not merely
coexist with the slowness but multiplied it.

The practical conclusion is unchanged and only the wording softens: an
hour-scale suite is not an iteration-scale gate, and every "green" I
reported from it this session was partial. But an hour-scale gate can be
run deliberately, whereas a hung one cannot be run at all, so the
distinction is worth having right.

File list committed as reference data so the cost is attributable without
re-scanning.

METHOD: a slow thing observed under contention looks like a stuck thing;
measure the work before choosing between "cannot finish" and "takes an
hour".
2026-08-29 04:02:34 +00:00
Sylpheed RE agent
145046f88f docs: the verification gate cannot terminate, and I left two runs going 4h
Two findings from checking whether last iteration's partial green had
finished. It had not, and the reason matters for anyone using the gate.

build-reborn test contains twin_pairs_do_not_share_a_buffer, which
decodes every .xpr in hidden/resource3d -- 166 files, 1.4 GB -- through
the full Xbg7Model anchoring path, and is NOT #[ignore]d. Its sibling in
the same file walks the same 166 files and IS ignored as known-failing,
which makes the binary's cost easy to underestimate. Measured: one
instance accumulated 3h26m of CPU at 89% without finishing.

So every "green run" reported in this corpus from a workspace or
sylpheed-formats test is necessarily PARTIAL unless it says the suite
terminated -- including the ones I reported this session. The honest form
is the suite count and elapsed state, not the word "green". Not proposing
to #[ignore] or subsample it: that changes what the suite asserts and is
the project's call, not an audit side-effect.

And the mess is mine. Two cargo test -p sylpheed-formats runs launched
detached in earlier iterations never exited, because they were sitting in
that test:

  pid 103375  4h12m elapsed  child mesh_consistency_disc  3h26m CPU  89.3%
  pid  99965  4h39m elapsed  child pak_idxd_disc          1h16m CPU  93.8%

Load average 14.18 on 12 cores. Killed, after checking the legitimately
running workspace suite and leaving it alone; load fell to 9.68.

What this does NOT explain, because it is tempting: the session's
emulator troubles. screenshot cost 0.49 s with both runaways live and the
emulator stopped, against 10.8 s measured earlier with the emulator
running. The 92x figure really was emulator contention; the runaways were
a background tax on top. The black surface and the unreachable title
stand as measured, with their own controls.

METHOD: a detached job you never check can outlive many iterations --
setsid was added so a timeout could not kill them, which also means
nothing does; and know whether your verification gate can terminate.
2026-08-29 03:45:14 +00:00
Sylpheed RE agent
6d9214fd07 docs: check that the docs' headline figures match their committed data
Nothing had ever verified that a number written in prose matches the
reference data file committed beside it. The figure is written once from
a run; the prose is edited around it afterwards and the data file is
regenerated independently, so drift is silent.

All 19 headline figures across four censuses -- the eff-bit census, the
plateau census, the top-level rotation census and the eff-bit alpha test
-- currently agree with their data files.

The checker had to be numeric, and the first attempt is the reason it is
a script rather than a grep: comparing strings reported almost every
figure as a mismatch, because the data files write 14709 where the docs
write "14 709" with a thin space, and the docs round 33.66 to 33.7. A
consistency check that fails on formatting trains you to ignore it, so
the tolerance is explicit: exact against the data, within 0.05 against
the doc to allow rounding.

Also ran the full disc-gated workspace suite (build-reborn test, which
wires SYLPHEED_DISC -- without it the disc tests self-skip and green
means almost nothing), covering this session's three decoder changes:
rotation_deg on Keyframe, the scale-0 fix in blit/fill_quad, and the
flags field on T8adImage. 122 passed / 0 failed across the four suites
that had completed; the long disc-gated integration tests
(records_roundtrip_disc, first_header_word_is_record0_hash) were still
running and are not counted here.
2026-08-29 03:42:13 +00:00
Sylpheed RE agent
714a26769d re(ui): premultiplied alpha refuted for bit 0x02; parking the field
A per-sprite premultiplied-vs-straight-alpha flag would matter a lot to a
port and has a sharp static signature: premultiplied means RGB <= A
everywhere. Over the 170 decoded GP_TITLE textures that pair to a flag
word:

  bit SET    n= 61   mean %(RGB>A) 55.52   median 52.52
  bit clear  n=109   mean %(RGB>A) 33.66   median 30.17

Premultiplied requires ~0% for the flagged group. Both groups are far
from it and the flagged group violates MORE -- the opposite of the
hypothesis. Refuted.

What remains is a weak association: flagged sprites carry more
bright-RGB/low-alpha pixels, which is what glow art looks like. But the
best single threshold classifies 76.5% against a 64.1% base rate -- a
12-point lift with badly overlapping distributions. A tendency, not a
rule, and reported with its base rate so it cannot read as more.

Noted for whoever returns: "0x02 selects an additive blend" was refuted
by blending those sprites additively and finding every measure worse
against the capture -- but that ran through a title render since fixed
twice (rest_plateau, and the 8AX background the composer drops). The
refutation may well stand; it was measured through a renderer with known
other errors, so it is worth one re-run if blit ever gains additive
blending.

Parking the field. Four candidate meanings are dead -- additive blend,
eff name in both directions, transient element, premultiplied alpha --
none produced a positive account, and the bit blocks nothing: the port's
screens composite at 0.947 correlation against a capture without it. The
negative space and the sound attribution method (child order, not size)
are written down so a later attempt starts here.

METHOD: report a classifier's lift over its base rate; and park a field
after N failed hypotheses, saying what was eliminated.
2026-08-29 03:23:40 +00:00