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.
The arm64 macOS build (platform=osx arch=arm) used the interpreters.
It now has the same recompilers as arm64 Linux, made to follow Apple's
rules for code written at run time:
- The code goes in memory mapped with MAP_JIT (vmem_platform_alloc_jit()),
once for the SH4, once for the ARM7 and once for the DSP, at start-up,
and kept for every game after that. MAP_JIT cannot be given an address
(no MAP_FIXED), so the static arrays in the library's text are not
used there (and are not built in: no 17 MB of zeroes in the dylib).
The memory is asked for right after the core's own __TEXT, then below
it, so that the core's functions are within one branch's reach
(128 MB); where the system puts it elsewhere, it is taken anyway.
- Such memory is writable or executable per thread
(pthread_jit_write_protect_np()). Everything that writes code -
compiling a block, relinking one, the main loop, the fast-memory
rewrite in the fault handler, the ARM7 and DSP compilers, clearing the
cache - is wrapped in vmem_jit_write_begin()/end(), a per-thread count,
so nested writers are fine and other threads go on running code. No
lock. On every other platform the two are empty.
- Calls and branches from the code to the core: when the target is out
of reach, through a register (x16/x17, vixl's scratch registers). The
three block-linking routines find their caller by the address after
the call, which has to be inside the block as linked: for those a
block calls a trampoline beside the main loop instead (one bl, as
today). Where the code is in reach - always, on every other platform -
exactly the same instructions are emitted as before.
- ngen_arm64.S is ELF assembly: for macOS its routines are in
rec_arm64.cpp in Apple's syntax (underscored names, .private_extern,
.alt_entry where one routine runs into the next), and the ARM7's
dispatcher in arm64.cpp is written for both assemblers.
- No JIT memory (a hardened frontend without the
com.apple.security.cs.allow-jit entitlement): the SH4 and ARM7
interpreters are used, and the DSP is left out (no effects), instead
of a crash.
- PAGE_SIZE is 16 KB on Apple's ARM systems: with 4 KB, mprotect() of the
page protections (textures, the SH4's code pages) was refused there, a
die() - that was already so for the interpreter build, and for iOS and
tvOS, which keep the interpreters.
Linux and Android arm64 are unchanged in what they emit; iOS and tvOS
stay on the interpreters (HOST_APPLE_JIT is for a macOS target only),
and macOS x86-64 keeps its x64 recompiler as before. No save-state
change.
Verified here (Linux, no Mac):
- full cross-build and link of parallel_dc_libretro.dylib for
arm64-apple-macos (clang 18, MacOSX 13.3 SDK, ld64.lld): links with
every symbol resolved against libSystem, libc++ and OpenGL; the
assembly routines come out in order and the fall-through symbols are
marked alt_entry
- full cross-build and link for x86_64-apple-macos (x64 recompiler)
- the changed files compile for arm64 iOS and tvOS targets, with
HOST_APPLE_JIT off there
- Linux aarch64 (platform=arm64) with the same recompilers:
tools/threads/headless.sh passes under qemu-aarch64
- the same, with every call and branch to the core forced through a
register and the link trampolines on (the code macOS runs when its JIT
memory lands out of reach): headless.sh passes too
Not verified: anything on a real Mac - MAP_JIT, the per-thread write
protection, the placement near the core's code, the fault handler and
the fallback without the entitlement. Unverified until GitLab CI builds
it and someone runs it on Apple silicon.
- A GD-ROM's single-density lead-out is track 2's end, not 13085 (falls
back to it when the image gives none); a GDI track ends where its file
ends; a GD CHD track's pad frames come off its end (Shenmue disc 3,
Re-Volt's CD player). Every GDI and GD CHD game gets these TOC values.
- Windows CE under the HLE BIOS: the boot file's first 2 KB goes to
8ce01000 (it was skipped), the rest to 8c010000 as before.
- With no disc, a hard reset leaves the drive busy for 1 s, then "no
disc" with unit attention, as upstream (it said "no disc" at once).
- The real BIOS for the games that need it when its file is there:
MIL-CDs, Jimmy White's 2, Railroad Tycoon II (EU), Miss Moonlight,
KenJu, Force Five, Fushigi no Dungeon, Slave Zero (PAL), WSB 2K2;
without it they boot as today. The HLE BIOS even with the real one for
Stupid Invaders, Suika and Cool Boarders Burrrn.
- Depth scales: NHL 2K2 1e8; Re-Volt JP, Sturmwind, V-Rally, South Park
Rally, Capcom vs SNK Pro (DC and NAOMI) added; Samurai Shodown VI's
replaced by upstream's game patch. The no-RGB list applies only with
the RGB cable chosen. TrickStyle (US) in English, the flash's language
put back at the end.
- Widescreen cheats as upstream's table: original values checked where
upstream does, matching by software name, 11 more Dreamcast games, the
NAOMI table upstream's 35; a bad address is warned about and skipped
instead of trapping. Game fixes applied every frame for Extreme Hunting
2, King of Route 66, Dave Mirra, Samurai Shodown VI, Fur Fighters,
Ooga Booga and the Dricas authentication of Aero Dancing i/i2/F,
Daytona JP, Sega Tetris, Golf Shiyou Yo 2, Culdcept II, Outtrigger JP
and F355 JP.
- 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.
- read_m3u counts discs itself (upstream f03faffd2) and an empty line no
longer reads line[-1].
- retro_load_game clears the disc list, tray and game data first: a
failed load has no unload. The tray is shut at unload, and the next
load starts its own emulation thread (first_run).
- Bounds: the port is checked before device_type[] is read; keycodes
past the keyboard table are ignored; VMU colours outside the list and
render-to-texture scales outside 1..8 fall back.
- A stick pushed fully into the bottom-left corner overflowed an int
and read as centred.
- Screen rotation is read at load: once on it never turned off again.
- A reset or an option change kept per-game cable and sorting settings;
a second arcade game no longer inherits the first's I/O board setup.
- nullDC: no VGA for Caesar's Palace, no RGB for Roadsters, Shin Nihon
Pro Wrestling 4 and The Grinch, forced NTSC/PAL for 13 games (the
flash bytes are put back before saving), translucent depth mask for
Monkey Ball (upstream's per-game list).
- The refresh-rate debounce is reset when timing is reported again.
- A packet command the drive does not know set the error and left it
at that: the drive stayed busy, no interrupt came, and nothing more
was taken. It ends now (upstream).
- REQ_MODE, REQ_STAT, REQ_ERROR, REQ_SES and GET_TOC send as many
bytes as the program asks for, from as far into the reply as it
says. Both numbers went straight into a copy out of ten bytes on
the stack or thirty-two of a variable, and whatever came after in
memory was sent. The program still gets the count it asked for:
what there is of the reply, then zeroes. (Upstream bounds GET_TOC
and stops with verify() on REQ_STAT.)
- SET_MODE with an offset past the ten bytes that can be written had
its count worked out as ten less the offset, unsigned: up to 255
bytes written over whatever follows the drive's settings. Nothing
is written from there now.
- The built-in BIOS's drive state was never reset with the machine: a
read by DMA that was under way went on into whatever was started
next (upstream's gdrom_hle_reset()).
Seventeen Windows CE games had it switched off for them in the table of
games; that was all their rows said, and the rows are gone with the
column. settings.dynarec.disable_vmem32 goes too.
They run as they did: through the table of pages, like every other
Windows CE game now.
It has not been used since "the table of pages for every host", where
vmem32_init() was made to say no; this takes out what was left for it.
- core/hw/mem/vmem32.cpp and .h, and the call to it from the fault
handler - with the working out, for every fault, of whether it was a
write and which SH4 instruction it was for, which only vmem32 asked.
- Every "if (vmem32_enabled())": in the MMU (a translation forgotten,
the TLB emptied, another address space), the block manager, the
table of pages, the two 64-bit recompilers.
- _vmem_enable_mmu(): it handed P0 and P3 of the host's layout of the
SH4's memory over to vmem32 when the MMU came on and mapped them
again when it went off. Nobody takes them now, so they are mapped
once, with the rest (the two tables are one again).
- x86-64 recompiler: a quick access to memory as the host has it
mapped is for the MMU off only, so the store of the instruction's
address ahead of it and the second "how far in is the move" are
gone. Sh4Context keeps its exception_pc, unused, so that what
follows it in the context is where it was.
- bm_IsRamPageProtected(), which vmem32 asked before it mapped a page.
What stays is that with the MMU on a page of code is protected in P1
and P2 only, not in P0 and P3 as well: nothing reads or writes those as
the host has them then. The comments that said they were vmem32's say
that instead.
Why the host's mapping was given up - the numbers - was in a comment in
vmem32.cpp; it is in mmu.h now, with the table that replaced it.
No change in what runs: the whole of headless.sh passes on x86-64, on
ARM64 and 32-bit ARM under qemu and on the Windows core under wine, and
Sega Rally 2's sound over 3600 frames on x86-64 is what it was, byte
for byte.
With no naomi.zip (naomi2.zip) a NAOMI game stopped at "cannot load
BIOS". A flat image now starts all the same: reios does for it what the
BIOS does. The BIOS is used as before when it is there, and a romset
still needs one.
What that turned out to be - found by letting the real BIOSes
(epr-21576h, and the NAOMI 2's epr-23605c) run up to a game's first
instruction and looking at what they leave, by reading the BIOS's code
for the parts games call, and by tracing Dead or Alive 2 and Virtua
Fighter 4 with the BIOS and without until the two runs went the same
way:
The start. The image's header says what to load where (up to eight
pieces at 0x360) and where the game begins (0x420). The BIOS hands over
with the display in its 640x480 mode, a copy of the header at 0c01f400,
the clock's time at 0c01f130, and registers pointing at both.
The system routines. The BIOS leaves a table of 24 routines at 0c018000
that games call for what every cabinet does alike: the operator's
settings and the check of them, the 28 coin settings, coins into
credits (bonus coins, service credits, the most credits, the meters,
the queue of sounds), what each player can do with the credits there
are, the board's region and the game's serial. They are written here
from what the BIOS's do; a routine's place in the table holds the trap
reios answers. The ones for the GD-ROM's DIMM board and the network
board answer 0 and say so once: no cartridge game seen calls them.
There being no test menu, the settings are the BIOS's defaults: one
coin, one credit.
The interrupt service. A game never takes the processor's vectors: the
vector base stays at the BIOS's 0c000000, and the game writes its
handlers into the BIOS's table at 0c0001c0. The vectors are traps here:
one puts the interrupted program's registers by, in the machine's own
memory so that a saved state has them, and sends the processor to the
handler; the handler returns to the other, which puts them back.
Two things a game looks at in the boot ROM itself:
- Before it will use the routines, a game compares 112 bytes at 1ffd00
with a copy it carries - the BIOS's "COPYRIGHT (C)SEGA ENTERPRISES"
lines and "NAOMI BOOT ROM" - and otherwise goes a way that never
ends. Those bytes are put there. (Both games carry the same copy.)
- Dead or Alive 2's start-up code waits for a word that it reads
through a pointer it has not yet set, which makes it the boot ROM's
word at fc0. It is given the value the BIOS has there.
naomi_cart: bios_loaded is set when a flat image's BIOS was found; it
was only set for romsets, and nothing had asked until now.
Tested with no BIOS in the system directory (RetroArch nightly, glcore,
and the headless harness): Dead or Alive 2, Dead or Alive 2 Millennium
(from its RAR archive) and Virtua Fighter 4 (NAOMI 2) reach their
attract modes; a coin in Virtua Fighter 4 gives CREDIT(S) 1 and PRESS
START BUTTON, as it does with the BIOS; a reset starts the game again;
a state saved at frame 1400 and loaded at 1800 leaves the machine, 800
frames on, where 2200 frames straight leave it but for 270 bytes of
92 MB - and that state was taken inside an interrupt handler. With the
BIOS all three run as before, and the Dreamcast test disc passes.
Not done: romsets (the cartridge would have to be read through its
decryption), the test menu, and so any setting but the defaults. Of the
coin routines only the paths the defaults take have been run: bonus
coins, a chute to each player and service credits are as the BIOS's
code has them, and not exercised.
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.
The option chose between two ways of treating what a game renders to
a texture: copy it to video memory after every such render, waiting
for the graphics card each time and losing Render To Texture
Upscaling, or leave it on the card and have the games that look for
it in video memory come out wrong. A table switched the first on for
the games known to need it.
Since the last three patches there is one way, and it is right for
both: the picture stays on the card and video memory gets it when it
is first touched there. Nothing is left for the option to choose.
- The core option, in every language, and settings.rend
.RenderToTextureBuffer.
- The copy-after-every-render paths of all four renderers, with what
only they used: the Vulkan drawers' second render target and its
render pass variant, the check for the setting having changed, and
WriteTextureToVRam() (rend/rtt_watch.cpp packs pixels now).
- The table's column, in the console's table and the arcade one, and
the 51 rows that had nothing else in them.
Render To Texture Upscaling stays, and now applies to every game.
A frontend that still has the old option saved ignores it.
The live test passes on gl, glcore and vulkan, the headless test
passes.
On the console what a game renders to a texture is in video memory
afterwards. Here it stays on the graphics card unless "Enable RTT
(Render To Texture) Buffer" is on, which it is not by default; games
that then go on to read that memory, write into it, or use it as a
texture of another shape come out wrong, and a table switches the
copy on for the ones known to.
- The table had 14 games. Upstream's list has 48 product numbers:
the 37 missing here are added - Jet Grind Radio, Shenmue, Grandia
II, Sakura Taisen 1 and 3, Silent Scope, Worms World Party and
others. Two of the 14 were written without the region suffix the
disc has (the European Tony Hawk's Skateboarding and Pro Skater 2)
and matched nothing; the added rows have them as the disc does.
- What the table switched on was switched off again the next time
any core option was changed, because the option's value alone was
written to the setting each time. The table's word now holds.
Builds; the headless test passes. None of these games is here: the
rows are upstream's, not tested.
The 32-bit ARM recompiler knew nothing of the SH4's MMU, so a game
that needs it - the Windows CE ones - could not use it; the commit
before this one gave those games the interpreter. They now get the
recompiler, and that fallback is taken out again.
A block compiled with the MMU on differs in three ways, as it does on
the other recompilers:
- Every access to memory is a call that translates the address. Where
translation fails, the SH4's exception is taken there - registers
saved, PC at the handler - and longjmp() returns to the main loop,
which goes on from the handler. Instructions the interpreter runs
and store queue flushes, which can raise exceptions too, go through
the same. A constant address is still read or written directly when
it is one the MMU does not translate.
- It ends by handing its next PC to the main loop, which looks the
block up: which block an address means depends on the mapping in
force, so blocks are not chained to each other.
- It begins by checking that the PC is its own, and that the FPU is
on if it uses it.
The main loop (ngen_arm.S) has the two entry points again for such
blocks, and a setjmp() where it starts, once a frame.
There is no access through the host's own mapping, as the 64-bit
recompilers have: a 32-bit host has no room to lay a translated
address space out. So with the MMU on every load and store is a call;
slower than without, a great deal faster than the interpreter.
Run headless on a 32-bit ARM core under qemu-arm, the live test's disc
passes with the MMU on - its MMU test, then everything else through
the MMU, including the TLB misses and the illegal instruction that
come back through longjmp() - under both SH4 timings; and without the
MMU as before. The x86-64 headless test passes as before. Built for
a device target and with NO_MMU defined.
Not run with a Windows CE game, and the path for an FPU instruction
with the FPU off is not reached by the disc.
The 32-bit ARM recompiler knows nothing of the MMU. With it on, the
recompiler went on reading and writing at the addresses the program
gave as though they were physical ones, so a Windows CE game, which
lives behind the MMU, could not run on a 32-bit ARM build at all.
Where the core turns its MMU on for a game - a Windows CE disc, or
the "Force Windows CE Mode" option - a 32-bit ARM build now runs the
SH4 with the interpreter, which does the MMU properly, and says so in
the log. That will be slow on such a machine, but it is the game and
not a crash. x86-64 and ARM64, whose recompilers have the MMU, are as
they were.
Seen with the disc of the commit before, run headless on a 32-bit ARM
core under qemu-arm: the MMU run failed with the address not
translated, and passes with this.
"elf" has been among the extensions the core says it takes, and the
HLE BIOS has had a loader for ELF files, but nothing connected them:
the setting that names the file was cleared at start-up and never set,
and an .elf was handed to the disc reader like any other content.
Now it works. Content ending in .elf is not taken for a disc. The HLE
BIOS is used for it whatever the HLE BIOS option says - it is what
loads the file, and the real BIOS would only find an empty drive - and
the drive is left empty, where before an empty drive made the core
switch the HLE BIOS off. The program is loaded into main memory and
started where the file says it starts, which is 0x8C010000 for
anything built the usual way, on every reset.
The loader no longer takes the file's word for where things are.
Before acting on them it checks that the file is long enough to have a
header, is a 32-bit ELF, and has its program header table inside it;
that each segment's bytes lie inside the file; and that where they go,
with the zeroes after them, lies inside main memory. Only segments
marked as loaded are loaded - a stack marker at address 0 used to draw
a warning. A file that fails any of this is refused, and the CPU is
then left idling at the start address, as it is for a missing disc,
and not run into whatever memory holds.
Checked with the live test's program built as an ELF by the compiler
and written as one by hand: both start and draw their scene. Five
damaged files - cut off inside the header, the header table outside
the file, a segment outside the file, one too long for memory, one
placed at the end of memory - are each refused and leave the core up;
before the checks the loader would have copied from or to wherever
they pointed.
The linker was asked which functions of the core no code path reaches
(-ffunction-sections with --gc-sections --print-gc-sections), and each
one it named was then looked up by name across the whole source,
every platform's code and the tools included. These have no caller
anywhere:
MemWrite32 hw/pvr/pvr_mem.cpp
pvr_ReadReg hw/pvr/pvr_regs.cpp
rend_framePending hw/pvr/ta_ctx.cpp
rend_init, rend_resize hw/pvr/Renderer_if.cpp
emit_WriteCodeCache hw/sh4/dyna/driver.cpp
bm_WriteBlockMap hw/sh4/dyna/blockmanager.cpp
dec_Un_rNrN, dec_Un_frNfrM, dec_rz hw/sh4/sh4_opcode_list.cpp
cpu_iWarn hw/sh4/interpr/sh4_opcodes.cpp
get_nvmem_data hw/holly/sb_mem.cpp
aica_Term hw/aica/aica_if.cpp
reios_term reios/reios.cpp
cryptoReset hw/naomi/decrypt.cpp
printtoc imgread/common.cpp
dc_start nullDC.cpp
shutdown_thread_pool rend/TexCache.cpp
gl_DebugOutput rend/gl4/gles.cpp
libPvr_GetRenderTarget,
libPvr_GetRenderSurface libretro/libretro.cpp
VArray2::LockRegion libretro/common.cpp
LogManager::SetEnable log/LogManagerLibretro.cpp
set_user_data_dir,
add_system_config_dir,
add_system_data_dir stdclass.cpp, with the three
variables only they wrote
The two *_Term functions were empty. cryptoReset was not: but the
cartridge decryption it would have cleared starts itself afresh
whenever a key or an address is set.
Left alone, though the linker named them on x86-64: what the ARM,
ARM64 and x86 recompilers and the cached interpreter call (rdv_*,
emit_Write32, emit_SetBaseAddr, bm_GetStaleBlock, the block reference
counting, _vmem_ReadMem8SX32/16SX32, shil_opcode_name), cpu_iNimp,
which a build without an MMU calls, mmu_QACR_SQ, which is called but
inlined so that only its separate copy goes unused, and
archive_num_entries, which tools/archive uses.
libARM_Term, also empty and never called, stays for now: its file has
CRLF line endings, and a patch that touches it does not go through a
plain git am.
A number of settings are given a value once, in LoadSettings(), and
never another: there is no core option behind them and nothing else
writes them. Each is replaced by its value where it was read, and the
branch that value never took is removed.
- dynarec.idleskip, always on: the idle loop detection in the block
decoder is unconditional, and the branch for it being off is gone.
- dynarec.unstable_opt, always off: the store queue shortcut in the
ARM recompiler that it guarded is removed.
- pvr.MaxThreads, always 3: a constant in the texture upscaler.
- pvr.subdivide_transp, always off, and only read inside an #if 0
block of the triangle sorter, which goes with it.
- rend.Clipping, always on: tile clipping no longer asks.
- rend.AutoExtraDepthScale, always on.
- debug.SerialConsole, always off: the serial port's echo to stdout.
- imgread.PatchRegion, never set: the two functions that would have
rewritten a disc's region strings returned at once, and are removed
with the calls to them.
- validate.OpenGlChecks and pvr.Emulation.zMin/zMax were written and
never read.
LoadSettings() also loses a block for builds that are not libretro,
which this core is not, and which set a field that no longer exists.
core/rend/soft/softrend.cpp, a software renderer that used one of
these settings, was not in any makefile. It is deleted, and its
declaration with it.
Neither did anything. Both settings were written from their options
and read nowhere: what they chose between went with the old threaded
renderer, which could run ahead of the picture and had to either wait
for it or drop a frame. Threaded rendering now makes one frame for
each retro_run, the frame non-threaded rendering makes, and presents
it at vblank. The core had already been hiding both options for that
reason.
The options and their settings are removed.
settings.aica.LimitFPS was set to zero and never read.
settings.aica.NoSound was set to zero and read in one place, where a
non-zero value would have thrown every sample away. Sound is always
written.
settings.aica.CDDAMute was never set, so CD audio was always mixed in
and passed to the DSP. The branch for it being muted is gone.
That leaves settings.aica empty, and it is removed.
settings.pvr.Video (ResolutionMode, VSync) and settings.pvr.OSD
(ShowFPS, ShowStats) were declared and never mentioned again.
Each exit of dc_init() tears down the stages it completed, and
plugins_Init() terminates the plugins it brought up before the one that
failed. A content load that fails no longer leaves the Win64 vectored
exception handler, the dynarec unwind table or the reserved address
space registered after the core is unloaded.
The option chose between two mixers. With it off, sound was mixed 32
samples at a time by a version that leaves the sound chip's DSP out:
no reverb, no chorus, none of the effects games route their channels
through, and a channel sent only to the DSP was dropped into the
output as it was instead. With it on, sound is mixed a sample at a
time with the DSP run for each.
The option, its setting and the batching setting that went with it are
removed, and so is the mixer without the DSP. Low-end builds, which
had it off by default, now run the DSP like every other.
Fog and modifier volumes are not enhancements: they are what the
PowerVR2 does, and a game that uses them looks wrong without them, with
no fog where there should be fog and no shadows where there should be
shadows. Both options defaulted to on and there is no reason to offer
"off".
The two options go, and settings.rend.Fog and
settings.rend.ModifierVolumes with them. The OpenGL, OpenGL per-pixel
and Vulkan renderers (both Vulkan drawers) always use the fog mode the
polygon asks for, always keep the fog table up to date, and always draw
the opaque and translucent modifier volumes.
The translated option tables still carry the two entries until they are
next regenerated; they are looked up by the English table's keys, so
they are not shown.
"Framerate" (Full Speed / Normal) chose when the core handed control
back to the frontend: after each rendered frame, or at each vblank. That
choice is gone. Since the threaded rendering rework every retro_run()
runs the machine to the next vblank and returns, with Threaded
Rendering on or off, with the frame deadline behind it for a game that
has the display off. The core never waits for the game to render.
What the option still set, settings.UpdateMode, was read in one place:
rend_end_sch(), and there only in a build made with NO_THREADS=1, which
no platform in the Makefile is. In every other build the option changed
nothing. In that one, its default (Full Speed) skipped the call that
presents the frame at the end of a render.
So the option is removed, with settings.UpdateMode. The per-game
override that forced it, settings.UpdateModeForced, goes too; the
updatemode_type column stays in rom_luts.h, unused, rather than every
row of the table being rewritten. rend_end_sch() presents
unconditionally.
The translated option tables still carry the entry until they are next
regenerated; they are looked up by the English table's keys, so it is
not shown.
Three were left in the core's own sources: the count of custom-texture
load requests in BaseTextureCacheData, the reset request flag, and an
unused <atomic> include in gles.h. They are retro_atomic like the rest;
the mirror of the texture counter in tools/threads/threads_test.cpp goes
the same way. No change in behaviour.
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.
The previous change routes non-threaded control-return through vblank in
os_DoEvents, which fixes the reported case (a static screen during disc loading
no longer blocks). But that still depends on the SPG raising vblank: rend_vblank
only runs when pvr_cur_scanline == SPG_VBLANK.vstart, so before video timing is
programmed, or if a title programs vstart out of range or a degenerate
Line_Cycles, vblank need not fire and dc_run() could still run unbounded.
Add a scheduler backstop that forces dc_stop() after at most SH4_MAIN_CLOCK/20
cycles (~2.5 frames), re-armed at every dc_run() entry. Under normal operation
vblank fires every frame, well inside that budget, so it never triggers; if
vblank stalls for any reason, dc_run() still returns and retro_run() is never
starved. Non-threaded only -- in threaded mode dc_run() is meant to loop
continuously on the emu thread, so the backstop is not armed there.
Verified by enumerating every CPU-stop source (dc_stop/Sh4_int_Stop): in
'Normal' mode with no rendering, none fired except the render-tied Present(),
confirming the reported stall; with these two changes control-return no longer
depends on either rendering or a well-behaved SPG.
dc_init() registers a vectored exception handler (AddVectoredExceptionHandler)
and a dynarec unwind function table (RtlAddFunctionTable, keyed to the code
cache) on every content load, but dc_term() removed neither. After Close
Content, dc_term() frees the code cache and vmem while both registrations still
point at that freed memory, so the next stack unwind or access violation -- even
one raised during teardown -- walks freed state inside the OS exception
machinery. That crashes on close/reload and is effectively undebuggable, since
the fault is in the unwinder itself rather than in steppable code. It only bites
on Win64 (VEH + RtlAddFunctionTable); other targets are unaffected.
Add common_libretro_cleanup(), called at the start of dc_term() before anything
is freed, to RemoveVectoredExceptionHandler() and RtlDeleteFunctionTable(). Keep
the VEH handle so it can be removed, make its install idempotent so reloads
don't stack handlers, and have setup_seh() drop any prior registration before
re-adding (the code cache is re-prepared each load).
Diagnosed by inspection; needs a Win64 runtime check to confirm.
emu_in_thread, performed_serialization and reset_requested are written on one
thread and read on the other (emu thread vs. the libretro thread that drives
serialize/reset/unload). They were plain bools, i.e. a data race / UB. Make
them std::atomic<bool>; the mutex handshakes still provide the ordering that
matters, so relaxed semantics are sufficient for these standalone flags.
wait_until_dc_running() and acquire_mainloop_lock() hot-spun for up to five
seconds, burning a full core. The first has no work to do until the emu thread
reaches dc_run; the second must keep kicking the emu thread out of its render
wait but not at spin speed. Back both off by a millisecond per iteration with
retro_sleep(1); timeout and kick semantics are unchanged.
Create maple devices after loading the game so the required arcade
controls are known (AW).
Map ftspeed and maxspeed accel and brake to triggers.
Issue #865
Use file extension to select the right disc driver
Better error reporting, less memory leaks and crashes on invalid content
Force HLE off when booting BIOS
Don't crash if missing BIOS file