Skip to content

Fix save-state, artwork, and identity regressions from review of 680e077#7

Merged
webhead2oo9 merged 9 commits into
mainfrom
fix/review-findings
Jul 18, 2026
Merged

Fix save-state, artwork, and identity regressions from review of 680e077#7
webhead2oo9 merged 9 commits into
mainfrom
fix/review-findings

Conversation

@webhead2oo9

@webhead2oo9 webhead2oo9 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Fixes all 15 findings from a deep review of 680e077 ("Harden media loading and state restoration"), independently confirmed by a second verification pass, plus the five follow-up findings from review of this PR. The original concerns remain split by commit, with one final hardening commit for exact decoder restoration and tooling regressions.

User-visible regressions fixed

  • Save states from unknown- or under-reported-length tracks were unloadable or resumed at the wrong audio (audio.c): playback now preserves the true source cursor beyond unreliable length metadata. Restore uses checked native seeks when possible and replays source PCM from the freshly opened decoder when FLAC/Vorbis seek tables cannot represent the saved position.
  • A failed load-state could silence healthy playback (core.c): retro_unserialize validates the incoming audio snapshot before touching the live decoder, so pure validation failures can no longer cascade into audio_close() on a playing session.
  • Sidecar album art stopped loading for ANSI Windows paths (image_codecs.c/metadata.c): art now opens through path_fopen_read (wide attempt, narrow fallback), matching audio path handling.
  • Core identity reverted to UltiMedia UGC / 17.0: frontends key per-core settings, remaps, and save-state folders off library_name, so the rename to Music Playlist Core orphaned existing users' data with no migration. The .info display fields now follow the code.

Tooling fixes

  • CoreStateSnapshot is shared with the harness via src/core_state.h instead of a hand-mirrored struct whose corruption offsets could silently drift.
  • Harness argv is converted ANSI to UTF-8 on Windows for fixture directories containing non-ASCII characters.
  • CFLAGS parsing preserves raw Windows paths such as -IC:\dev\extra without breaking normal escaped quotes, escaped spaces, or # characters.
  • Header-only path helpers are static inline, so -Wall -Wextra -Werror builds no longer fail on unused definitions.
  • The identity test both checks music_playlist_libretro.info consistency and pins the stable UltiMedia UGC library name.

Cleanups

  • Single unload_session() teardown shared by retro_unload_game and every retro_load_game bail-out.
  • Wide-open fallback and path splitting are centralized in path_io.h, including mixed-separator handling.
  • Redundant stbi_info pre-validation removed; ART_MAX_DIMENSION drives STBI_MAX_DIMENSIONS.
  • Magic values named: GLYPH_WIDTH, SEEK_ICON_FRAMES.
  • CLAUDE.md, README.md, and CONTRIBUTING.md document the harness knobs and small codec regression seeds.

Verification

  • python3 tests/run_tests.py: 23/23 passed.
  • CFLAGS="-Wall -Wextra -Werror" python3 tests/run_tests.py: 23/23 passed.
  • Exact next-frame continuity verified for zero-total FLAC and under-reported OGG save-state restoration.
  • Malformed decoder-position overflow is rejected without disturbing playback.
  • Windows x86-64 DLL builds cleanly with all required LibRetro exports.
  • Manual EmuVR FLAC playback was confirmed; a complete tests/SMOKE_CHECKLIST.md pass is still outstanding.

🤖 Generated with Claude Code

webhead2oo9 and others added 9 commits July 10, 2026 11:00
Snapshot validation rejected cur_frame > total_frames unconditionally,
which made every state saved from a track whose decoder reports an
unknown length (total_frames == 0: FLAC without a STREAMINFO total, OGG
length-probe failure) unloadable. Restore additionally rejected
resume_frame > total_frames, refusing states saved after playback ran
past an under-reported track length (VBR MP3 estimates) where the
resample cache legitimately overshoots the reported total.

Treat total_frames == 0 as unknown and skip the positional bound, and
drop the resume-frame bound (cur_frame is already bounded for known
lengths, and the cache overshoot is real decoder state).

retro_unserialize also assumed any audio_restore_state failure had
disturbed the decoder, so a state that merely failed validation could
cascade into audio_close() on a healthy, still-playing session when the
rollback snapshot failed the same validation. Expose the validation as
audio_snapshot_valid() and reject bad snapshots up front, before the
live decoder is touched; rollback teardown now only happens when a
restore attempt actually disturbed playback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

STBI_WINDOWS_UTF8 made stb_image convert art paths as strict UTF-8 with
no fallback to plain fopen, so sidecar artwork silently stopped loading
for frontends that pass ANSI/locally-encoded paths on Windows -- while
the audio side of the same path still opened via path_fopen_read, which
deliberately keeps that fallback. Open art files through path_fopen_read
and decode with stbi_load_from_file so artwork follows the same policy.

The stbi_info pre-validation pass duplicated the STBI_MAX_DIMENSIONS
limit compiled into stb_image and header-parsed every image twice (per
signature hit during the embedded-art scan). Drop it, and derive
STBI_MAX_DIMENSIONS from ART_MAX_DIMENSION (now in metadata.h) so the
limit lives in one place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The convert-UTF-8-to-wide, try-the-_w-API, fall-back-to-narrow
boilerplate was copy-pasted four times across audio.c and metadata.c,
so a change to the fallback policy had to be replicated in every codec
open. Replace the copies with a single PATH_OPEN_WIDE_THEN_NARROW
helper next to path_fopen_read (which now uses it too).

The bounded find-last-separator, clamp, copy, terminate directory split
was likewise duplicated between retro_load_game and metadata_load; both
now share path_dirname/path_basename. The shared helpers treat the
later of '/' and '\' as the separator, which also fixes splitting of
mixed-separator paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The audio/art/playlist teardown sequence existed in three copies across
retro_load_game and retro_unload_game, and the copies had drifted: the
all-tracks-failed path skipped reset_runtime_state, leaving the shuffle
order generated for the failed playlist behind with track_count at 0.
Factor a single unload_session() used by retro_unload_game and every
retro_load_game bail-out.

The 8-pixel glyph width lived as a bare literal in four text-measuring
sites (plus the font renderer itself) and the 15-frame seek-icon timer
in three; name them GLYPH_WIDTH and SEEK_ICON_FRAMES so a font or timer
change cannot silently desync the copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…angling

Share CoreStateSnapshot with the harness through a new core_state.h
instead of hand-mirroring the struct (and CORE_MAX_TRACKS) in
core_harness.c, where any future field edit would silently move the
malformed-save-state corruption offsets while CI stayed green. This
matches how AudioStateSnapshot is already shared via audio.h.

Convert the fixtures-dir argument from the ANSI code page to UTF-8 on
Windows: the core treats paths as UTF-8, so a machine whose temp path
contains non-ASCII characters produced mixed-encoding track paths and a
spurious suite failure (reproduced from a directory containing a
u-umlaut before the fix; passes after).

Parse CFLAGS with escape processing disabled: posix-mode shlex.split
was eating backslashes, turning -IC:\dev\extra into -IC:devextra and
silently dropping the documented instrumentation hook on Windows.

Make core_identity_matches_info read display_name/display_version from
music_playlist_libretro.info instead of re-asserting hardcoded string
literals, so the code/.info sync it is named for is actually enforced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CONTRIBUTING.md and README.md already describe the CFLAGS
instrumentation hook, but CLAUDE.md was left out, violating the
stated rule that build/test convention changes update both docs.
Also list core_state.h and the path-splitting helpers added to
path_io.h in the source layout table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frontends key per-core option files, remaps, and save-state folders off
library_name, so this morning's rename to Music Playlist Core / 1.0
orphaned existing users' settings and made their save states invisible,
with no migration. Restore the identity users' data is keyed to.

The .info display fields (which had said Music Playlist Core since the
file was added, mismatching the core) follow the code instead, keeping
core_identity_matches_info green and the identity coherent everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Preserve true playback positions past unreliable length metadata, replay unknown-length decoders to exact save-state positions, and reject failed seeks without falsifying runtime state. Add FLAC/Vorbis continuity fixtures and close the CFLAGS, strict-build, and identity-test review findings.
@webhead2oo9
webhead2oo9 merged commit 22df847 into main Jul 18, 2026
12 checks passed
@webhead2oo9
webhead2oo9 deleted the fix/review-findings branch July 18, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant