maple_if.c drives the devices through a small C API in maple_if.h
(maple_dev_dma, maple_dev_has_slots, maple_dev_lightgun_pos,
maple_dev_flush_save), which also declares the I/O boards' state parts
and the mcfg_* calls once, with C linkage. A device writes its answer
straight into the room maple_out keeps for it until the transfer ends;
the copy is left only for when that room has run out.
dns.c builds its query and reads the answer as bytes, with no picoTCP
headers: dns_query_build() and dns_answer_parse() are tested on their
own by tools/modem/dns_run.sh (with the sanitizers, in headless.yml),
names too long for the query included.
A retro_run - where the frontend reads its pad and shows the picture -
ended at line 0. Most games start the render of the frame they have
made, and read their pads, in their vertical blank in interrupt, which
need not be there:
- Sega Rally 2 (Windows CE) has its interrupt at line 0 and starts its
render a line after it: that render was the first thing in the next
run, and its picture was shown at that run's end, a frame after the
game started it.
- Virtua Fighter 3tb and Jet Grind Radio have theirs at line 260: they
render, then read the pads. Read in the middle of a run, the pads were
read a run before the render that answered them could end one.
Now a run may end in the lines after the vertical blank in interrupt
(32, and never on or past the vertical blank out line, where the
hardware's maple transfer reads the pads): at a render to the screen the
game starts there - its picture is shown at once - or, before one, at a
maple transfer the game starts there, which is then made at the start of
the next run, with that run's pad. No time goes by between two runs, so
the transfer is made where the machine stands, as the game started it;
only the processor's last few hundred cycles of its time slice run before
it. A run that ended in a window ends in the next one, at either, or at
the window's end in a frame with neither (a game at 30 frames a second, a
loading screen), unless it rendered to the screen elsewhere in that frame:
then it goes on to line 0, where the runs of a game that does nothing in
the window (renders mid-frame, has the hardware read its pads) end as
before - but not past the next window: where line 0 came while the window
was open (Sega Rally 2's interrupt is at line 0), it ends at the window's
end. Each run has one vertical blank in interrupt in it. A window closed
early has the line event aimed again: one aimed at its end and taken for
a later line lost lines - a frame 13 lines (Sega Rally 2) or 233 lines
(Jet Grind Radio) short.
Measured with tools/lag: machine frames from the press to the first
picture that differs, RetroArch's default ("late") polling, the same
with threaded rendering on; this core before / after, and upstream
flycast with "Delay Frame Swapping" on (its default) / off, from states
each core made by booting the game the same way:
Sega Rally 2, title, START 4 / 3 upstream 4 / 3
Virtua Fighter 3tb, attract, START 3 / 2 upstream 3 / 2
Jet Grind Radio, title, START 3-4 / 2-3 upstream 5 / 3
Dead or Alive 2 Millennium, START 1 / 1 upstream 4 / 3
Jet Grind Radio, street (s10), jump 3-4 / 2-3
Jet Grind Radio, graffiti (s9), jump 3-4 / 2-3
Virtua Fighter 4, START; DOA2, COIN 1-2 / 1-2 (they render before their
interrupt and read their
pads after line 0)
(Jet Grind Radio runs at 30 frames a second: the lag depends on which of
its two frames the press comes in, and is one frame less in the one of
them that was the longer.)
Where a run ends is decided by the machine alone, the same however the
frame is shown or not (a frame run ahead, fast-forward's frameskip): a
run starting in a window is one that ended in a window, a transfer
waiting for the next run is one started with nothing scheduled to end it
(it is made at the start of the next run, whether or not a state was
loaded in between), no run ends with the window open (dc_run()'s
backstop closes it too), and whether the game has started a render since
the last vertical blank - which the vertical blank goes by to show a
framebuffer the game wrote itself, and which was never so at line 0 - is
now in the state: bit 1 of pvr_serialize_v22()'s first byte, 0 in every
state before, which a state saved at line 0 has too. The state's size is
the same, and old states load as before. Without that bit, a frame run
ahead on the live test disc, which writes its framebuffer itself, was not
the frame run straight through (tools/threads/headless.sh).
tools/determinism (plain, again, reload, run-ahead, rollback, and frames
not shown, every one or RetroArch's run-ahead way) passes with threaded
rendering on and off on Jet Grind Radio, Sega Rally 2, Virtua Fighter
3tb, Virtua Fighter 4, Dead or Alive 2 and the live test disc;
tools/threads/headless.sh passes; tools/lag/samestep.sh has every
renderer in step on Jet Grind Radio, Sega Rally 2, Virtua Fighter 4 and
Dead or Alive 2. The sound each retro_run makes stays a frame's on
average, with no drift: over 1800 frames 735/736 samples a run in the
Dreamcast games and 737/738 in the NAOMI ones, as before; Sega Rally 2
alternates 713-718 and 754-759 where it renders every other frame (the
run ends at its render one frame and at the window's end the next), and
the first run after loading a state saved at line 0 is shorter, once.
A frame is a duplicate (video_cb with no picture) when no picture was
made in its run: a render to the screen or of the framebuffer the game
wrote itself. That was asked of the vblank, which no longer always ends
the run; a render to a texture alone no longer counts as a new picture.
A maple occupy cancel (MP_SDCKBOccupyCancel) dropped the light gun's
occupy but left the gun's transfer marked as owed its end at vblank-out,
as upstream clears it; spg_gun_occupy_cancel() clears it, and the
transfer ends at its own time.
- A hard reset now resets the cartridge's registers - PIO and DMA
offsets, M1 decompression and decryption state, M4 and Atomiswave
device state, the GD-ROM cartridge's DIMM address - without reloading
ROMs, and any reset clears the I/O board program buffer: an upload cut
off by a reset is not taken for part of the next one.
- The NAOMI per-game table is matched on the whole name: a name that
was part of another entry's (an empty one: Samurai Spirits 6's) got
that entry's settings.
- NaomiNetwork::receive waits for both bytes of the packet number, and
does not report a half-received packet as received when stopping.
- A state saved before the frontend changed a Dreamcast port's device
and loaded after (run-ahead does this every frame) brought back the
old devices with nothing to put the new ones in: the change was lost.
After a load, what is not as the settings say is unplugged and the
right devices plugged in a tenth of a second later, as for a live
change - the same for the same state and settings every time.
- The bus a frame goes out on comes from the DMA descriptor, not from
the frame's address, and a device without expansion slots answers the
frames addressed to its slots (upstream a429fba21, MAME agrees). A
device answers with its own bus's attached-device bits.
- The VMU screen after a load: a controller's second card, never drawn
on, no longer puts its blank screen over the first card's picture.
- When the frontend changes a port's device, a Dreamcast unplugs only
what changes (the bus whose device changes, a slot whose device or
card file changes) and plugs in the rest a tenth of a second later.
Everything was unplugged: a memory-card write in that window was lost
(the threaded/non-threaded state comparison in live.sh failed on it).
- The VMU clock reads the console's RTC, which is in a save state, not
the host's clock: a frame run again from a state got another date.
- A later light-gun occupy request for a bus with no gun no longer
clears the flag a gun had set.
- Without the fast-memory layout: each memory has a zeroed spare page
(unaligned accesses at the end went past the heap block); Windows frees
_aligned_malloc memory with _aligned_free; video memory fault offsets
drop the Android ARM64 pointer tag.
The I/O boards' coin counters were not in a state: a load made the
boards anew with nothing counted, and the game, which goes by how far a
count has moved since it last looked, saw coins come or go - on every
frame with run-ahead. They are in maple_serialize_v21(), with the coin
switches (a coin held across a load was otherwise counted twice), Wave
Runner GP's drive board byte, the rotary encoders' positions, and the
time at which unplugged devices are plugged in again: without that, a
change of device under run-ahead left every port empty from then on.
84 bytes, whatever the machine has.
How many boards there are was written as a size_t: eight bytes by a
64-bit build and four by a 32-bit one, and each took the other's state
for its own, with everything after it read four bytes out. What is
written is unchanged; the reader tells the two apart (the four bytes
more are zeroes, and what follows a 32-bit count never starts with four
of them).
Devices that came back after the wait replaced the ones a state loaded
in the meantime had put there without taking those away first.
A Maple frame was read by what each device expected to find in it. A VMU
block write with a short frame made the count wrap and copied some 4 GB
into the card's memory. A word is now read only when the frame has one
left; the VMU's screen and the rumble pack act only on a frame that is
all there.
The table of frames and each frame have to lie in RAM, and an answer goes
to RAM, all of it: the addresses are the game's, or a state's. What a
device is given is no longer than the table says the frame is.
JVS: lengths, channel numbers and counts in the cabinet's messages are
held to the frame and to the reply (255 bytes); the encoder count to the
eight there are; a request kept for later is dropped if it and the new
one do not fit. The I/O board's firmware upload is cut at the end of its
64 KB. From a state: no more boards than the cabinet has, and a pending
answer length that cannot be one is none.
The scheduler's list is the process's and outlives the machine. Nothing
is ever taken out of it, so each game loaded after the first added up to
ten entries of its own, and an event the old machine had asked for - the
old scanline event asked for itself again each time - went on coming to
the new one. A callback that is already in the list now gets the place it
has, with nothing waiting in it. Ids no longer change from one load to
the next; states name the events by variable, so nothing in them moves.
A reset left events of the machine before it waiting: the next piece of a
GD-ROM transfer, the end of a Maple, AICA or NAOMI 2 transfer, a render's
end, a modem step. Each came after the reset and raised its interrupt for
a transfer the registers no longer knew about. Each device's reset now
takes back its own, where it clears what the event works on. There is no
central clear and the clock is not restarted.
The sound chip's sample tick and the clock's second are not the machine's
to lose: they are asked for by the first reset, which is when they were
asked for before, and left alone by every later one.
A soft reset leaves an AICA transfer's end to come (SB_ADST stays set and
the event is what ends it), and the NAOMI 2's, whose chip it does not
reset.
sh4_sched_is_scheduled() answers no for an id that was never given out.
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).
A VMU block write and a JVS EEPROM write only update the device's
memory and mark it dirty; maple_FlushSaves() writes the dirty span out
once per frame from retro_run, while the machine is asleep, and the
device destructors flush what is left. A save that used to cost a
seek, a write and an fflush per 128-byte phase on the emulation thread
is one write of its span per frame instead.
Load default eeprom from mame rom if present
Fix controls for hotd2 and alpilot
Fix vathlete graphical gltches due to huge negative z values
GD-rom games now have proper game id
Fix csmash crash entering game test mode