e07c1de1b59209d00a83112b80651091a067cdc8
1072 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e07c1de1b5 |
re: I cannot measure this emulator's clock -- and that answers the port's 2%
The port put two of my pages against each other: settle->plate 2.135 s and one
focus-ring revolution 2.177 s, both a declared 120 units during a static hold,
2% apart against a 6 ms run-to-run agreement. Fair challenge.
The resolution is that the question assumes a stable wall clock. Same interval,
same container, same day: 2.138, 2.132, and 2.549 s -- a 19% swing, caused by
adding --log_ui_draws=true. The 2% is a fifth of that. The two pages were never
in conflict about the game; they are three readings of one declared quantity
through a clock that moves. What settles the quantity is the disc.
Wall clock cannot separate the hypotheses, so I tried to measure frames instead.
Both instruments are recorded as failures rather than published as numbers:
* Canary's own [UI-CAP] counter -- the one that produced the corpus's 28.5 fps
-- costs a third of the frame rate. 300 frames in 16.567 s = 18.11 fps on a
screen that gives ~28 without it. That reclassifies 28.5 as a load-dependent
lower bound; it does not overturn it.
* A distinct-frame counter over the spinning ring FAILED its decisive control:
15.88 fps against the game's own 17.59 in the same window, 10% low, so the
ring does not change on every presented frame. Its static control also read
2.63 instead of ~0. Dead, not tuneable, per METHOD.md.
The rule that follows, and it applies to everything I hand the port: a measured
interval landing near a round number of declared units almost certainly IS that
number of units. Ship the units.
Also recovered here, because the same question needed it: the static PPC route.
Four tools open /work/xenia-rs/sylpheed.db and nothing in this repository builds
it -- no disassembler, no PPC decoder, and default.xex is encrypted (zero
plaintext "GamePart"). Xenia decompresses the image at load, so dump_image.py
reads it out of guest memory and validates it against the corpus's own landmarks:
the 29-entry GamePart id table at 0x820A1630 and the Xbox 360 D3D runtime
strings. String search and table dumps work again; instruction-level work does
not, and the present interval I wanted is an immediate, not a string.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
|
||
|
|
5b0a6e6666 |
re: the plate delay was on the disc all along -- my instruction is refuted
The port caught this with arithmetic off the disc, and it was right: build 2 has a keyframe group of its own, so "when build 4 has settled, wait 2.13 s, then composite build 2" puts the plate at settle + 2.13 + 3.97 s. Confirmed build 2's group here independently of their message: `ptbtn00.t32` reaches a=255 at t=238. The reconciliation needs no free parameter. Both builds run on ONE clock, started together, and the premise that fails is `rest.t`: rest.t is NOT when a screen settles. It is the last HOLD keyframe before the exit. ptlogo1 rests at t=251 and stops moving at t=42. The title's visible build-in ends at t=118, where pteff01, pteff02.prm and ptlogoall_eff end their ramps together. 238 - 118 = 120 units = 2.000 s, against a measured 2.138 and 2.132. So the interval the two runs agreed on to 6 ms was a DECLARED one and I handed over a wall-clock reading of it. That reading is 6.7% long, and the corpus already knew why: 120 units in 2.135 s is the game presenting at 28.06 / 28.14 fps against a nominal 30, and the idle title was independently measured at 28.5 fps before these runs. Corroborated from inside the same two runs -- first pixels -> settle is 1.643 s and 2.131 s, a 30% spread, while settle -> plate is 2.138 and 2.132. Frames are dropped during the build-in, not during the hold, which a change in the game's own timing could not do. So the port authors nothing here. What is unchanged: ScreenView still has to draw two builds at once and the boot's end state is still not plate-free. Not settled, and said so on the page: which reading of the keyframe times is right (it moves the plate by 2 units and I cannot separate them from these traces), and my settle landmark to better than +/-5 units. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw |
||
|
|
c1f3608b07 |
formats: a music bank's third sub-wave was its own header
The port hit `sound_bank_riffs("BGM_103.slb")` returning three against a census
that says two, and refused to guess which to drop. It was our reader.
`to_xma_riffs`'s hybrid branch derives a leading packet stream's start as
`first_riff % XMA1_PACKET`. That is right only when the bank header is smaller
than one 2048-byte packet -- true of the voice banks the branch was written for
(1392/1468/1600/1728), false of a music bank, whose header is exactly five
packets. The modulus returned 0 and the whole 10 240-byte header was emitted as
sub-wave 0.
The header states its own length, so the guard needs no threshold: BE u32 0x800
at +0x18 with the bank id repeated at +0x00 and +0x20, header length in blocks at
+0x24. Disc-wide over sound.pak's 9 519 entries, 28 match at offset 0 -- every
music bank, ids 1001-1023 and 1101-1105 -- and on 28/28 the declared header ends
EXACTLY at the first RIFF. Zero have a gap, so a header and a leading packet
stream never coexist here; zero false positives among the other 9 491.
Controlled rather than argued: decoding the emitted region through the same
chain, on the same bank, in the same run gives 0.009 s of PCM where the bank's
real wave 0 gives 87.744 s against a declared 87.75. The region is also 99.1%
zero bytes. And the oracle had already said two -- the XMA probe at the main menu
saw exactly two streams, at BGM_103's two declared wave sizes.
BGM_106-109 are deliberately NOT in the 28: their entries start mid-bank, so they
have no header at offset 0 and their leading region is real audio. The
VOICE_D_453 recovery is untouched and its tests still pass, 10/10 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
|
||
|
|
fb536df8b4 |
re: the PRESS-A plate arrives 2.13 s after the title settles, measured twice
The one number the port said decides a structural question on its side: whether
`ScreenView` has to draw two builds at once. It does. The boot title shows
build 4 alone, and 2.13 s after build 4 stops animating it composites build 2
over it. Two independent boots agree to 6 ms (2.138 / 2.132), which is under one
sample interval.
Measure from SETTLED, not from first pixels. "First drawn -> plate" is 3.78 s in
one run and 4.26 s in the other, because the build-in animation itself ran 1.64 s
and 2.13 s -- that spread is the emulator's frame pacing, and it is exactly the
kind of number that looks like a measurement.
Ruled out before believing it: that the plate was pulsing all along, too dim for
a thresholded glyph counter. The counter reads EXACTLY 154 -- the committed
no-plate title's own value -- for every frame of a plateau nearly one full pulse
period long, with zero variation, and the surface mean is flat to +/-0.03 across
it. A cycling overlay moves both.
Also settled, and also not:
* the black hold between two screens is 0.14-0.30 s, which brackets the port's
authored 0.17-0.23 s and the file's declared 12 units. Their constant stands.
* the (A)->menu latency is STILL not available, and now the reason is known.
Both runs freeze one frame for ~1.4 s at surface mean 26.626 -- agreeing
between runs to six decimals, and reproduced in run 2 with stream restarts
disabled, so it is not the capture path. It is a guest load stall: the (B)
path, which loads nothing, has no freeze at all. Any figure from it would be
an emulator load time.
Refutation attempt, recorded whether or not it survived: navigation.md's "the
title is not input-ready for about ten seconds, and even then (A) registers
roughly half the time". At 7.29 s and 7.28 s after the title settled, (A) was
accepted first press in both runs, as was (B) on the menu. n=2 only makes "half
the time" unlikely (p ~ 0.25); it contradicts the ten seconds outright.
And the standing red banner is withdrawn: the interactive title IS reachable in
this container, twice, with no pad input, in ~3.5 minutes. Why it changed is NOT
established -- this container came up with no Xenia storage root at all, so run 1
created a profile -- and that is written as a correlation for the next session to
test rather than as a cause.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
|
||
|
|
e2365004f0 |
tools: a title timing probe that costs 8.7 ms a frame, not 1503
The four durations withdrawn yesterday were produced by a classifier costing
1503 ms/frame draining an 8 fps x11grab at 0.64 fps -- a backlog, which
preserves ordering and destroys durations. This is the instrument for retaking
them.
What makes it cheap: every committed capture aligns at exactly dy=0 dx=0
(five-screens-acceptance), so the +/-8 px offset search screen_match does at
full resolution is 25 ZNCCs buying nothing on this path. Decimate 4x, do one
ZNCC per reference. Measured 8.7 ms per frame including the glyph count -- 173x.
Controls, run before the measurement and not after it:
* 9/9 content controls, including the two committed movie frames that are the
class this oracle exists to reject;
* 4/4 on the plate detector itself, which is a threshold on the green-glyph
counter and so needs its own control (no-plate title 159, plate title 753,
movie frames 0).
And three things learned from run 1, folded back in:
* do NOT restart the stream once the measurement is under way. Run 1's restart
landed 0.25 s after the (A) press and its stale frames straddled exactly the
interval being timed;
* press INLINE, not through pad.py's subprocess -- an interpreter start plus
the 0.25 s hold sat between the press and the timestamp;
* count the longest run of byte-identical surface means and report it. That is
the freeze signature, and it is how run 2 showed the 26.626 hold is the
guest rather than the capture path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
|
||
|
|
89f6644957 |
tools: fade_quads could not run -- two pre-monorepo paths
`screen-transitions.md` cites `fade_quads.py` as the command behind its decoded fade ramp, and the command had been dead since the monorepo migration: it read regn_decode.py from /work/Syplheed-Reborn and defaulted its pak to /work/sylph_extract, neither of which exists. Resolve the helper beside the script and default the pak under $SYLPHEED_DISC, the way run-canary and sylpheed-cli already do. A cited command that no longer runs is a citation nobody can check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw |
||
|
|
66e74d49c8 | Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures | ||
|
|
2021eee47d |
agents: merge main at the start of every iteration
Some checks failed
Both agents read the protocol, their mission and the shared tooling from their OWN checkout, and both work on topic branches -- so without an explicit sync they follow whichever version of the rules existed when the branch started. Found concretely: tools/audio-capture and two protocol revisions were on main while the decoder worked for hours from a branch that had neither. The port had merged on its own initiative and did have them, which is exactly the kind of divergence nobody notices until the two disagree about what the rules say. |
||
|
|
9a10258738 |
re: the main menu has no idle self-return -- and four of my own durations were a backlog
Two results and one retraction, all from the same session. REFUTED: 'an ~8-10 s idle returns to the title' does not apply to the main menu. Held untouched it stayed put for >= 60 s, correlation never leaving 0.9245-0.9249. That timer is real but belongs to the TITLE. It was the only reason 'B leaves the main menu' was classed as authored, so Q5's B rule is upgraded to measured-ordering: B is delivered (canary logs vk=5801) and is the only input in >= 100 s before the return. The PRESS (A) plate: the boot title presents build 4 WITHOUT the plate first -- green-glyph 154, against 159 on the committed no-plate capture and 753/977/1493 on plate titles -- and the plate arrives after. That is the port's third option. RETRACTED: four durations taken the same day. classify_array costs 1503 ms per frame; running it per frame against an 8 fps x11grab drained the pipe at 0.64 fps, so every classified frame was stale and increasingly so. It manufactured 'plate 24.66 s after the title art', 'B->title 15.58 s', 'B->title 25.60 s' and 'A->menu 20.26 s'. The tell: a transition, a press and a fade do not share a duration, and the two B figures GREW across a longer run. A backlog preserves ordering and destroys durations, which is why the sequence results above stand and every timing does not. The ring's period is unaffected and that was checked, not assumed -- ring_period ran at 15.03 fps against a requested 15. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP |
||
|
|
4fa3099249 |
re: the main menu's focus ring spins continuously -- period 2.18 s, measured
Answers the port's ask: ptbtneff01 is ANIMATED while a button is focused, not drawn once and held. The existing page said 'the ring SPINS' from one frame at a large angle, which is equally consistent with a static draw at a fixed angle. No angle is quoted anywhere. The 360-bin angular estimator written for this FAILED its own control -- a synthetic 30 deg came back as 0 deg (peak 0.596) while 90/180/270 came back exactly -- so it was not used. What settles it needs no angle: total annulus brightness is conserved to 0.4 % while individual angular bins swing by 24, i.e. brightness moving AROUND the ring, which excludes a pulse. The temporal-std map is a clean annulus, falling to ~1 both inside and outside the stroke, which excludes positional jitter. Period from the profile's autocorrelation: eight evenly spaced peaks, mean 2.177 s over nine revolutions. Even spacing is the internal check a drifting instrument cannot pass. That is 120 units = 60 frames = 2.00 s at a true 30 Hz. Also measured, same run: the ring is the ONLY moving thing on the settled main menu -- temporal std is exactly 0.000 on every unfocused button, the labels and the footer. And the ring's centre, located from the std map at game (520.7, 339.7), matches the declared leaf offset's prediction of (521, 340). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP |
||
|
|
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 |
||
|
|
a8d2491366 |
audio: actually install the capture path I kept deferring
Some checks failed
The audio work was three parts and I shipped two. The transcode-fidelity method and the pinned 5.1 downmix landed; the null sink -- the only one that answers "what does the GAME play" -- I deferred to "the next natural rebuild window" and then rebuilt both images four times without doing it. pulseaudio-utils is now in both, with tools/audio-capture wrapping it: a null sink is a real device as far as an application is concerned, so Canary and Godot open it normally and parec records what they emit. This unblocks the decoder's Q8. The cue-to-event bindings are currently a name match against the authors' own identifiers -- a plausible guess, not a measurement -- and capturing what the game plays on a menu move converts them. `audio-capture run` reports the peak level and warns when the capture is silent, because silence is the failure that looks like success: a WAV of exactly the right duration, full of zeroes, because the application opened a different sink. A duration check alone passes it, which is how a confident wrong number gets made. |
||
|
|
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
|
||
|
|
20b3c74b2c |
agents: they never spoke, the decoder lost the disc, and both shared one state dir
Some checks failed
Three defects, all mine, found by checking instead of assuming. **They never exchanged a word.** SendMessage=0, ListAgents=0 across both new sessions. PROTOCOL.md specified in detail what a message MAY and MAY NOT do and never said how to send one or that the other agent was addressable -- they knew that last time only because the human told them directly, and rebuilding with fresh volumes wiped it. Policy without mechanism is prose. Now documented with the two addresses, a worked example, and an instruction to introduce themselves on the first iteration rather than waiting to have a question. **The decoder lost the disc and the ISO.** They used to arrive inside the project mount and silently stopped when /work became a clone. Silently is the word: the disc-gated tests SELF-SKIP without SYLPHEED_DISC and report green, so a whole test suite would have passed while measuring nothing. Both are now mounted explicitly, the ISO at a stable path so run-canary does not depend on host directory names. **Both agents shared one Claude state directory.** They share the host's ~/.claude, and once both working directories became /work they resolved to the same projects/-work/ -- two supposedly independent agents writing to one place, which undoes the point of separate checkouts. Each now has its own volume, seeded once from the host with credentials only, so a token refresh writes locally and neither can corrupt the host's auth. Also widened the pacing rule. It banned ScheduleWakeup by name; the decoder then scheduled itself an hourly cron job -- not harmful, but the same instinct that ended a run yesterday, through a door I had left open. Now: no self-scheduling by any route. Mount audit after the changes: shared and intentional are the exchange volume and the read-only credential seed. Everything else -- repo, Claude state, cargo, target, canary, disc, ISO -- is per agent or one-sided. |
||
|
|
01a3505b1e |
containers: fix volume ownership and make the clone guard survive interruption
Some checks failed
Two bugs, both mine, both found by starting the thing.
**Volume mount points must exist AND be owned by the agent before USER agent.**
Docker seeds a named volume from whatever the image has at that path, ownership
included, and creates a ROOT-OWNED directory when the path is absent. Either way
the agent cannot write, and the failure surfaced far from its cause: "clone
FAILED", with no permission error anywhere in sight. The port's own Dockerfile
already carried a comment explaining this trap, which I then walked into for
/work and /exchange.
**The clone guard checked for a .git directory, not a usable HEAD.** A clone
interrupted partway -- the container was removed while one ran -- leaves a .git
with no commits, and a presence check then skips the retry forever and hands the
agent an empty repository that looks like a checkout. It now verifies HEAD, and
clones via a temp directory so a partial result never lands in /work at all.
Also: the port launcher's path defaults still assumed the old repo root, so it
mounted no disc; and the stale /reborn notice is gone now that there is one
repository.
Verified running: both agents cloned
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
06676d3dc0 |
containers: each agent clones the monorepo into its own volume
Some checks failed
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. |
||
|
|
a8815f2826 |
agents: the team protocol, the share tool, and a player's-eye navigation doc
Some checks failed
**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. |
||
|
|
65cefa74c3 |
monorepo: one repository for the decoders, the port and the corpus
Some checks failed
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
f6fccee11c |
deps: bump the pin to f817dd5 -- the menu screens get their background
WHAT I WANTED FROM IT:
|
||
|
|
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>`). |
||
|
|
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). |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |