Sio.cpp, MemoryCardProtocol.cpp, MultitapProtocol.cpp and MemoryCardFile.cpp
become C89 units, and their headers C ones every unit can read. Sio0 and
Sio2 are plain structs with functions (sio0_set_tx_data, sio2_write and so
on) in place of methods; a state carries both whole, so their members keep
their order and types and a build check holds them to the layout states
have had on 64-bit hosts. The byte FIFOs move to SioFifo.c with their
state freeze, which tests/savestate now exercises directly instead of a
copy. The memory card images keep their slots in plain arrays, and read
what they need of the configuration through three functions in
Pcsx2Config.cpp; the card type the configuration carried at run time is
gone with them, since an open card file is what makes a card present. The
pads' three SIO entry points get C linkage (PAD/PADsio.h), and
sioConvertPadToPortAndSlot hands its port and slot back through pointers.
tests/sio drives SIO0, SIO2, the cards and the multitap through every
command a game sends, PS2 and PS1 cards, keyed and unkeyed MagicGate, DMA
blocks, auto-eject, the Python 1 slot latch and a state saved mid-command,
and compares every byte that comes back, the card files and the SIO state
blocks with the transcript the C++ made: they are the same. The tests that
built the C++ files now build the C ones.
The vertex kick handlers come in two sets, plain and auto-flush, and
which one a draw kicks through is chosen when a register that decides it
is written -- TEX0 and TEX2, TEST, FRAME and ZBUF of the current context,
PRIM and PRMODE -- and changes only when the answer does: the draw state
reads what it draws. Every other draw kicks through the plain handlers,
which do not ask, where before every kick asked whenever auto flush was
on (by default in the software renderer, and in the hardware renderer
for every game the database turned it on for).
Auto flush is now always on, for every primitive, in both renderers.
The hardware level setting, the software switch, their core options and
the GameDB fix and its entries go.
tests/gsautoflush/autoflush_select pins the choice against the state
after every register write.
With the automatic game fixes off, an entry that sets eeDivRoundMode
and not eeRoundMode was reported by eeRoundMode's name: the mode left
at MaxCount indexed the four-entry name table one past its end, and
the pointer read there was printed as a string. The message for
applying it printed eeRoundMode's number the same way. Both now name
the division mode.
tests/patch's dbload lane applies every entry's fixes with the
automatic fixes on and off, as a VM start does, so a log line that
indexes a table with a mode the entry left unset shows up in the
sanitizer runs; it reported this one under UBSan.
The database was a std::unordered_map<std::string, GameEntry>, each
entry holding std::strings, std::vectors of fixes, hacks, memory card
filters and dynamic patches, and its own std::unordered_map of patch
blocks, with member functions over them and std::optional through the
parser.
Now the file is read into a few pools shared by every entry (text,
game fixes, speed hacks, GS fixes, patch blocks, dynamic patch
entries); while it is read an entry names its runs by offset, and
once the pools are final the entries are made with pointers into them.
Serials are found through an open-addressed index, built as the file
is read so a serial repeated in another case is still refused before
it is parsed. Memory card filters are stored already joined with '/'.
GameDatabase_FindGame/FindPatch/ApplyGameFixes/ApplyGSHardwareFixes
replace the namespace and members; DynamicPatch is a pair of pointer
and count runs; ParseSpeedHackName takes a pointer and a length. The
GS fixes skipped under manual hacks were also collected into a string
nothing read, which is gone.
Checked by dumping every one of the 12807 entries through both the old
and the new code, every field plus what both apply functions do with
the automatic fixes on and off: byte for byte the same, and clean under
ASan, UBSan and LeakSan across an unload and reload. tests/patch's
dbload lane now also loads a small database whose every field is known,
including the eight kinds of line that must be refused, and checks
what each field reads back as, lookups in any case, the default patch
block as fallback only when there is one, and unloading.
The game-database lane had stopped linking: its stubs for the three
GS lookup functions still had the std::string_view/std::optional
signatures from before GSHwHack.cpp took a pointer and length and
returned s16, so the suite failed on every run.
It also set SANFLAGS to empty before reading SANITIZER, discarding
the flags the CI matrix passes, so the ASan and UBSan jobs ran it
unsanitized. SANFLAGS is now kept and SANITIZER added to it.
With the flags honoured, the TSan build no longer linked:
libretro-common's .c files went through the C++ driver, which gives
retro_procbarrier's __tsan_acquire/__tsan_release declarations C++
linkage. They are built by the C compiler now, as in the core.
The logging facade in common/ -- IConsoleWriter and IDevConWriter,
Console and DevCon -- formatted every message into a heap std::string
and then handed it to the frontend's log_cb with "%s\n". One thousand
and six call sites call log_cb themselves now, with the level the
facade's method mapped to, read from its source rather than assumed:
Console.WriteLn is INFO and DevCon.WriteLn is DEBUG, but DevCon.Error
and DevCon.Warning were ERROR and WARN, and stay so. The newline the
facade appended is in each format string; the eighteen literals that
already ended in one keep the blank line they produced. The colour
overloads discarded their colour, and the argument is gone with them.
log_cb is one symbol with C linkage, declared in Pcsx2Defs.h for the
C++ translation units and in ATA.h for the C ones, which called the
facade through a C shim that appended nothing -- and whose callers,
despite its comment, put no newline in either; they have one now. It
was also called unguarded by every log line, and a frontend that offers
no log interface left it null, a crash at the first message: main.cpp
installs a stderr fallback in that case.
Ninety-four files had Pcsx2Defs.h only through Console.h and include it
directly, by the tree's rooted spelling. The patch harnesses, which
counted errors and warnings through the facade's methods, count them
through a log_cb that dispatches on level, and the settings harness
provides one.
Full default build links; all thirteen suites pass; every touched C++
file is clean for Windows, macOS and iOS shapes with
-Winfinite-recursion -- the iOS shape caught a colour variable the arm64
recompiler left behind, which no Linux build compiles -- and every
touched C file under the C89 lane's flags; the added includes sit at
depth zero; no file references anything removed.
The last of common/Threading.h. What it held was KernelSemaphore and
UserspaceSemaphore, and they left together, because on x86 the second
parks on the first and the second's one user is the VU1-to-GS packet
handoff, semaXGkick.
semaXGkick counted: the VU1 thread posted once per path-1 packet, the GS
took one credit per packet it popped. Every post sits next to a push
into gsPackQueue, on both paths that push, so the count was a second
copy of that queue's occupancy -- one a reset could let drift, where the
queue cannot. The handoff is retro_asym_eventcount now, with
GetPendingGSPackets as the condition and the pop as the consume: the VU1
side pays a release store per packet instead of a lock xadd, and the GS
side spins its budget on the occupancy, then registers, re-checks and
parks. Measured per handoff pair on x86: 15.8 ns to 1.5 ns. aarch64
gives up its WFE/SEV park for the eventcount's; the GS reaches the park
only after the spin budget, so that is the rare case, and it was
measured on x86 only.
With the header deleted, twenty-eight files that included it -- most by
relative path -- lost what they had been getting through it, and the
build named each: __POSIX__ from Pcsx2Defs.h in sockets.cpp, ordered
after the block that tests it; retro_atomic.h in HostSys.cpp; and
<unordered_map> in IopBios.cpp. Threads.cpp went with it, and
Semaphore.cpp.
The unordered_map was the IOP import-stub cache, hit millions of times a
minute, and it is a fixed open-addressing table now, C89-shaped: an
array of records, Fibonacci hashing on pc >> 2, linear probing, no
allocation, no pointer chasing. libstdc++'s find for a u32 key divides
by the bucket count -- a 64-bit DIV, twice on the general path, 110
instructions -- and walks a heap-allocated node list; the replacement is
22 instructions, one imul, one indexed load. Erase was only ever
followed by re-inserting the same key, so it is an overwrite; a full
probe window overwrites its first slot, which a cache may. On 300 stubs
laid out as an import table lays them: zero collisions, 3.3 ns to 1.6 ns
per hit, 23 ns to 1.4 ns per stale overwrite.
The resolve path behind that cache took the module name as a
std::string built from the table's 8-byte field and compared it against
each module name in a chain. The name is two words now -- irxImportName
reads the field, zeroes everything after the first NUL as the old string
read did, and packs it little-endian -- and each MODULE(n) compares them
against constants folded from the literal at compile time: 'sysm',
'load', 'ioma', 'intr', 'sifc' are immediates in the object, and no
string-compare call remains in the lookups. IopBios.h no longer names
std::string, and it includes what it uses. The JIT's psxRecompileIrxImport
calls the word form directly, so irxImportHLECh goes; irxImportFuncname,
irxImportLog and irxImportLog_rec had no callers and go with it, and
IopModuleNames.cpp, which only irxImportFuncname included, is deleted.
Every touched TU was compiled for Linux through the Makefile,
syntax-checked for Windows with MinGW x86_64 and in an Apple shape, and
every added include sits at preprocessor depth zero. The full default
build links; all twelve suites pass, the patch harness having lost its
Semaphore.cpp link entry; and a whole-tree search finds no reference to
anything removed. One thing for anyone building incrementally: the
multi-ISA rasterizer objects and the .d files do not track the deleted
header, and need a clean.
Companion to dbparse, which covers the patch lines. This covers the rest
of GameIndex.yaml -- the game fixes, speed hacks, GS hardware fixes,
rounding and clamping modes and the memory card filters -- by linking
pcsx2/GameDatabase.cpp and the real yaml reader and requiring the load to
produce no errors and no warnings. It spot-checks three serials afterwards
so an empty parse cannot pass by reporting nothing.
It catches the failure that motivated it: renaming a gamefix so the parser
no longer knows it -- XGKickHackk -- is reported and fails the run. That
is the WRC-class shape, a name in the database the code has stopped
recognising, dropped with a console error and no other consequence.
Two silent failures it does not catch, both checked rather than assumed
and both in the header. Renaming a key -- speedHacks to speedHackss --
makes the entire block invisible with nothing reported, because an
unrecognised key is skipped rather than flagged. And an out-of-range
value, autoFlush: 99, loads without complaint; the numeric gsHWFixes are
range-checked nowhere. Closing either means the parser reporting unknown
keys and validating ranges, which is a change to GameDatabase.cpp, not to
a test.
One more gap is structural: the three gsHWFixes fields whose values are
function names are looked up in tables in GSHwHack.cpp, and linking that
pulls in the GS renderer -- sixty symbols for three fields. They are
stubbed, so 349 lines in the database go unvalidated by this, and the
header says which.