3 Commits
Author SHA1 Message Date
LibretroAdmin 581abd98e0 serialize: no function addresses in the save state
register_serialize() wrote data32 out for every register. For one that
is read through a function that field is the low half of the function's
address, which is not state - register_unserialize() reads it into a
dummy - and is different in every process. About 1,300 of them went into
each state, so two states of the same machine at the same moment were
never the same file.

A register read through a function is now written as zero. The format
does not change and older states load as before.

With this and the read-only registers no longer holding an address
either, two runs of the same content for the same number of frames
leave states that differ in the real-time clock and nothing else: 4
places instead of 2,500 bytes. tools/threads/live_same.py now requires
that - more than a few bytes of difference between two identical runs
fails - and still compares the threaded state with the non-threaded one
everywhere but the clock.
2026-10-06 01:29:26 +02:00
LibretroAdmin 93c8432cfb tools/threads: compare save states byte by byte
live_same.py left out of its comparison every 8-byte word that differed
between two non-threaded runs, taking it to hold a host address. The
addresses in a save state are not aligned, though, and two runs need not
differ in every byte of one. A word holding the low byte of an address
could match in the two non-threaded runs and differ in the threaded one,
and the test then failed on a state that was the same: seen once on the
Vulkan run, 28 words in the block of pointers.

It now works on bytes: any byte within seven of one that differs between
the two non-threaded runs belongs to an address and is left out.
2026-10-05 17:14:27 +02:00
LibretroAdmin 4bd929c953 threaded rendering: make one frame per retro_run, the one non-threaded makes
With Threaded Rendering on, nothing tied the emulation to retro_run. The
emulation thread ran free, held back only by the audio it pushed to the
frontend itself, 512 samples at a time, from its own thread, whenever
that call blocked. retro_run waited up to 100 ms for whatever render
turned up and returned a duplicate frame if none did. A render the game
started while the last one was still being drawn was thrown away. So a
retro_run could last any length, carry a new frame or not, and its audio
arrived some other time from some other thread.

Each retro_run now makes exactly one frame, the same one it makes with
Threaded Rendering off:

  - retro_run wakes the emulation thread, which runs the machine up to
    vblank and goes back to sleep, as dc_run() does in retro_run itself
    when rendering is not threaded;
  - when the game starts a render the emulation thread hands it to the
    libretro thread and sleeps; that thread reads what the frame takes
    from the game's memory (display lists, textures, framebuffer), lets
    the emulation thread go on, and draws. A render to a texture, which
    ends up back in the game's memory, keeps it asleep until it is done;
  - every render is drawn. The emulation thread waits for the one before
    to be finished; none is dropped;
  - at vblank the frame's audio is sent from the libretro thread in one
    batch, together with its video.

The renderers read some PVR registers while they draw, by which time the
emulation has moved on and may have changed them. Each render now
carries a copy of the registers made when the game started it, and the
renderers' files read through that (PVR_REGS_FOR_RENDERER), so they draw
from what non-threaded rendering would have read.

EmuBaton (core/libretro/emu_baton.h) is that handshake. There is no
lock, no sleep and no timeout in it: every change of hands is one atomic
operation and a thread with nothing to do sleeps on an eventcount. A
save state from the frontend's task thread takes the machine between
two frames.

The "synchronous rendering" and "delay frame swapping" options have
nothing left to choose and are hidden.

tools/threads/live.sh runs the test disc for the same 300 frames with
Threaded Rendering on and off and requires the same screenshot, byte
for byte, and the same save state (live_same.py leaves out the host
addresses a state carries). The old threaded path fails that: its state
differs from the non-threaded one. tools/threads/run.sh puts the new
EmuBaton under ThreadSanitizer.
2026-10-05 13:39:02 +02:00