Files
Joseph MattielloandClaude Opus 5 ed06c6998d feat(options): split Timing into Speed and Hardware Timing; add overclock gate-1 tooling
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]>
2026-08-25 22:29:38 -04:00

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))