Skip to content

sweep-media-r2 (2026-11-01, batch r2): movie AAC / VP8 / ARGB, clean guest exit, npad device types, LAS golden route, doc rows

Branch claude/sweep-media-r2-2026-11-01-r2 (Mac session, base orchestrate/2026-11-01-r2 = 79415f0).

Movie library decoders (runtime/src/movie.c, services/movie_audio_core.[ch])

  • DecoderOutputFormat read from the library itself (LAS subsdk1, disassembled from the shared title data): DecoderImpl::ConvertDecoderOutputFormat (+0x1f230) copies the codec's NV12 rows for format 4 and tail-calls DecoderImpl::ConvertToArgb (+0x1ef90, reached through its PLT slot / GOT relocation) for format 2. ConvertToArgb writes 4 bytes per pixel, bytes 1..3 = R G B (byte 0 = A), with BT.709 YUV coefficients on full-range Y: R = Y + 1.28033 V, G = Y - 0.21482 U - 0.38059 V, B = Y + 2.12798 U (constants at +0x524090, +0x524158, +0x52407c, +0x52410c), truncated and clamped. Format 1 is PCM (audio only), 3 texture mode; anything else for a video type is refused by IsOutputFormatSupported.
  • Format 2 (ARGB) implemented for VP8 / VP9 / H.264 buffer-mode decoders with exactly that formula (A = 255). The output format metadata for format 2 (color-format 16, stride = width x 4) is Android's ARGB description: the library's own GetOutputFormat for format 2 was not read (unverified).
  • AAC (type 4) on the host by default: AudioToolbox AudioConverter with an esds magic cookie built from csd-0 (AudioSpecificConfig), HE-AAC through kAudioFormatProperty_FormatInfo, ADTS framing accepted (stripped; the first header configures when there is no csd-0, is-adts), raw units without either taken as AAC-LC at the announced rate / channels. Output s16 interleaved in WAVE order (L R C LFE Ls Rs ...), the order Android's SoftAAC2 gives: the converter is asked for the WAVE layout, a manual remap from the bitstream order is the fallback (both paths tested). NAPPLE_MOVIE_AAC=0 leaves type 4 to the library, whose own SoftAAC2 (found in subsdk1) runs on the CPU.
  • VP8 (type 2) through libvpx like VP9 (vpx_codec_vp8_dx), buffer or texture mode.
  • Portable parsers in movie_audio_core.c (ASC with escape rates and explicit SBR / PS, ADTS, a two-byte ASC writer), tested on the Linux-compatible test_svc_movie_audio (+11 checks).
  • Evidence: new runtime/tests/test_svc_movie_host.c drives movie.c's decoder entries exactly as the library's callers do, with fake guest objects (Buffer, MediaData with csd buffers, events) and streams made here by encoders (no downloads, no fixtures): AudioToolbox AAC stereo with csd-0 / ADTS-framed / no csd-0 (440 / 1000 Hz tones back at 440.0 / 1000.0 Hz, RMS within 0.3 %), 5.1 with one tone per channel (channel order L R C LFE Ls Rs verified; a mutation without the remap fails the check), libvpx VP8 NV12 (worst PSNR 51.6 dB over 30 frames), VP8 ARGB (47.9 dB against the library formula applied to the source), VP9 ARGB (44.0 dB), plus refusals (video in PCM format, AAC with a video format left to the library, a VP8 decoder given a VP9 format): 1015 checks. No test title creates a type 2 or type 4 decoder or uses format 2, so these are tested, not verified on a title. LAS's VP9 + Vorbis path, which the refactor touched (generic audio decoder), still plays: headless LAS runs decode the opening movie (5100+ VP9 frames) with Vorbis at RMS 5939.

Clean guest exit (runtime/src/main.c)

cb_finis (the SDK 15 nn::init::Start finalizer callback) used to abort with "exit path not implemented". Now guest_exit_clean() runs every module's finalizers in reverse load order (DT_FINI, else fini_array from last to first; NROs unloaded by ldr:ro skipped; null / -1 entries skipped), then fs_report_uncommitted() (journaled saves: uncommitted changes roll back at the next start, as on the console; commits are already synced), sync(), the audio reports, a summary (last_stage exit, status partial, reason "guest exited normally"), gfx_shutdown() and GUEST_EXIT(0). Harness test test_svc_guest_exit runs it in a forked child with fake modules: finalizer order S0cba, exit status 0, summary contents (5 checks). Not covered: the 2-callback Start form has no finalizer callback; a title that returns from nnMain there ends in svcExitProcess, which kernel.c (not this branch's) still reports as fail.

npad device types (input/)

Decision given by the orchestration: the standard styles now report libnx HidDeviceTypeBits like a console: Pro Controller FullKey (bit 0), handheld HandheldLeft | HandheldRight (0xc), Joy-Con pair JoyLeft | JoyRight (0x30), single Joy-Con its side. NAPPLE_NPAD_DEVICE_TYPES=0 restores 0. make -C input test: styles 51 checks.

LAS golden route (tools/golden/routes.json)

boot-to-village was found and tuned in nine headless runs (fresh private saves, run lock). On a fresh save the opening movie (VP9 + Vorbis) plays for ~84 s and cannot be skipped (PLUS / A / B / X during it do nothing). The title then asks for L+R (the old A-only route stayed on it forever). After that: file select (New Game), difficulty (Normal), name entry (A opens the software keyboard library applet, which returns "Link"; DOWN to OK, A), and Link wakes in Tarin's house. B advances the dialogue (A would start talks and checks with whatever Link faces). Walking down, Tarin stops Link at the door; walking up 0.5 s and right 0.5 s and pressing A talks to Tarin, who hands over the shield; then left and down out of the door into Mabe Village (name banner, autosave). Frames: 5100 title, 6300 house, 9700 shield, 11200 village. 205 s (over the 120 s batch guideline: the movie alone takes 84 s). The route sets "validation": false: input is timed in seconds but Link's walking distance is per frame, and under Metal validation (~7 % slower) the 0.5 s walks stop short of Tarin, he is never talked to and the run ends at the door (seen in a golden.py run). Verified twice: a direct run (x9: shield at 9700, Mabe Village banner at 10700) and golden.py run las --route boot-to-village (PASS, exit 0, 12061 frames, no gfx warnings; frame 11200 shows Link outdoors in the village by the phone booth). Found on the way (not mine): NAPPLE_SCREENSHOT_FRAMES writes at most 20 files (gfx_glue.c), and seq with a decimal step prints commas on this Mac's locale (pt-BR), which split my first scripts.

Docs

docs/architecture/runtime-services.md: the audren:u (performance layouts), bcat:u (console delivery-cache layout, Done = 9), hid (GC trigger lifo, retro types, device types) and audin:u (auto microphone with silence fallback; decision kept) rows refreshed. docs/reference/environment-variables.md: NAPPLE_AUDIN_SOURCE default corrected (auto), rows for NAPPLE_NPAD_DEVICE_TYPES and NAPPLE_MOVIE_AAC.

Four-title regression (this branch's runtime, fresh private saves, run lock, audio device off)

SMO 100 s (exit 0, 5935 frames: Start menu -> intro), MK8D 100 s (exit 0, 5630 frames: title -> Grand Prix panel -> cup select -> race), ACNH 90 s (exit 0, 2679 frames: airport counter dialogue advancing), LAS 160-205 s in eleven route runs (exit 0 each; the opening movie's VP9 and Vorbis decode through the reworked audio path: 5100+ frames, Vorbis RMS ~5900). No crash in any run; input drives every title with the new npad device-type bits. Tests: make -C runtime test test-services rc 0 (every suite 0 failed; svc_movie_host 1015, svc_movie_audio 54, svc_guest_exit 5), make -C input test (styles 51), make -C audio test all 0 failed.

Not done / follow-ups (not in this branch's paths)

  • runtime/src/kernel.c svc_exit_process still writes a fail summary; titles on the 2-callback nn::init::Start (no finalizer callback) end there when nnMain returns.
  • runtime/src/services/audio.c OpenAudioIn comment still calls the microphone opt-in.
  • runtime/src/gfx_glue.c: NAPPLE_SCREENSHOT_FRAMES silently stops after 20 files (a log line when the cap is hit would have saved a run).
  • No test title creates an AAC / VP8 movie decoder or asks for output format 2: those stay tested (encoder-made streams), not verified on a title.