A game waiting on the beam reads SPG_STATUS hundreds of times a line,
and each read found the next line due and divided twice to place the
beam. spg_beam() now keeps what it found - the line, the pending
event's end, the cycles that bound the line - and answers a read on the
same line, aimed by the same event, with a subtraction. Aiming the
event, a timing change or a state loaded starts it afresh.
The headless disc runs 3000 frames in 11.5 s, from 17.8 s.
tools/pvr/spg_beam_test.sh makes every read twice, kept and afresh,
straight and with every frame run ahead, and fails where they differ.
GD-ROM sets that come with the network DIMM board now have it answer, as
firmware 3.17 with its network settings, instead of seeing no board at
all: games that check the board's version at start-up (error 14 below
3.17) boot. The network commands are still refused, and those games go on
after their network check times out. What the plain and the network board
leave in the NAOMI's memory is in naomi_dimm.c, C89, with a regression
lane in tools/dimm. The network board's part of the save state is a new
V31 part; states from before it load with the board silent until reset.
A state saved or loaded copied the Dreamcast's SRAM, which it does not
have: no memory and a size of 0, a copy from or to a null pointer
(UBSan). Parts of no size are now nothing to copy.
headless.sh's run-ahead stage passes: its do-nothing OpenGL read back
from the mapping the core writes through, so what it fetched off the
card depended on the core's history, and frames run from a state loaded
were not the frames run straight. Readbacks are now the same bytes every
time. headless.sh runs in CI (Headless).
gl_frames runs frames ahead (GLF_AHEAD), and tools/gl/gl_ahead.sh runs a
disc (live_disc.py --keep) that renders to a texture at the very end of
a frame and only draws with it after: every frame run ahead has to show
what a plain run shows, the picture saved in the state while still on
the card. On OpenGL and OpenGL ES, and under ASan and UBSan in CI.
A state is what netplay compares between two machines (a hash of it
every so often: a difference and the state is sent again) and what
run-ahead and rollback take back. Two things in it were not the
machine's:
- The byte left for what used to be NaomiDataRead was skipped, not
written: it was whatever the frontend's memory had there. It is a zero
now (tools/determinism found it: the state saved into memory full of
0x00 and into memory full of 0xff came out one byte different).
- The DSP's code as the recompiler made it, 32 KB of host instructions,
was written out with the DSP's registers. It is not the same on an ARM
host, nor without the recompiler, nor where the core was loaded at
another address (it has addresses in it). A load has always skipped
it; zeros are written in its place.
The state's layout is as it was: states from before load as they did.
A frontend (detcheck.c) that runs a game from a state with inputs of its
own making - the same each time, a function of the frame alone - and
writes down after every frame the state, part by part in units of 64 KB
at most, with the hashes of the frame's sound and picture. detcheck.sh
runs the game that way five times and holds each run against the first:
again as it was; with a state saved and loaded straight back every K
frames; as run-ahead runs it (each frame run, saved, the next run ahead,
the state loaded, and that frame run again, which has to be what it was
ahead); and as netplay rolls back (every D frames back D frames and
those run again). Every 50 frames the state is also saved into memory
full of 0x00 and into memory full of 0xff: a byte that comes out
different is one the core does not write. At the first frame that is not
the same, the parts of the state that differ are named, with the bytes.
A trace made on another machine or by another build (a core for ARM
under qemu, a 32-bit one, one without recompilers) is held against this
one's with REF=, or with detcheck --compare at any time.
It runs with an OpenGL that does nothing (nogl.c and stubs written for
every gl function the core calls) or, with GL=egl, the system's through
EGL with no display, and then the pictures are read back and hashed.
Plain C, no Python.
To name the parts, dc_serialize() keeps where each one began in the
state it wrote or counted last, with its name - a field written there by
name, or the function that writes a part of its own - in a table nothing
in the core reads. detcheck finds it by its symbol (from nm, against
retro_init's address). Two stores a part, a few hundred parts a state.
The state is not changed by it.
What the new CI targets tripped over:
- Apple's headers define nil: the unused bitfields named nil in aica.h
and modem_regs.h are unnamed now, and the GD-ROM's ByteCount register
is GDByteCount (the name clashed there too)
- arm64 macOS/iOS/tvOS: the signal context's registers are __ss.__pc
and __ss.__x[], and the instruction cache is flushed with
sys_icache_invalidate
- an ARM build without a recompiler (Apple arm64) still needs the
interpreter's do_sqw_nommu_area_3, and bm_RamWriteAccess only exists
with a recompiler
- 32-bit x86: the prepass alpha static_assert compares both sides as
float (x87 otherwise compares the literal at its wider precision)
A card reader keeps the card out of the reader in memory, read from its
file when the machine starts, and the game's writes to it change that
copy; the IC card readers of VF4 and Mushiking the same with both cards.
None of it was in the state: a state loaded after the game had written a
card (run-ahead does that every frame) left the newer card, and the next
one put in was not the card the state had. Both are in the state from
V30: a byte saying which readers there are, then 208 bytes with a card
reader, 256 with the IC card readers. An older state keeps the cards in
memory as they are, as before. The reader's copy is zeroed before the
file is read into it, so a short file leaves no stray bytes.
Since SB_ADST is read through a function (the busy-once read for Le Mans
and Looney Tunes), its RegisterStruct holds the read function's address
where a plain register keeps its value, the two sharing a union. SB_ADST
was still that slot's data32, so the first write to it - a DMA start, its
end, a state load's check - overwrote the low half of the function's
address, and the next read jumped there: Jet Grind Radio crashed on boot
and on loading a state, at an address ending in 00000001.
The value now lives in its own variable (sb_adst, aica_if.cpp, which
SB_ADST names), cleared by a hard reset. The register block left it out
of the state since that change; it goes in from V29 (4 bytes). An older
state marks a transfer under way, and the check after the scheduler is
back in place keeps that only when its end is on its way.
- A vertex with x or y over 1e25 is dropped as upstream (Manic Panic
Ghosts); one with a negative z is kept and clipped by the GPU (World
Series Baseball 2K1). The test is on the bits, so fast-math keeps it.
- Punch-through polygons are tested on the shaded alpha (texture x
vertex colour), not the texel's, in GL, GL4, Vulkan and Vulkan OIT
(Langrisser Millennium), as upstream.
- Per-strip sorting: sorted translucent polygons write no depth there
either (Sonic Adventure, which is forced to it, and LOW_END builds).
- A TA context unused for 60 renders is reused, as upstream; counted per
render started, so frame skip does not change it, and never one of
another frame size.
- NAOMI 2: the render-end interrupt always 1,500,000 cycles on.
- Light guns: at the gun's line and at vblank-in only SB_MDST is
cleared; a transfer the game started ends at vblank-out with its answer
and interrupt (RevolveR), one vblank started gets none (Virtua Cop 2).
Ninja Assault, Mazan and Gun Survivor get the raw 640x480 position.
The gun's X follows the widescreen hack when no widescreen cheat is
on. A "Pointer" controller type aims and fires with one touch and
reloads with two; the left stick aims on a gamepad in a gun game.
- Render to texture with a clip whose minimum is above 0: the target
starts at the frame's pixel 0 and the clip is the scissor, in GL and
Vulkan; only the clip goes back to VRAM, at its place, dithered by the
pixel's place in the frame. Pixels past max - min were lost, and VRAM
below the minimum was overwritten.
Save state V28: the gun's transfer owed its end, and the TA contexts'
ages (17 bytes). An older state gets ages 0.
The JVS I/O boards upstream has and the fork lacked:
- the 837-13844 racing board of F355 Challenge and Tokyo Bus Guide,
which answers for the drive board on P1/P2 BTN2-5 (Tokyo Bus sets its
power through its spring commands);
- 18 Wheeler's: the racing board and its shifter (SHIFT L/H toggles low
and high, SHIFT R is reverse; the gear on P2 UP/DOWN/LEFT), the wheel
kept within 0x1000-0xefff;
- Samba de Amigo's maraca sensors, from the analog sticks as upstream;
- Shootout Pool's cue roller (while CUE ROLLER is held, up/down moves
the roller and the aim stays where it is), Kick '4' Cash's and
Crackin'DJ's encoder boards (its turntable motor turns them).
They are chosen by game ID. Lamps are dropped; the drive board's force
goes to rumble. A board's own wiring applies after the game's button
mapping, as upstream; JVS command 0x33 (analog output) no longer goes
to the switched outputs. F355's and 18 Wheeler's input descriptors are
upstream's, for these boards (F355's assists on the directions, gears
on P2; the Deluxe clutch has no second pair of triggers to go on).
MIDI force feedback (midiffb.cpp): the steering board of Initial D (1,
2, 3, Cycraft), The King of Route 66 and Club Kart, on the AICA's MIDI
out and in. Its replies come in at the sound chip's next step, and its
input holds 64 bytes. The option "NAOMI Wheel Force Feedback"
(reicast_naomi_wheel_ffb, off by default) turns the wheel's pull and
shake into rumble on port 1, handed to the frontend from one atomic word
when it changes.
F355 Challenge's printer on the G2 bus: its status reads 0x0f, bit 0 set
after each read and cleared when the game writes it, as upstream; what
it prints is dropped. These addresses read 0 before.
BIOSes naomi-ko-h and the two multi-region ones (epr-21576h_multi,
epr-23605c_multi), which take their region from motherboard DIP
switches: with one of those loaded the JVS reply sets them to Korea
(0xf9, 0x08 at 15 kHz); every other BIOS sees what it saw.
Save state V27: the boards' outputs, power and gear (12 bytes), the MIDI
board (79 bytes on its games) and the printer's status (1 byte on a
NAOMI). An older state gets them idle.
The board and cartridge serial IDs were reicast's stand-in: a fixed
BoardID and two made-up serial strings shifted out by a state machine,
which the BIOS took no usable serial from. Both are now X76F100 serial
flash chips as on the hardware and upstream: the main board's on BSC
port A (bit 5 chip select, bit 4 reset, bit 2 clock, bit 3 data; a read
has bit 0 set, which tells the BIOS the chip is an X76F100), the ROM
board's at 5f7078/5f707c (bit 2 chip select, bit 3 reset, bit 1 clock,
bit 0 data, read back as bit 15). A hard reset leaves them idle with
their contents.
They hold upstream's default (MAME's x76f100_eeprom.bin), the ROM
board's chip the set's own where it has one: alpilot airlinepdx.sf,
doa2/doa2a/doa2m 841-0003.sf, tduno x76f100.ic37, now in the table with
MAME's checksums. A set is still found without that file. The chip
class (core/hw/flashrom/x76f100.cpp) is the one the GD-ROM DIMM board
had, moved out; that board now reads the ROM board's chip, as upstream,
with the same bytes as before. Atomiswave and Dreamcast are untouched.
The old part of the state keeps its format (the stand-in's bytes are
written as zeros and ignored). Save state V26: the two chips, 284 bytes
on a NAOMI (142 where the DIMM board's part already has the ROM
board's), their data and passwords included since a game can write
them. An older state gets both idle with the set's contents.
The board a NAOMI GD-ROM game's disc is loaded into answered nothing
(0xffff, as MAME without its firmware), and Ikaruga, Tetris
Kiwamemichi, Puyo Puyo Fever and Sports Jam stopped at boot with error
04. Its registers now answer as upstream's: at start-up the board
writes its configuration, firmware version and serial number into the
game's memory, sets its base address and raises EXP_PCI; STATUS reads
0x111, bit 8 clear while the interrupt is pending; once a second it
answers the BIOS test menu's DIMM check; the network commands fail.
The game compares the DIMM's serial number with the ROM board's, read
through 5f7078/5f707c: a GD cart with a DIMM board has an X76F100 there
now, with upstream's default contents, so the two are the same. ROM
carts keep their serial as it was. Unlike upstream's chip, a password
change copies the buffer into the password, and a write after the
password starts at the buffer's first byte, not past its end.
Every GD set but vf4*, mj1 and wccf* (upstream's network DIMM) gets the
board; those keep today's answers. For the others 5f703c reads the
command register (was 0xffff), 5f704c 0x111/0x011 (was 0x11), and the
board writes its RAM and interrupts once a second.
Save state V25: the board, its poll and the X76F100 (151 bytes, on those
sets only). An older state has no board until the next hard reset.
- A System SP has no JVS I/O board or controller (MAME's segasp has
none, nor does upstream); its inputs are read from the same per-frame
input words. The Insert Card button is read when it reads IN_PORT0.
- The Hirocon IC card reader of dinokich and loveber3cn (dinokich.cpp,
dinokich_card.cpp), on UART1 with its two slots. Cards are 1 KB, kept
as <game>-<region>.rfid1 and .rfid2 and written when the game writes
them, including its check value. A command shorter than it needs is
refused, and what comes in and goes out has a fixed room.
- Mushiking's I/O board (837-13551 with the card dispenser): the
dispenser reported ready unless TEST is down, P1 RIGHT and P2 START
clear, TEST seen one read late, by the game IDs upstream uses.
- Touch de Uno 1 and 2: the touch board reports the raw 640x480 position,
ORed with 0xf000 while the screen is touched; it sent Ninja Assault's
scaled one, which a touch never reached. Touch de Uno 2 and Mirai Yosou
Studio get the 837-13844 touch board (MAME and upstream agree), and both
Touch de Uno get upstream's calibrated default EEPROM where there is no
file yet.
- anpanman2 gets its 837-13844 board.
Save state V25: the dinokich reader's cards and messages (2828 bytes, on
those two games only) and a byte per I/O board for Mushiking's late TEST.
The modem:
- V.42 error correction (LAPM) and V.42bis compression for the games
that dial in sync mode, which were forced to async, and V.8bis and
56K (V.90/K56 speeds).
- Receive and transmit FIFOs timed by the line's speeds, the TXFNF/TXHF
status bits, and Power Smash's symmetric speed.
- PPP waits for the game to speak first, on the network thread; this
replaces the quarter-second delay on reads.
- Its ID reports the country as USA, as upstream's.
- Frames short or long, an XID with bad lengths, a REJ for a frame that
is gone, a V.8bis message without its end, a V.42bis step-up of a
dictionary not a power of two: none can read or write past a buffer,
loop for ever or be refused wrongly any more.
The Broadband Adapter could not be turned on: settings forced it off
and nothing set it. The option "Expansion Port Device (Restart
Required)" (reicast_network_device) chooses the modem, the adapter or
nothing; the modem by default, as before. The adapter gets its
link-change interrupt 10 ms after a reset, as KallistiOS waits for, frames
under 6 or over 1514 bytes are dropped, and the network starts with the
game's first frame and stops on a GAPS reset.
DCNet: "Use DCNet" (reicast_dcnet, off by default) goes online through
the dcnet.flyca.st relay instead of picoTCP, on the same network thread,
non-blocking, without locks. Power Smash and Pro Yakyuu always go
through it, as upstream has them.
picoTCP: dns.flyca.st as the name server; a socket that is ready is read
whole (an upload no longer stalls); the ports of Daytona, Sega Tetris,
Golf Shiyouyo 2, Dee Dee Planet, Driving Strikers, Floigan and the
Internet Game Pack. Stopping the network thread no longer waits for it.
In the save state (V24), on a Dreamcast: the device chosen and what the
modem (FIFOs, V.42, V.42bis, V.8bis) and the adapter (GAPS, its RAM, the
RTL8139) hold, 34 KB. A live connection cannot be in a state: one loaded
replays what came over the line at the times it came, and a line the
state does not have hangs up, without waiting for the thread.
tools/threads/pico_test.cpp tests the thread with the new interface,
and DCNet against a stand-in relay.
The board after the NAOMI: its BIOS (segasp) and memory map (the G2
area at 0x01xxxxxx is the board's; the flash window at 0x00200000 has
nothing on it), the 128 KB SRAM, the 93C46 EEPROM, the M4-type cartridge
and its cipher, the CompactFlash games read from <set>/<media>.chd,
the RFID card readers, the touchscreen, the medal and hopper sensors and
motors, and the network board's sockets. It runs as a NAOMI that is an
SP (naomi_cart_systemsp).
Kept in the save directory beside the NAOMI's own files: the EEPROM as
<game>-jp|us|exp.eeprom, RFID cards as <game>-<region>.rfid1 and
.rfid2, and what the BIOS installs from a CompactFlash card as
<game>.flash, so it installs once.
Fixed on the way: a multi-sector ATA read carries sector into cylinder
into head; an RFID write past the card's 128 bytes, an 8.3 name with no
dot, and flash writes and erases out of range no longer reach outside
their buffers; a sector erase clears the 128 KB sector the flash's own
table gives.
The sets, 28: brickppl, tetgiant, tetgiano, huhimage, galilfac,
monomedal; kingyo, kingyoch, manpuku, shateki (hopper); magicpop,
ochaken, puyomedal, unomedal, westdrmg (medal pusher, unfinished);
dinoking, dinokior, dinoki25, dinoki4, lovebery, lovebero, loveber3
(RFID readers); isshoni (touchscreen, on port 1's light gun); btlracer;
bingogal, bingogals (one board of the two). dinokich and loveber3cn need
an IC card reader not emulated.
In the save state (V24), on an SP only: the board's registers, sector
buffer, EEPROM, card and sensor state, a random-number state for new
cards, and the network board's addresses and the pages of its memory
written so far, 35 KB.
The NAOMI's arcade peripherals:
- The SCIF serial port gets its FIFOs, status and interrupts while a
device is plugged into it; with none it is the stub it was, and its
scheduler event runs only while something is on its way.
- Card readers (card_reader.cpp): Initial D and Club Kart on the SCIF,
Derby Owners Club (both kinds) on the I/O board's RS-422 commands, the
barcode reader (the barcode is the first line of <game>.barcode), and
the IC card readers of VF4 and Mushiking on maple buses 2 and 3 (a new
device type, numbered after the others). Cards are kept in the save
directory: <game>.card, <game>-p1.card, <game>-p2.card. With a reader,
L is "Insert Card". The option "NAOMI Card Readers"
(reicast_naomi_card_readers) turns them on; off by default.
- The touchscreen of Manic Panic Ghosts, Pokasuka Ghost and Touch de
Zunou, from where each player's light gun points.
- The hopper of the Club Kart Prize, Shootout Pool and Kick '4' Cash
sets; its settings and credits in <game>-hopper.bin.
- The printer of Touch de Uno 1 and 2 and Mirai Yosou Studio: answered,
what it prints dropped.
A message from the guest longer or shorter than the device takes is
dropped where it could read or write past its buffer.
New sets that need them: mushikk, mushi2k3, mushik4t, mushi2k61,
mushi2k62, drbyocwt.
In the save state (V24): which devices the game has and what each holds,
nothing on a Dreamcast or Atomiswave, 1 byte on a NAOMI without any.
A game can make a VMU beep, through the clock function's SetCondition:
the period and the time on, the tone 1 MHz over the period (Chao in
Sonic Adventure, alarms in Skies of Arcadia and others). It is played
from a short recording of a VMU's beep, faded in and out over 128 samples,
and added to the sound at the point the libretro layer sends a frame's
samples: a test per frame, nothing per sample while no VMU beeps, and the
console's master volume does not apply, the VMU having its own speaker.
Samples held back for the next frame get it when they are sent, so the
held-back sound in a save state never has it in.
The option "VMU Sounds" (reicast_vmu_sound) turns it on; off by default.
A device reset or the VMU's removal silences it, as does a reset of the
core.
The save state goes to V24 for it (13 bytes); an older state is silent.
- Area 0's mirror at 0x02000000 has no boot ROM and no flash: reads
give 0 and writes are ignored (upstream 6af509159; MAME maps every
area 0 device with that mirror except these two).
- HLE GDCC_PLAY and PLAY_SECTOR keep four bits of the repeat count, as
the drive does: more spilled into REQ_STAT's disc format nibble.
- A G2 (sound) DMA start written while a transfer is under way is not
taken (MAME's dc_g2if does the same).
- serialize: values a bad state could make harmful are made what the
hardware can have, with no change for a valid state: AICA timer
steps (a 0 froze a timer), the CD audio repeat count, scheduler event
tags (constants of this build, not state: one was used as an index),
ITLB_LRU_USE (a table worked out at start, not state), events asked
for more than a second ahead or already past (made due now), and a
sound DMA marked busy with no end on its way.
- tools/chd/run.sh links again (archive.c needs the rar and thread
sources).
- NAOMI: the comm board's registers and its two 128 KB memories, and the
64 KB I/O board program on its way (Crazy Taxi and HOTD2 boards lost
their mode when a state was loaded mid-upload). The upload frame is
bounded by its length.
- NAOMI 2: the geometry processor's near and far planes.
- libretro: mouse motion not yet read and the samples held back at the
end of a frame, which a load dropped (a gap of up to a tick).
States before V23 load as before.
A load copied video memory over and had every texture deleted, to be
made again from what were usually the same bytes: with run-ahead, every
texture on screen converted and uploaded again every frame.
vram_load() reads video memory from the state. A page that textures were
made from - a write-protected one - is compared with the state's: the
same, it stays as it is and so do its textures; another, it is written
as the game would have written it, which is what has those textures
made again. The other pages are copied as before. (A copy over a
protected page used to fault once a page, some 200 times a load.)
Textures that are checked by a hash of their memory each time, and the
palettes, find out for themselves as they always have.
What a game rendered to a texture and has not been fetched into video
memory yet (rtt_watch) is not in a state. When a load drops it, the
texture that shows it is made again from video memory, which is what
the machine has there: rtt_watch_forget() marks its pages written.
The custom texture loader was stopped before every load, because the
textures it was working on were about to be deleted. They are not any
more, and it is left running. (With custom textures on, every load
stopped its thread, forgot the list of files, and started over.)
A state that stops short of video memory has the textures made again,
as before.
With every frame run ahead, per frame shown - the frame, a save, the
next frame and a load - on the same machine, before this series and
with all of it:
Dead or Alive 2, a fight 127.8 + 7.8 + 13.9 + 22.5 = 172 ms
5.0 + 7.9 + 5.2 + 8.1 = 26 ms
Sega Rally 2, attract mode 66.2 + 5.4 + 26.6 + 16.7 = 115 ms
3.2 + 5.6 + 3.4 + 5.3 = 17 ms
What is left of a save and a load is one copy of the machine's memory
each, 59 megabytes on a NAOMI and 26 on a Dreamcast.
The DSP's program is compiled when it may have changed, which is every
time a state is loaded and every time a game writes to it, whether or
not what is written is what was there.
The x86-64 and ARM64 recompilers keep the program their code was made
from, and make nothing when asked again for the same.
The ARM64 one keeps its code in the DSP's structure, which is in a save
state whole: a load put the state's code there - another program's, with
that program's addresses in it - and it was only ever right because it
was compiled again at once. A load now leaves the first 32K of the
structure, the code, as they are, and reads the rest. (The state's size
and layout do not change.)
Before every load the sound processor's recompiler threw away all it
had compiled, and to do so set its two million entry points again, 16
megabytes of them: 2.4 ms a load, and then the sound driver compiled
again block by block.
Both recompilers (arm7_rec.cpp for x86-64, the older one in arm7.cpp
for ARM and ARM64) now note each block they make: where it starts and
how many words of the program it was made from.
Emptying the cache sets back the entry points that were given out, a
few hundred, and all of them only the first time.
On a load the state's sound memory is looked at before it is read
(unserialize_aram()): if it has the same words wherever a block was
compiled from, nothing is thrown away. With run-ahead that is every
time. A state with another program, or one that stops short of sound
memory, empties the cache as before.
For a Windows CE game the core keeps every translation the game has
given until the game takes it back - Sega Rally 2 has about 500 - where
the machine's TLB holds 64. A save state had the 64. After a load the
others were found again one by one: read out of Windows CE's page
tables by the core, which writes a TLB entry and PTEH as it does, or
asked of the game's own handler, which is the game running code it
would not have run. A game loaded from a state went another way from
the same game left alone, and a frontend that runs frames ahead had it
do so every frame: Sega Rally 2's stack differed from the plain run's
53 frames after run-ahead began, and it started 87 ms earlier.
V22 adds them (fastmmu.cpp): a count and three words each, in the order
they are looked through, so that a load puts back exactly the table
that was saved - where two answer for one address, the first found is
the answer. Room is taken for a full table, 786K; a game writes a few
kilobytes of it. Only for Windows CE games.
And a byte for every machine: whether the program is one with a TLB of
its own, for which the whole MMU is done (mmu_strict). That is found
out when the program turns translation on, and stays so while it has
translation off, which such a program has for a moment all the time. A
state saved in such a moment did not say, and the machine loaded from
it was one without an MMU until translation came on again: other code
compiled, timed otherwise, and the program somewhere else at the end of
the frame than in the same machine left alone. Found on ARM64, where
the frame in which the test program first turns its MMU on came out
different from the state loaded; on x86-64 the same from other frames.
And for every machine, whether the game has written to the framebuffer
itself since it was last shown, and where the framebuffer is taken to
be (Renderer_if.cpp). A game that writes there and also renders left
that mark standing after the render, and at the next vblank with no
render the framebuffer was shown. A machine loaded from a state had the
mark as the machine before the load had it: Virtua Fighter 4 showed
its framebuffer at frame 378 left alone, and not loaded from the state
of the frame before.
States of before V22 load as before: the translations are found again,
which way the MMU is done is found out again, and nothing is taken to
have been written to the framebuffer.
Sega Rally 2 with every frame run ahead from its hundredth frame, 3500
frames through its boot and into the attract mode: the sound is the
plain run's less one frame, to the sample, and every frame run from the
state loaded leaves the same state, to the byte, as the same frame did
before the load.
Loading a state emptied the SH4 recompiler's code cache: every block was
compiled again the next time it ran. A frontend that runs frames ahead
loads a state after every frame, the state of a frame ago, in which the
code is what it is now almost to the byte - and the game was compiled
from scratch sixty times a second. In a fight of Dead or Alive 2 the
frame after a load took 128 ms where a frame takes 5.
bm_LoadRam() reads main memory from the state in place of a plain copy.
A page that blocks were compiled from and that is write-protected for
it is compared with the state's: the same, it is left alone with its
blocks; another, its blocks are thrown away, as when the game writes
there, and the page is copied. Pages that are not protected are copied
as before: blocks from those look at their own code before they run. So
a load costs what it did - one pass over memory - and what was compiled
stays right by the same two means that keep it right while the game
runs.
The cache is still emptied when a load fails, memory then being neither
one thing nor the other, and when the MMU is on in one of the two states
and off in the other, or on in another way: code is compiled for that.
ra_unserialize_has(): whether the state being read has so many more
bytes, for code that looks at them before they are read.
(What else was made again after every load - the sound processor's
code, the textures - follows in this series.)
What each part of the machine was missing from a state, in a block of
its own at the end, in this order: the sound chip (66 bytes), the
drive's sector cache (2356), Maple (84), the arcade board (0 on a
Dreamcast, 15, 573 with an M2 cartridge), the modem (4165 on a
Dreamcast, written as zeroes by a build without one), the HLE BIOS (4),
its GD-ROM calls (12), the built-in NAOMI BIOS (4), and the tile
accelerator's frames (counted at their largest). Each part reads back
what it wrote and checks it, and says what a state from before V21
implies instead. An older state loads as it did.
A state is now the same size every time for the machine and devices
that are running, which is what a frontend that asks once needs.
After any load the timers work out again what follows from their
registers (tmu_state_loaded()).
A state of a version this build has not heard of is refused before
anything is put in place.
On a machine with no drive the drive's event does not exist, and its id
started out as 0 - some other device's event, whose times were written
in the drive's place and read back over it. The id starts as -1 and
nothing is read into another device's event. The same for the modem's.
A state is the frontend's bytes: from a file, a netplay peer, another
build. Several things were taken from it as they came.
The sizes of main, video and sound memory and their masks were stored
into the running machine's. They are what the machine was set up with,
and its memory is that large whatever a state says: with other values
every access was masked for memory that is not there. They are compared,
and a state of a machine with other memory is refused. One that is
smaller than the machine's three memories together is refused before any
of it has been put in place, so the game that is running goes on.
Each register's flags - whether it holds a value, or the address of a
function that reads it - were taken from the state: a value could be
made an address and called. The build's are kept.
A state that says there is a broadband adapter had the adapter's
contents read into a device that does not exist, and the flag left set.
This build has none: refused.
On an arcade board the event for a disc being looked at does not exist
and its id is -1: every save read, and every load wrote, the two words
before the scheduler's list. Nothing is saved there but what was, and
nothing is written outside the list.
Also bounded: the tile accelerator's state (an index into its table), the
current scanline, two flags that were read as whatever byte was there,
and a Maple device type that is no device, after which the rest cannot
be read: that load fails.
The format is unchanged.
The sound CPU has no Thumb: an address loaded into the pc loses its low
two bits. Branches did that, and in the interpreter arithmetic to the pc
and LDR. LDM with the pc in its list did not; nor, in the x86-64
recompiler, LDR, arithmetic or a constant moved to the pc, nor LDR in
the older one. The next fetch was then from an address that is not an
instruction's, and a block was compiled from there. One AND where the pc
is loaded, nothing per instruction. A pc from a state is treated the
same way.
How much of the drive's buffer is in use, where in it the next word goes,
how long a sector is, where SET_MODE's data goes and which word of a
packet is next were taken from a save state as they came, and a state
that is damaged had the drive read and write outside its buffers, or
divide by nothing. What does not fit is made to, and a transfer that
cannot be is called off; upstream checks two of these. The same for the
place in a sector that the BIOS-less read routine goes on from.
Nothing changes for a state the drive wrote.
retro_serialize() took no notice of the size it was given. A state is
not the same size every time - the tile accelerator's list is in it,
as long as it is at that moment - and a frontend that asked for the
size earlier has room for what it was told then. The size is counted
first, which costs nothing, and a state that would not fit is refused
instead of written past the end.
The list's part of a state was read as this build's version whatever
the state's: one from before the render passes were added (V10, V11)
was read with passes it does not have.
Left for a patch of its own: what is saved of the list is not the live
copy, so a list that is half sent when a state is taken is not in it.
Putting that right makes states larger and of more sizes, and needs the
size the frontend is told to be the largest there can be.
A flat NAOMI image whose header says "Naomi2" is run as a NAOMI 2, with
the BIOS from naomi2.zip (or .7z). Until now every such image was taken
for a NAOMI 1, and the BIOS answered "this game is not acceptable by
main board" - which is what upstream flycast does with one as well.
What a NAOMI 2 has that a NAOMI does not:
- The geometry processor of the last patch, in area 2: its registers
at 08800000, its command port at 09000000, its 32 MB at 0a000000,
and at 085f6800 and 085f8000 the holly's and the PowerVR's registers
written to both PowerVRs at once (core/hw/pvr/elan_host.cpp). What
it sends goes into the tile accelerator the way a store queue write
does.
- A second PowerVR. The two are one here, as in upstream: one set of
registers, one video memory. What the second has of its own is its
normal interrupt status, at 025f6900: the end of a list and the end
of a render are raised in both, and the game clears each.
- The second PowerVR's video memory is the mirror 32 MB above the
first's, and games write every texture there too. The texture
cache did not watch the mirror ("the mirrors don't seem to be
used"), so a texture changed through it kept its old picture. On a
NAOMI 2 it is watched.
- Frames several times the size of anything a NAOMI sends: the tile
accelerator's buffer and the lists a frame is parsed into are four
times as big on a NAOMI 2, and a buffer kept from another machine
is not reused.
Save states: version V20 adds, on a NAOMI 2 only, the second interrupt
status, the chip's state and memory, and the time its texture transfer
ends. Older states load as before.
"Is this a NAOMI" is asked in a dozen places: SYSTEM_IS_NAOMI() answers
for both.
Tested with a flat image of Virtua Fighter 4 and naomi2.7z, in the
RetroArch nightly of 7 October under Xvfb (Mesa llvmpipe):
- The BIOS starts the game; the attract mode's temple scene and the
demonstration fight are drawn - letterbox, coin text, the stage, the
fighters and their shadows - by the OpenGL and the Vulkan renderer,
per triangle and per pixel, and look alike in all four.
- Against upstream flycast (which lights in vertex shaders), made to
run the same image as a NAOMI 2 by a change of three lines: the same
scenes, the same picture to the eye. The two do not reach a scene on
the same frame, so they were not compared pixel by pixel.
- What the chip sends the tile accelerator over 1500 frames is the
same, byte for byte, from run to run.
- Threaded rendering on and off: 138 pixels of a 1440x1080 frame
differ, where two runs with the same setting differ in 54. Those
are Mesa's: a texture with mirrored repeat comes out differently
from run to run on llvmpipe, and identically with plain repeat.
- A state saved, the game run 50 frames on, the state loaded and the
game run on: 100 frames later its state differs from a straight
run's in no more bytes than two straight runs differ from each
other (about 900 of 92 MB).
- tools/threads/headless.sh, the Dreamcast test disc: passes.
Not done here: the chip's memory is not in the recompiler's direct
mapping, so its accesses go by the handlers; NAOMI 2 games in MAME's
zip form are not in the game table; starting without a BIOS.
Two things a loaded state did to the sound:
- A game starts sounds in two steps: it sets the key-on bit of each
channel it wants, then starts them all with one write. Loading a
state put every channel's envelope back through the function that
changes its state - which, for a channel that is released, as every
idle one is, also clears its key-on bit. A state loaded between the
two steps lost the mark, and the sound never started. The envelope
is put back without that. Upstream loads states the same way.
- The drive's audio status was read back as "playing or not": a drive
that was paused, or had played to its end, came back playing. That
reading is for states from when the status was a flag, and is kept
for those only.
This is what the live test's "sound, with states saved and loaded"
run has been failing on, now and then and lately every time: it saves
and loads by the clock, and whether a save fell between the disc's
two steps depended on how fast the machine ran.
tools/threads/headless.c can now save a state before one frame and
load it before another (HEADLESS_SAVE, HEADLESS_LOAD), and
headless.sh has a sixth run that does, at fixed frames, with a channel
marked and not yet started: the sound from the load on has to be the
first run's from where the state was saved. On the old code it is not
(the marked channel stays silent); with this all six runs pass, and
the live test passes on gl and glcore.
When the frontend put a disc in, the drive had it at once: ready, of
its kind, with "the medium may have changed" already in its sense
data. A real drive spins the disc up and reads its table of contents
first, and games that have the player change discs wait for what it
says while it does. Upstream gave it a second, and lists Skies of
Arcadia, Shenmue II, Alone in the Dark and Dancing Blade as games
whose disc change that fixed.
So now, as upstream has it:
- for a second after a disc goes in, the drive is busy and its sense
data says "not ready, becoming ready" (2, 04h/01h); the kind of disc
it reports is still what it reported with the lid open;
- then it has the disc, and says once "medium may have changed"
(6, 28h), or 29h if there turned out to be nothing to read;
- unit attention sets CHECK in the status of the commands that follow,
which upstream changed for D2's disc change;
- opening the lid no longer puts unit attention in the sense data.
A disc that is there when the game starts is ready at once, as before.
The second is in the save state (V19, eight bytes); a reset or the lid
opening again ends it.
The headless frontend can open and shut the lid (HEADLESS_SWAP), and
the headless test gains a fifth run that does. The disc's program
watches the drive every frame: it has to be busy for 55 to 65 frames,
then ready, then answer REQ_ERROR with 6/28h. With the second cut to
nothing the run fails (bad00c01).
Not tried with any of the games named: none is at hand.
Read against upstream's, which changed all of this for games:
- The answers were written into the game's memory the moment a
transfer was started, and the interrupt came later. A game that
looks before the interrupt found them there already. They are now
kept until the transfer is over and written then, with the
interrupt. (Upstream: "Maple dma xfer must not be executed
immediately. Delay until interrupt is raised.")
- An answer with nowhere to go, its address not in main memory, was
written to some other address made out of it. It is now not
written, and the overrun interrupt is raised when the transfer is
over. (Upstream: fixes Cheats 'N Codes Volume 1.)
- The time a transfer takes was every byte at 2 Mbit/s. Only the
console sends that fast: devices answer at 724 to 738 kbit/s,
measured on the wire, and each frame has its start, parity and stop
as well. Now 2 Mbit/s out and 740 kbit/s back, as upstream has it.
(Upstream: fixes Silent Scope's memory card load at boot. Its commit
also notes that Power Drift then only boots with one controller
connected; that is not looked into here.)
- In the mode where the bus sends the most significant byte first
(SB_MMSEL 0), the last word of a frame was not byte-swapped on its
way to the device.
The pending answers are in the save state, which becomes V18 and
16 KB larger; older states load as before. They are dropped on reset.
The disc gains a test: a controller's answer must not be in memory
right after the transfer is started and must be when it is over. With
answers written at once it fails (bad00901).
The SH4 has two store queues, and each has a register (QACR0, QACR1)
naming the area its 32 bytes are sent to. With the MMU off the core
picked one routine for both queues from the area of whichever of the
two registers was written last, and the general routine read QACR0
for either queue. Games nearly always set the two alike, and then it
makes no difference. One that aims a queue at the Tile Accelerator
and the other at main memory had both go where the register written
second said.
Upstream goes by QACR0 for both, which is right for the first queue
and wrong for the second in the same case.
The routine is now picked from both registers: the quick ones for
main memory and for the TA when the two agree, as before, and the
general one when they do not, which now reads the register of the
queue being flushed. It is picked again when a save state is loaded,
from the registers, not from what the state says was in use. A QACR
reads back as its area bits only, as upstream has it.
The live test's disc has a test of it: the second queue aimed at main
memory, then the first at the TA, 32 bytes through the second, which
have to arrive in memory with nothing either side disturbed. If not,
the background is orange and the verdict word says so. Run headless on
the core as it was, the disc reports exactly that (0xBAD00006); with
this, it passes, under both SH4 timings, and the live test passes.
The sound chip's ARM7 was interpreted on x86-64, one instruction at a
time; only ARM hosts had a recompiler for it. This brings in the one
flycast has upstream for x86-64: arm7_rec.cpp turns the instructions
at the ARM's program counter into a block of simple operations, and
arm7_rec_x64.cpp turns the block into machine code. Instructions it
has no code for - block transfers of several registers, multiplies,
anything odd - are run by the interpreter from inside the block.
The recompiler ARM hosts use is the old one, untouched.
On the Soul Calibur state the ARM7 costs 690 cycles a sample this way
against 1049 interpreted, a third less. The driver's compiled code is
21 KB.
It is not upstream's as it stands. Run against the interpreter on
random programs (tools/arm7, next patch) it stopped on some valid
instructions and got others wrong. Fixed here:
- The two-operand instructions worked on their operands' own
registers, and for some pairings of result and operand overwrote
one before using it (SUB and SBC with the result in the second
operand changed the first operand's register; RSC with the result
in the first operand subtracted the second from itself). With r15
as the first operand most could not be assembled at all and threw.
Both operands are now taken by value before anything is written.
- RSB was done as negate and add, which has another carry.
- A shift by a register took the whole register for the count, where
it is the low byte: a count of 0x102 was "32 or more".
- A flag-setting logical instruction with a shift by a register left
the carry alone; so did one with a rotated constant under 256, and
one with r15 as second operand took its carry from r15. These go
to the interpreter. The carry of a constant was also looked for in
the first operand.
- r15 shifted by 32 (LSR #0, ASR #0) was folded as a shift by 0, and
r15 with a rotate, or with a shift whose carry is wanted, lost the
shift.
- A block load or store of one register is rewritten as a plain load
or store, and wrote the base back whether asked to or not; and the
load turned the word by the address's low bits, as a plain load
does and a block load does not.
- An instruction after a status register write with the same
condition was run or skipped on the flags from before the write.
- Filling the code cache threw. It is flushed instead.
With these, the recompiler and the interpreter leave the same
registers, flags and memory behind on 26,391 random programs.
As upstream: code is compiled as it is first reached and kept until
the ARM is reset or a state is loaded, so a driver that rewrote its
own instructions while running would keep running the old ones; and
interrupts are taken between blocks of up to 32 instructions, not
between instructions.
The registers gain a scratch slot after the last, for the recompiler.
It is not counted with them, so a save state is what it was.
Compiled for Windows x64 and, for the parts shared with the old
recompiler, ARM64; run on Linux x86-64 only.
The sound chip mixes the sector it is playing straight out of a mapped
track file through TrackFile::Lend(), which lends a sector for the
disc's lifetime; a decoded hunk cannot be lent and is still read into
the AICA's buffer. libCore_CDDA_Detach() copies a lent sector into
that buffer before the disc goes away and around a save state, so a
lid opening mid-sector or a state saved then is safe.
The live test's disc now starts its audio track playing, looping, so
the saves, loads, resets and lid cycles all happen on a lent sector.
The palette's conversion to host formats, and the hashes paletted
textures are keyed by, were redone when pal_needs_update said so: a
flag set by the register writes and cleared by palette_update(), which
the emulation thread called when the game started a render.
With Threaded Rendering that is the wrong thread at the wrong time. The
frame before can still be drawing, on the frontend's thread, when the
game starts the next render, and palette_update() rewrote the very
tables that frame's texture decoding was reading. It also converted
from the live registers, which by then may hold the palette of a frame
the renderer is not on yet.
palette_update() is now called by the renderer, for each frame, before
the frame's textures are processed, and reads the copy of the registers
the frame is drawn with; TexCache.cpp reads that copy throughout, as
the renderers do. Whether the tables are stale is decided by comparing
the 4 KB of palette and the control register with what was converted
last, so there is no flag for two threads to share, and a palette
written back unchanged costs no conversion and no upload.
pal_needs_update is gone.
The live test's disc now draws its background with an 8-bit paletted
texture and, after the texture's last repaint, changes the palette
entry it uses to yellow: the yellow screen takes the texture write and
the palette write both. With the conversion taken out the screen is
black and the test fails.
Whether to upload the fog table was decided by fog_needs_update, set by
the register write on the emulation thread and cleared by the renderer.
With Threaded Rendering those are two threads, and a frame is rendered
from a copy of the registers taken when the game started it. A table
written after that copy was taken set the flag; the renderer, still on
the earlier frame, uploaded the old table from its copy and cleared the
flag. The new table was then never uploaded, until the game wrote the
table again.
The table is 256 bytes as a texture. The renderers now build those
bytes from the registers the frame is rendered with, every frame, and
upload them when they differ from what the GPU has. There is no flag,
so nothing to lose, and a game that rewrites the same table every frame
no longer has it uploaded every frame. OpenGL, OpenGL per-pixel and
Vulkan all do it this way; fog_needs_update is gone.
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.
retro_unserialize() called CalculateSync() after dc_unserialize(). Besides
recomputing the derived video timing, CalculateSync() also reset
pvr_cur_scanline to 0 and re-armed the vblank scheduler event at
now + Line_Cycles, discarding the raster position and vblank schedule that
had just been restored from the state.
Because the running instance never does this, a saved-then-reloaded run
diverges from an uninterrupted one: the vblank event fires up to one
scanline early right after the load, shifting frame timing and cascading
into a full audio/CPU desync. It is most visible on the reios (HLE BIOS)
path, where the attract loop polls GD-ROM and is sensitive to frame
timing; a real BIOS happened to hide it. This breaks runahead and netplay
determinism when no BIOS is present.
Add a reschedule flag to CalculateSync() (default true, so register-write
and reset callers are unchanged) and pass false on the unserialize path so
only the derived timing is recomputed. Serialize pvr_cur_scanline as well,
bumping the savestate version to V15, so the raster line round-trips.
Pre-V15 states still load and keep the previous reset-to-0 behaviour.
Two host-clock dependencies in the emulation path:
QueueRender() had a wall-clock speed limiter under SynchronousRendering: it
measured emulated-cycles-per-real-second via os_GetSeconds() and waited if the
SH4 was running faster than real time. A libretro core must not throttle itself
against the host clock -- the frontend paces via audio/video sync -- and doing
so is non-deterministic and fights that pacing. Drop the wall-clock measurement;
keep only the render-to-texture wait (a queued RTT frame must finish before the
SH4 reads it back), which is a correctness sync, not a timer.
The AICA RTC increments deterministically off the SH4 scheduler (DreamcastSecond
every SH4_MAIN_CLOCK cycles), but its initial value is seeded from the host
clock and RealTimeClock was never serialized, so two netplay peers -- or a
runahead rollback -- diverged the moment a game read the clock. Serialize it
(V14) so it is synced with the rest of the state. The host-clock seed is kept:
it is the correct behaviour for a real-time clock, and is now deterministic
across the state transfer that netplay and runahead both perform.
Left in place (not emulation timing): log timestamps, the vendored Vulkan
allocator's QueryPerformanceCounter, the dead #if 0 fastmmu benchmark, the SH4
guest-side GetTickCount/QPC address decode, the NAOMI multiboard network thread's
pacing sleep, rumble duration (output-only), and the threaded serialize/reset
handshake timeouts.
retro_unserialize() receives the frontend buffer length but dc_unserialize()
only ever used it to disambiguate two legacy save-state versions, never to
bound the reads. ra_unserialize() did an unconditional memcpy from the input
cursor, so a truncated or malformed state (netplay peer, runahead, corrupt
file) walked straight off the end of the input buffer, and several length
fields taken verbatim from the state were used as memcpy destinations:
- UnserializeTAContext: 'size' copied into the fixed TA_DATA_SIZE buffer and
'render_pass_count' used to index the fixed render_passes[10] array.
- sys_nvmem_sram/flash: stored .size overwrote the live size and drove the
copy length into the fixed data[] allocation.
Track the input base/limit in dc_unserialize() and reject any read that would
run past the end, latching a failure that propagates to the retro_unserialize()
return value. Clamp the attacker-controlled TA size, render_pass_count, and
nvmem copy lengths to their real destination capacities. Legitimate states
round-trip byte-for-byte unchanged.