mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
The Timing category held three different intents at once: two overclocks that change what the game computes, two fast-forwards that change only how much work the host does, and three accuracy models that deliberately make things slower. One flat list gave a user no way to tell "this makes it faster" from "this makes it slower on purpose". Split into Speed and Hardware Timing (Experimental), and order Speed by what to try first: the two free bit-exact fast-forwards, then the two overclocks that can break a game. blit_memo moves here from Video -- it is the same free-speedup class, and it silently disables idle-skip, which was undiscoverable from a different category. Option keys are unchanged, so no user config breaks. Also corrects the M68K text, which cited AvP and Checkered Flag as games overclocking can smooth. Both were measured under #378 and both failed: AvP is locked to one frame per 5 fields, Checkered Flag caps itself in software. Adds scripts/ocgrid.sh (gate-1 grid), scripts/rzip_extract.py (RetroArch compressed states are otherwise unreadable by the harness) and the #378 checklist, which records the first title to clear gate 1: Cybermorph, +44.7% at RISC 1.5x and +72.8% at 2x, with an underclock control proving the metric live. Refs #378 Co-Authored-By: Claude Opus 5 <[email protected]>
65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract a RetroArch rzip-compressed save state to a raw one.
|
|
|
|
RetroArch writes save states through rzip_stream when "Save State Compression"
|
|
is on, which our test harnesses cannot read -- retro_unserialize sees the
|
|
compressed bytes and rejects them on size. This converts such a file into the
|
|
plain state blob the harness expects.
|
|
|
|
Layout (little-endian), confirmed against RetroArch's rzip_stream.c:
|
|
|
|
0 6 magic "#RZIPv"
|
|
6 1 format version
|
|
7 1 '#' terminator
|
|
8 4 chunk size (uncompressed bytes per chunk)
|
|
12 8 total uncompressed size
|
|
20 4 length of the first compressed chunk
|
|
24 .. zlib chunk, then [u32 length][zlib chunk] repeating
|
|
|
|
A zero length terminates the stream. Files without the magic are already raw
|
|
and are copied through unchanged, so this is safe to run over a whole folder.
|
|
|
|
usage: rzip_extract.py <in.state> [out.state]
|
|
"""
|
|
import sys, os, zlib, struct
|
|
|
|
MAGIC = b'#RZIPv'
|
|
|
|
|
|
def extract(src, dst):
|
|
with open(src, 'rb') as f:
|
|
blob = f.read()
|
|
|
|
if not blob.startswith(MAGIC):
|
|
with open(dst, 'wb') as f:
|
|
f.write(blob)
|
|
return len(blob), 'already raw'
|
|
|
|
chunk_size, total = struct.unpack_from('<IQ', blob, 8)
|
|
out, pos = bytearray(), 20
|
|
while pos + 4 <= len(blob):
|
|
(clen,) = struct.unpack_from('<I', blob, pos)
|
|
pos += 4
|
|
if clen == 0:
|
|
break
|
|
out += zlib.decompress(blob[pos:pos + clen])
|
|
pos += clen
|
|
|
|
if len(out) != total:
|
|
raise SystemExit('rzip_extract: got %d bytes, header declares %d '
|
|
'(chunk size %d) -- format mismatch, refusing to write'
|
|
% (len(out), total, chunk_size))
|
|
|
|
with open(dst, 'wb') as f:
|
|
f.write(out)
|
|
return len(out), 'decompressed'
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit(__doc__)
|
|
inp = sys.argv[1]
|
|
outp = sys.argv[2] if len(sys.argv) > 2 else os.path.splitext(inp)[0] + '.raw.state'
|
|
n, how = extract(inp, outp)
|
|
print('%s: %s -> %s (%d bytes)' % (os.path.basename(inp), how, outp, n))
|