Frame metadata: active width, lines, interlace, field; shots crop from it #29

Merged
Gatchan merged 6 commits from frame-metadata into main 2026-10-29 19:07:08 +00:00
Owner

Fixes #2

Changes

  • core/hw/hw.c hw_picture: fills the metadata from the LakeSnes PPU state (no LakeSnes patch):
    • height = 224, or 239 when frameOverscan; doubled when frameInterlace;
    • interlaced = frameInterlace; new field = evenFrame ? 0 : 1 (the field this frame rendered; snes_runFrame stops at vblank start, so the flags latched at (0,225)/(0,240) belong to the frame just drawn);
    • width = 512 when the picture holds a hires pixel (two columns of a pair differ), else 256. The mode can change on any line through HDMA and the end-of-frame register value may already describe the next frame, so the width is read from the picture LakeSnes produced (checked in the existing conversion loop). LakeSnes blends pseudo-hires into one colour per pixel, so pseudo-hires reports 256 (nothing lost by cropping).
  • core/api/ff6_core.h: pixels stays the 512x480 buffer (byte-identical, so movie picture hashes do not change); the layout is documented (first row 16 or 2, two rows per line or one per field when interlaced, two columns per pixel unless width 512). New field member and ff6_frame_picture(output, dest, dest_pixels) copying the active picture out, one pixel per active pixel.
  • ff6cli --shot, drive server shot (reply now width=256 height=224 interlaced=0 field=<0|1> frame=<n>), tools/mcp/tools/ff6_drive.py description: crop through ff6_frame_picture. Shots are now 256x224 (previously 256x240 including 8 black lines top and bottom).
  • Tests: rom.frame.US_V10/JP_V10 (new, label rom): frames 1-600 from power-on are all 256x224, progressive, fields alternate; at the title (frame 600) the rows outside the 224 lines are black and the crop has lit pixels. unit.common: ff6_frame_picture on a synthetic buffer for 256x224, 512x239, 256x448 interlaced, 512x478, too-small buffer and invalid metadata. serve_test.cmake, test_ff6_drive.py (256x224 + metadata in the reply), docs/build.md.

Does FF6 use overscan, interlace or hires?

No evidence that it does, neither in the source nor at run time:

  • Source (disassembly src/): all six writes to SETINI ($2133: interlace, overscan, pseudo-hires, OBJ interlace) store 0 (field/reset.asm, menu/menu_init_2.asm, world/init.asm, btlgfx/interrupt_2.asm, cutscene/cutscene_main.asm, cutscene/the_end.asm). BGMODE ($2105) values written directly or through HDMA tables are modes 0, 1, 2 and 7 only (field/menu $09 = mode 1, world 2/7, cutscenes 1/7, opening HDMA $09/$07, ending big-airship HDMA 1/7, the_end 0/1, battle mode 1 + slot menu mode 2); battle animation scripts use screen_mode 1 and screen_mode 7 only.
  • Run time: a temporary instrumented ff6cli (scratch build, not committed) logged any frame with width != 256, height != 224, interlace, overscan, pseudo-hires, OBJ interlace or BG mode 5/6 at frame end, while replaying narshe_to_world_map (US 79 761 / JP 80 670 frames: title, opening, field, battle, menu, name entry, world map) and field_menu_tour (US, JP): 0 frames logged, all four replays movie: match.
  • Not covered by routes: the ending / The End, airship and Floating Continent cutscenes, later battles. The source survey covers them statically (no SETINI value other than 0, no mode 5/6), so the expected answer is 256x224 progressive throughout; the metadata handles the other cases anyway.

Validation

  • Builds clean (0 warnings): debug, clang, mingw.
  • release --fast 22/22, debug --fast 22/22 (includes rom.frame, rom.serve, rom.movie, unit.common).
  • tools/mcp/tests/test_ff6_drive.py: ALL OK.
  • Shots checked by eye: US title logo and JP title clouds at 256x224 without the black bands.

🤖 Generated with Claude Code

Fixes #2 ## Changes - `core/hw/hw.c` `hw_picture`: fills the metadata from the LakeSnes PPU state (no LakeSnes patch): - `height` = 224, or 239 when `frameOverscan`; doubled when `frameInterlace`; - `interlaced` = `frameInterlace`; new `field` = `evenFrame ? 0 : 1` (the field this frame rendered; `snes_runFrame` stops at vblank start, so the flags latched at (0,225)/(0,240) belong to the frame just drawn); - `width` = 512 when the picture holds a hires pixel (two columns of a pair differ), else 256. The mode can change on any line through HDMA and the end-of-frame register value may already describe the next frame, so the width is read from the picture LakeSnes produced (checked in the existing conversion loop). LakeSnes blends pseudo-hires into one colour per pixel, so pseudo-hires reports 256 (nothing lost by cropping). - `core/api/ff6_core.h`: `pixels` stays the 512x480 buffer (byte-identical, so movie picture hashes do not change); the layout is documented (first row 16 or 2, two rows per line or one per field when interlaced, two columns per pixel unless width 512). New `field` member and `ff6_frame_picture(output, dest, dest_pixels)` copying the active picture out, one pixel per active pixel. - `ff6cli --shot`, drive server `shot` (reply now `width=256 height=224 interlaced=0 field=<0|1> frame=<n>`), `tools/mcp/tools/ff6_drive.py` description: crop through `ff6_frame_picture`. Shots are now 256x224 (previously 256x240 including 8 black lines top and bottom). - Tests: `rom.frame.US_V10/JP_V10` (new, label rom): frames 1-600 from power-on are all 256x224, progressive, fields alternate; at the title (frame 600) the rows outside the 224 lines are black and the crop has lit pixels. `unit.common`: `ff6_frame_picture` on a synthetic buffer for 256x224, 512x239, 256x448 interlaced, 512x478, too-small buffer and invalid metadata. `serve_test.cmake`, `test_ff6_drive.py` (256x224 + metadata in the reply), `docs/build.md`. ## Does FF6 use overscan, interlace or hires? No evidence that it does, neither in the source nor at run time: - Source (disassembly `src/`): all six writes to `SETINI` ($2133: interlace, overscan, pseudo-hires, OBJ interlace) store 0 (`field/reset.asm`, `menu/menu_init_2.asm`, `world/init.asm`, `btlgfx/interrupt_2.asm`, `cutscene/cutscene_main.asm`, `cutscene/the_end.asm`). `BGMODE` ($2105) values written directly or through HDMA tables are modes 0, 1, 2 and 7 only (field/menu $09 = mode 1, world 2/7, cutscenes 1/7, opening HDMA $09/$07, ending big-airship HDMA 1/7, the_end 0/1, battle mode 1 + slot menu mode 2); battle animation scripts use `screen_mode 1` and `screen_mode 7` only. - Run time: a temporary instrumented ff6cli (scratch build, not committed) logged any frame with width != 256, height != 224, interlace, overscan, pseudo-hires, OBJ interlace or BG mode 5/6 at frame end, while replaying `narshe_to_world_map` (US 79 761 / JP 80 670 frames: title, opening, field, battle, menu, name entry, world map) and `field_menu_tour` (US, JP): 0 frames logged, all four replays `movie: match`. - Not covered by routes: the ending / The End, airship and Floating Continent cutscenes, later battles. The source survey covers them statically (no SETINI value other than 0, no mode 5/6), so the expected answer is 256x224 progressive throughout; the metadata handles the other cases anyway. ## Validation - Builds clean (0 warnings): debug, clang, mingw. - `release --fast` 22/22, `debug --fast` 22/22 (includes rom.frame, rom.serve, rom.movie, unit.common). - `tools/mcp/tests/test_ff6_drive.py`: ALL OK. - Shots checked by eye: US title logo and JP title clouds at 256x224 without the black bands. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Gatchan/FF6Portage!29
No description provided.