Nexus MENU.BPK to VDP1 snapshot join

This receipt retains a byte-level observation from the Japanese retail title menu. It is deliberately narrower than a renderer admission: it proves a subset of real MENU.BPK output was present in one VDP1 VRAM snapshot, not which VDP1 commands consumed it or how the Saturn palette/compositor presented it.

Inputs

Input SHA-256
Japanese retail MENU.BPK 740ab2a864f04b89cddb172ce2560044fcc8c6a7f98ae2fe50461aa8da886636
Paused retail-title-menu vdp1-vram.bin 968e9d0f05de45ff8c97ea562988cd0aea7c125b54822983ca52c12edc88b895

The 512 KiB VDP1 image belongs to nexus_v1_ymir_title_menu_replay_20261020. Its accompanying receipt identifies the state as a Japanese retail-disc boot followed by one Start press at the title prompt. It is an emulator observation, not a hardware capture. The snapshot has no command trace from the same capture, so do not join it with a trace from another run.

Reproduction

python3 scripts/analyze_nexus_menu_bpk_vdp1_snapshot_join.py \
  /home/trv2/.firestaff/data/nexus/MENU.BPK \
  /home/trv2/work/firestaff-parity-debug-20261024/parity-evidence/verification/nexus_v1_ymir_title_menu_replay_20261020/dumps/vdp1-vram.bin \
  --expected-vdp1-sha256 968e9d0f05de45ff8c97ea562988cd0aea7c125b54822983ca52c12edc88b895

Only add --vdp1-command-trace when the trace and VRAM dump are from one atomic capture, and pair it with --vdp1-command-trace-frame for the frame represented by the snapshot. This retained replay has no such command trace.

The tool rejects a different MENU.BPK identity, malformed BPPK/PRS3 framing, an image that is not a 512 KiB VDP1 dump, or a supplied snapshot hash mismatch. It decodes the documented bounded DMWeb PRS3 stream and searches for every possibly overlapping occurrence of each whole decoded surface, directly and with 16-bit word order swapped. A supplied command trace must come from the same capture as the VRAM snapshot. The analyzer counts source-start checks only for its explicitly paired frame.

Result

The hash-pinned rerun decodes all 162 declared PRS3 surfaces. Entries 77 through 162 occur byte-exactly in VDP1 VRAM (86/162 surfaces); none of those joins requires word swapping. Their first direct-match offsets descend from 0x6c700 (entry 77) to 0x61d60 (entry 162). The other 76 declared surfaces do not have a whole-surface direct or word-swapped match in this snapshot.

This is byte-level evidence that authentic menu-package surfaces were present in VDP1 VRAM. It does not prove which command consumed them or authorize a host menu renderer. The following independent evidence is still required before changing presentation admission:

Until then, the tool emits semantic_admission=blocked and no decoded pixels are handed to the production renderer.

Command trace status

Retained same-run capture (2026-10-26)

A new Ymir 0.4.2-dev run loaded the hash-verified Japanese title-prompt state, held Start for 60 of 120 capture frames, and dumped memory and the VDP1 trace in the same run. The saved screenshot shows the retail three-choice menu. The VDP1 VRAM dump has SHA-256 0989ce1cba450dbe5a766d346b979ac038a236589225f3f795c8dc06f2ec6ea7; the complete command CSV has SHA-256 b80c7a118e7f9df4f941804cdc045f4024336080504c512bbdf5c4e0df19d8d3. The bundle records 10,668 command rows, zero dropped rows, and 178 commands in its last captured VDP1 frame, index 59. Filtering the CSV to frame 59 and joining with the final VRAM dump finds 86 whole MENU.BPK surface matches, 137 executed textured commands, and no textured command whose source start falls in those matched ranges.

An initial analysis incorrectly included skipped VDP1 commands and reversed the character width and height fields. With both corrected, the independent source scan initially matched all 137 executed texture spans to retail files. Eleven spans are all zero and also match unrelated file padding, so they do not establish source identity. Excluding those uninformative matches leaves 126 of 137 spans with nonzero byte matches to authenticated retail files. This is a byte-source observation, not proof of final pixel composition or input behavior.

The source scanner verifies each MNS file against its retail hash before extracting candidate surfaces. Re-running the retained frame with this check still finds 126 informative source matches among the 137 textured commands.

Five informative commands in that trace instead match the Japanese retail TITLE.BIN (SHA-256 51f1f18b68acf5993b00ffcb458ef2a7372b21595656f3ed5b95520c9a305fc3). Commands 0xd8e0 and 0xd900 both match 288 bytes at file offset 0x15f6; commands 0xd920, 0xd940, and 0xd960 match spans of 256, 256, and 416 bytes at offsets 0x0446, 0x0576, and 0x07d6, respectively. Each is a normal sprite in 4-bit colour-bank mode, with captured CMDCOLR values 0x52f0, 0x52f0, 0x5220, 0x5230, and 0x5250. These are byte matches against the later VRAM snapshot, not a time-aligned palette or a proof of the final menu image. The later full-frame capture replay combines the captured VDP1 display framebuffer with VDP2 state; it does not reconstruct the frame by replaying these command records. Separate capture-only paths resolve source-bound mode-1 lookup textures and preview or blend captured mode-0 bank sprites. Those paths do not establish sprite ordering or priority, VDP2 layer selection, or a time-aligned reference image. nexus_v1_vdp1_decode_mode0_bank_texture now decodes the sprite’s packed indices and VDP1 colour-bank words from a VRAM snapshot. The retained 0xd8e0 command and its 576 texels pass the external capture test. These words still need transparency, end-code, sprite priority and time-aligned VDP2 colour RAM handling before they can produce a trustworthy displayed image. The retained VDP2 snapshot has SPCTL 0x1536 (sprite type 6), RAMCTL 0x0300 (CRAM mode 0), and CRAOFB 0x0000. The mode-0 type-6 capture resolver now maps the observed title sprite’s ten-bit colour indices through that captured CRAM; the external test verifies its palette addresses and RGB components. It rejects unsupported sprite formats, enabled end codes, and shadow/RGB cases. The separately dumped state remains a snapshot, so this is not proof of time-aligned composition or permission to display the result. An isolated normal-sprite preview now places these mode-0 pixels using the captured local coordinate and clip, including horizontal and vertical flips. The external test checks the 0xd8e0 sprite at (104,144) against all 576 decoded texels. It remains a capture preview; sprite priority, the other VDP1 commands, VDP2 layers, and a time-aligned reference image are not established. With all external capture paths set, the test now exercises the preview for all five commands, including the horizontally flipped sprite. Its visible pixels are checked against both captured VDP1 buffers and the captured CRAM values. The test accepts FIRESTAFF_NEXUS_YMIR_DUMP (VDP1 VRAM), FIRESTAFF_NEXUS_VDP1_TRACE (frame-59 little-endian records), FIRESTAFF_NEXUS_YMIR_CRAM, FIRESTAFF_NEXUS_YMIR_REGS, and FIRESTAFF_NEXUS_YMIR_VDP1_FBS (Ymir’s draw/display buffers). None of these game-derived files is stored in the repository.

The retained Ymir framebuffer dump supplies a stronger VDP1 comparison. Both the draw and display buffers contain the expected big-endian colour-bank word at every nontransparent pixel of all five sprites: 213, 213, 313, 293, and 463 pixels respectively, for 1,495 matches out of 1,495 per buffer. The external lookup test checks the command positions and horizontal flip against both buffers when FIRESTAFF_NEXUS_YMIR_VDP1_FBS is provided alongside the trace and VRAM paths. Transparent pixels and final VDP2 output are not covered by this comparison, and the dump does not prove per-command snapshot timing. The separately saved 640×448 screenshot contains all 1,495 nontransparent sprite pixels at twice the VDP1 coordinates. Their RGB channels equal the captured CRAM components converted to 8 bits and multiplied by 31/32, rounding down. The earlier zero-match result compared unscaled colours and is withdrawn. Recheck with python3 scripts/verify_nexus_title_sprite_screenshot.py BUNDLE SCREENSHOT TITLE.BIN; Pillow is required. The retained registers explain the top-layer factor: CCCTL 0x0043 enables sprite colour calculation in top-ratio mode, SPCTL 0x1536 selects the priority-equals-5 condition, and PRISC 0x0504 assigns priority 5 to these type-6 pixels. Their ratio index is zero and CCRSA 0x1000 selects a top ratio of 31/32. At all 1,495 compared positions, the captured NBG0 bitmap contains transparent index zero. The captured per-line back-screen table is black on the affected lines. The verification now reads these lower-layer values from the VDP2 dump instead of assuming black. The screenshot was taken after the dump, so exact capture timing remains unproven; this covers these sprites, not the complete image or menu interaction. The capture preview can now blend a normal mode-0 sprite over a supplied lower framebuffer using type-6 priority and top-screen ratio registers. The retail capture regression checks the observed 31/32 result over black and the blend over a nonblack test background. It does not select the lower VDP2 layer or apply sprite-window masking; those inputs must be established separately.

For mode-1 palette analysis, Ymir’s software VDP1 renderer counts two end codes per rasterized line: each end code is skipped and the second stops the line. The no-draw palette decoder follows that count. The triangle capture compositor also skips the first code and stops at the second in source order, but this remains an approximation: VDP1 resampling and texture direction can change which texels are encountered. If the texture contains a code, its preview cannot authorize rendering.

The same CMDSIZE and skipped-command corrections also apply to the Mednafen VDP1 source, material, and write-join analyzers. Earlier numerical receipts produced by those analyzers must be regenerated against their original raw captures before they are used as evidence; they are not automatically validated by the Ymir result above.

One retained Mednafen startup capture has now been reanalyzed with the corrected command decoder. Frame 0 of the four-frame nexus_v1_startup_render_27518_20261009 capture (SHA-256 1eaa507e314b60f1cc123dd6eb9fbcff4e635dcd1ea5b6cb7aeb16d12e33baae) has a 236-record chain and 221 executed textured draws. The authenticated LEV00.DGN material join finds 215 matching source/palette pairs, including 186 with Structure3 face owners. Six draws remain unmatched. This is a capture-level result; it does not establish display composition or permit production rendering.

The broader source-only scan counts 201 of 221 draws with nonzero matching retail bytes when the verified Japanese ITEM.IBS is included. Of the six unmatched source/palette pairs, five use the same 448-byte all-zero texture, which cannot identify a resource. The remaining draw at command 0x00f00 uses a 1,152-byte span matching the word-swapped Japanese ITEM.IBS bytes at file offset 0x123dc; the file SHA-256 is fc32ca5875906e6e0dc69e0b5edfa5d00cb1f4401b7d497397c699be7c4530c1. This initial file-level join does not establish item identity, placement, or production renderer permission. A matching all-zero image must not be used to infer its owner or promote a renderer gate.

After aligning the C chain walker with Ymir’s single return-address model, the snapshot heuristic selected an invalid longer candidate that drew before any captured local coordinate. Candidates without an origin before their first draw are now excluded. On the same frame the C replay selects 222 draws, resolves 221, and verifies 201 nonzero source/palette joins. This remains a heuristic snapshot chain, not proof of the original execution order; the Python scanner and direct trace may select different chains.

Further inspection of that same Japanese ITEM.IBS and capture binds the 1,152-byte image to floor-image descriptor 33: encoding 0x0008, dimensions 48×48, palette id 8. Its descriptor reuses the palette first supplied by descriptor 0 at file offset 0xb098. All 32 palette bytes, after the same 16-bit word swap, equal the VDP1 command’s captured CLUT at VRAM byte offset 0x18820. This establishes the descriptor, image bytes, and CLUT byte join; the displayed item’s game identity, transform, and composition remain open. The retained files can be rechecked with python3 scripts/verify_nexus_item_floor_capture.py CAPTURE ITEM.IBS --frames 4 --frame 0 --command-offset 0xf00 --floor-index 33. Firestaff’s verified ITEM.IBS parser now resolves reused palettes by palette ID; previously it reused the most recently declared palette even when that palette belonged to another ID. The real Japanese corpus regression checks all 16 words for descriptor 33 against the original palette at 0xb098.

The frame counter in the VDP1 trace is distinct from the 120 requested capture steps, and the screenshot is taken after the dump. This receipt therefore does not establish that all three artifacts represent precisely the same displayed frame or exclude use of MENU.BPK in other frames. It also tests source starts, not complete texture spans. The raw files are retained on the external evidence volume; no game-data payload is committed.

Comparing the 178 traced command records in frame 59 to the final VRAM snapshot gives 177 exact 32-byte matches. The first traced command at address zero differs in its link word: the trace has 0x1874, while the final dump has 0x0004. This confirms that the command table changed between execution and the snapshot; it does not identify when any texture bytes changed. A snapshot-only source join therefore cannot prove the historical command chain or promote a renderer on its own.

The recorded frame-59 command order was independently checked against each record’s CMDCTRL jump mode and CMDLINK: all 177 transitions match, and the last record at 0xd9a0 has END set. The observed root is 0x00000; its skip-and-jump command uses the traced link 0x1874. This validates the recorded order for this Ymir frame, without claiming that its texture bytes or later screenshot were frozen at each command’s execution time.

A previous draft of this note reported a one-frame VRAM/command-trace pair and concluded that no textured command started in a matched MENU.BPK range. The two raw files are not present together in the retained evidence, so that join cannot be independently reproduced. Withdraw the 168-command, 135-textured, zero-source-hit result and do not use it as evidence. The retained 2026-10-25 command history and the 2026-10-20 VRAM snapshot are separate captures; pairing them would be invalid. A new atomic capture with both outputs is required before making any command-consumption claim.

The confirmed VRAM snapshot proves only that 86 real MENU.BPK surfaces were resident in that snapshot. It does not establish whether the title menu reads them. This remains non-admission evidence and does not identify the active menu assets, palette, placement or compositor path.

Five-frame Start write trace

A separate isolated capture loaded the same retained Japanese title-prompt state, held Control Pad 1 Start for capture frame zero, and ran five frames with --debug --trace-sh2-writes-begin 0x05c00000 --trace-sh2-writes-end 0x05c80000. Ymir wrote a complete 1,082-row VDP1 SH-2 trace without dropped rows. It is intentionally a post-state-load observation: it can identify work caused by Start, but cannot claim to contain the earlier pre-load that produced the title-prompt textures. Its trace is not paired with the 2026-10-20 VRAM snapshot above.

The most frequent writer was master SH-2 PC 0x0608ba46, with 1,004 writes in 0x05c000c0..0x05c18710. Disassembly of the captured WRAM bytes at that PC is a zero-fill loop (mov.l r5,@r4, then add #16,r4 and branch). It is therefore a VDP1 clear, not a texture upload. No traced write covers the title-frame texture starts 0x05c02460, 0x05c02660, or 0x05c02740, nor the resident MENU.BPK match interval.

Use the retained deterministic trace with:

python3 scripts/analyze_nexus_vdp1_sh2_write_trace.py \
  /path/to/sh2-memory-write-trace.csv \
  --source-address 0x05c02460 \
  --source-address 0x05c02660 \
  --source-address 0x05c02740

It retains only the observed PC/address facts and always leaves semantic admission blocked.

The C command-sequence validator also accepts ordered command words from a producer trace. It checks every jump, call, return and END, and tracks local coordinates without applying skipped commands. The caller must first verify that the trace covers a complete frame and that its producer dropped no rows. The Japanese retail title-menu trace at Ymir frame 59 passes with 178 commands; the captured words are used directly, rather than substituted from the later VRAM snapshot. This does not establish that textures or the final image match. Transition checks follow Ymir’s VDP1 model: a single saved return address, sequential execution on a return without a call, 32-byte link alignment, and 512 KiB address wrapping. Ymir describes nested-call behavior as observed, so this model is not independent hardware verification. The capture compositor can consume those traced command words while resolving materials from a separate VRAM snapshot. It requires explicit verification of trace completeness and keeps renderer permission disabled because the later snapshot alone cannot prove the texture and palette state at execution time.

Full Ymir title-frame capture replay

The atomic frame-59 Ymir bundle also contains the VDP1 display framebuffer, VDP2 VRAM, CRAM, and registers. The bounded C function nexus_v1_vdp2_capture_replay_ymir_title_menu now reconstructs the full 320x224 capture from those real buffers. It admits only the observed NBG0 8bpp bitmap configuration, register offsets, scroll/scale, priority and colour calculation settings, black per-line back screen, and type-6 sprite palette lane. It rejects missing capture/trace attestations, other register layouts, unexpected sprite priorities, and a nonblack back-screen row.

The verifier pins all inputs to this retained capture by SHA-256:

Capture input SHA-256
vdp1-fbs.bin 2dc15ef07aaacd89a00af18fe3e0bf3677bdb626f3a852f37820eb29bc9ce1ce
vdp2-vram.bin de1a77427dc62457eb4b59fa0424fd25e9768ce229505445311929d3bdae405c
vdp2-cram.bin db282593790dec4a2bf8f76293c0458a1a140adc3351650bd56a417f0359e0de
vdp2-registers.bin 79efa616c2db727749aa9018864d1ad32c1583935652fafb16534ba69f8e1f2d
vdp1-command-trace.csv b80c7a118e7f9df4f941804cdc045f4024336080504c512bbdf5c4e0df19d8d3
Frame-59 screenshot 8721cce3354bc20b1cd367fd44be3d4ecc60ca65fa89c7d00654e83f5ad7db23

The display framebuffer contributes 70,185 direct RGB words. NBG0 overlays 18,513 pixels with the captured CRAM and 31/32 top ratio; 1,495 indexed type-6 VDP1 pixels use their captured priority and CRAM words over the verified black back screen. The output matches every one of the screenshot’s 286,720 pixels exactly, including each of the four pixels in the 2x-scaled source pixel. Re-run the C implementation against the retained real capture:

python3 tests/test_nexus_ymir_title_capture_parity.py \
  /path/to/test_nexus_v1_vdp2_capture_compositor \
  /path/to/ymir_nexus_atomic_menu_20261026/dumps \
  '/path/to/ymir_nexus_atomic_menu_20261026/screenshots/DUNGEON MASTER NEXUS [T-9111G]-20261026T200759.691.png'

This result establishes exact static capture composition for the retained frame. The screenshot was taken after the dump, so frame identity remains unproven. The API reports capture_only=1 and renderer_permitted=0; it does not yet feed the live Firestaff viewport, prove the startup sequence, or test controller and mouse interaction. The evidence dumps remain on the external disk and are not committed.

Frame 8047 repeat menu capture

The later ymir_nexus_menu_source_wide_frame_read_20261027 capture provides a second full-frame replay, now paired with the Japanese Continue-menu screenshot in one Ymir run. Its VDP1 trace contains 331,364 rows, including all 178 commands for frame 8047, and reports zero dropped rows. The matching register receipt also names frame 8047. The C replay matches all 286,720 pixels in the 640×448 screenshot exactly after 2× scaling.

Capture input SHA-256
vdp1-fbs.bin 2752a34f5d200c965e2fe5c35ff6f973361319d769a2d76944af2c7b0f480979
vdp2-vram.bin de1a77427dc62457eb4b59fa0424fd25e9768ce229505445311929d3bdae405c
vdp2-cram.bin db282593790dec4a2bf8f76293c0458a1a140adc3351650bd56a417f0359e0de
vdp2-registers.bin e69d03094ef856922413e073cb9078015e52849786268d6421c7df00a655965e
vdp1-command-trace.csv d13fcede5adcad648b9c167df9c54eb928760185886bf667223f1272bd505e98
Frame-8047 screenshot 741c620e945b5ab3e05f6bc5d182fc4ada9439071b79718f67326bcdd56c6118

The replay reports 70,239 direct VDP1 framebuffer pixels, 1,441 type-6 palette pixels, and 18,513 NBG0 overlays. These counts differ from the older frame-59 capture while covering the same full 320×224 output. The separate source verifier binds all 1,441 visible Japanese menu-sprite texels to TITLE.BIN. The screenshot has no embedded frame number; the run log and register receipt bind it to the same capture run, while exact acquisition-frame identity remains unproven.

Reproduce this paired capture against the authentic Japanese data and retained external evidence:

python3 tests/test_nexus_ymir_title_capture_parity.py \
  /path/to/test_nexus_v1_vdp2_capture_compositor \
  /Volumes/Extern-disk/firestaff-evidence/ymir_nexus_menu_source_wide_frame_read_20261027/dumps \
  '/Volumes/Extern-disk/firestaff-evidence/ymir_nexus_menu_source_wide_frame_read_20261027/screenshots/DUNGEON MASTER NEXUS [T-9111G]-20261027T194440.170.png' \
  --run-log /Volumes/Extern-disk/firestaff-evidence/ymir_nexus_menu_source_wide_frame_read_20261027/run.log

Both profiles are capture-only and keep renderer_permitted=0. The exact composition replay uses the emulator’s final display framebuffer, VDP2 memory, CRAM, and registers as inputs; it does not reconstruct the menu from retail assets or admit them to Firestaff’s live startup renderer. The Japanese source read trace still omits byte values at access time, and the NBG0 source join is partial. Runtime input, mouse hit areas, startup presentation, save loading, and playable-level/HUD parity remain open.