mirror of
https://github.com/libretro/parallel-ps2.git
synced 2026-11-10 03:11:38 -10:00
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.
351 lines
13 KiB
C++
351 lines
13 KiB
C++
/* The whole game database, through the real parser.
|
|
*
|
|
* Companion to dbparse.cpp, which covers the patch lines. This covers
|
|
* everything else in bin/resources/GameIndex.yaml: the game fixes, speed
|
|
* hacks, GS hardware fixes, rounding and clamping modes and the memory
|
|
* card filters.
|
|
*
|
|
* The failure it exists to catch is the same one that hid the "bytes"
|
|
* operand size. A name the parser does not recognise is reported with a
|
|
* console error and then dropped -- the entry loses its fix and the game
|
|
* quietly misbehaves, with nothing failing and nothing to notice. A
|
|
* rename on either side of the database, or a fix removed from the enum
|
|
* while entries still ask for it, looks exactly like that.
|
|
*
|
|
* So this parses every entry and requires no errors and no warnings. It
|
|
* links pcsx2/GameDatabase.cpp and the real YAML reader rather than
|
|
* reimplementing either.
|
|
*
|
|
* What it catches, checked by corrupting the database and re-running:
|
|
* renaming a gamefix so the parser no longer knows it -- "XGKickHackk"
|
|
* -- is reported and fails the run. That is the WRC-class failure, a
|
|
* name in the database that the code has stopped recognising.
|
|
*
|
|
* What it does not catch, and both are worth knowing:
|
|
* - a renamed key. Changing "speedHacks:" to "speedHackss:" makes the
|
|
* whole block invisible and the parser says nothing, because an
|
|
* unrecognised key is skipped rather than reported. Every entry
|
|
* under it is silently lost.
|
|
* - an out-of-range value. "textureInsideRT: 99" loads without complaint;
|
|
* the numeric gsHWFixes are range-checked nowhere.
|
|
*
|
|
* Both are real silent failures this does not close. Catching them means
|
|
* the parser reporting unknown keys and validating ranges, which is a
|
|
* change to GameDatabase.cpp rather than to a test.
|
|
*
|
|
* Usage: tests/patch/dbload <path-to-GameIndex.yaml>
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <libretro.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Common.h"
|
|
#include "GameDatabase.h"
|
|
#include <formats/ryaml.h>
|
|
|
|
static int g_errors = 0;
|
|
static int g_warnings = 0;
|
|
static int g_shown = 0;
|
|
|
|
static void note(const char* kind, const char* fmt, va_list a)
|
|
{
|
|
if (g_shown++ < 8)
|
|
{
|
|
printf(" %s: ", kind);
|
|
vprintf(fmt, a);
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
static void test_log(enum retro_log_level level, const char* fmt, ...)
|
|
{
|
|
va_list a;
|
|
if (level != RETRO_LOG_ERROR && level != RETRO_LOG_WARN)
|
|
return;
|
|
va_start(a, fmt);
|
|
if (level == RETRO_LOG_ERROR) { g_errors++; note("error", fmt, a); }
|
|
else { g_warnings++; note("warning", fmt, a); }
|
|
va_end(a);
|
|
}
|
|
extern "C" retro_log_printf_t log_cb = test_log;
|
|
|
|
/* The database reads its yaml through this. */
|
|
static std::string s_yaml;
|
|
namespace Host
|
|
{
|
|
std::optional<std::string> ReadResourceFileToString(const char* name)
|
|
{
|
|
(void)name;
|
|
return s_yaml;
|
|
}
|
|
}
|
|
|
|
/* The three gsHWFixes fields whose values are function names --
|
|
* getSkipCount, beforeDraw and moveHandler -- are looked up in tables that
|
|
* live in GSHwHack.cpp, and linking that pulls in the GS renderer: sixty
|
|
* more symbols for three fields. They are stubbed to a valid id here, so
|
|
* this test does not validate those names. That leaves 349 lines in the
|
|
* database unchecked, and they are the ones most likely to break on a GS
|
|
* refactor, so it is worth knowing they are not covered rather than
|
|
* assuming a green run includes them. */
|
|
s16 GSLookupGetSkipCountFunctionId(const char*, size_t)
|
|
{ return 0; }
|
|
s16 GSLookupBeforeDrawFunctionId(const char*, size_t)
|
|
{ return 0; }
|
|
s16 GSLookupMoveHandlerFunctionId(const char*, size_t)
|
|
{ return 0; }
|
|
|
|
/* Memory card naming, reached for the memcardFilters field; C, as
|
|
* MemoryCardFile.c is. */
|
|
extern "C" {
|
|
void FileMcd_GetDefaultName(char* buf, size_t n, unsigned) { if (n) buf[0] = 0; }
|
|
unsigned FileMcd_GetMtapPort(unsigned) { return 0; }
|
|
unsigned FileMcd_GetMtapSlot(unsigned) { return 0; }
|
|
bool FileMcd_IsMultitapSlot(unsigned) { return false; }
|
|
}
|
|
|
|
/* A small database whose every field is known, loaded in place of the
|
|
* real one: what each field reads back as, lookups in either case, the
|
|
* first of two serials that differ only in case winning, the default
|
|
* patch block as the fallback and only when there is one, and the eight
|
|
* lines that must be refused and reported. */
|
|
static const char s_fixture[] =
|
|
"SLUS-00001:\n"
|
|
" name: \"Fixture One\"\n"
|
|
" region: \"NTSC-U\"\n"
|
|
" roundModes:\n"
|
|
" eeRoundMode: 0\n"
|
|
" vuRoundMode: 3\n"
|
|
" vu1RoundMode: 1\n"
|
|
" clampModes:\n"
|
|
" eeClampMode: 2\n"
|
|
" vuClampMode: 1\n"
|
|
" gameFixes:\n"
|
|
" - XGKickHack\n"
|
|
" - SkipMPEGHack\n"
|
|
" - XGKickHack\n"
|
|
" - NotAFixHack\n"
|
|
" speedHacks:\n"
|
|
" mtvu: 0\n"
|
|
" eeCycleRate: 2\n"
|
|
" bogus: 1\n"
|
|
" gsHWFixes:\n"
|
|
" textureInsideRT: 2\n"
|
|
" mipmap: 1\n"
|
|
" halfPixelOffset: x\n"
|
|
" nonsense: 1\n"
|
|
" getSkipCount: \"SomeFunc\"\n"
|
|
" memcardFilters:\n"
|
|
" - \"SLUS-00001\"\n"
|
|
" - \"SLUS-00002\"\n"
|
|
" patches:\n"
|
|
" default:\n"
|
|
" content: |-\n"
|
|
" patch=1,EE,00000001,word,1\n"
|
|
" ABCD1234:\n"
|
|
" content: |-\n"
|
|
" patch=1,EE,00000002,word,2\n"
|
|
" abcd1234:\n"
|
|
" content: |-\n"
|
|
" patch=1,EE,00000009,word,9\n"
|
|
" zz:\n"
|
|
" content: |-\n"
|
|
" patch=1,EE,00000009,word,9\n"
|
|
" dynaPatches:\n"
|
|
" - pattern:\n"
|
|
" - offset: 0\n"
|
|
" value: 1\n"
|
|
" - offset: 4\n"
|
|
" value: 2\n"
|
|
" replacement:\n"
|
|
" - offset: 8\n"
|
|
" value: 3\n"
|
|
"slus-00001:\n"
|
|
" name: \"Duplicate\"\n"
|
|
"SLES-00002:\n"
|
|
" name: \"No Default\"\n"
|
|
" roundModes:\n"
|
|
" eeDivRoundMode: 2\n"
|
|
" patches:\n"
|
|
" 11111111:\n"
|
|
" content: |-\n"
|
|
" patch=0,EE,00000003,word,3\n";
|
|
|
|
static int s_fixture_fails;
|
|
#define FCHECK(c, what) do { if (!(c)) { printf(" FAIL: %s\n", what); s_fixture_fails++; } } while (0)
|
|
|
|
static int fixture(void)
|
|
{
|
|
const GameDatabaseEntry* a;
|
|
const GameDatabaseEntry* b;
|
|
const int errors_before = g_errors;
|
|
const char* p;
|
|
|
|
s_yaml = s_fixture;
|
|
g_shown = 8; /* the refusals are expected; count them, do not print */
|
|
GameDatabase_Unload();
|
|
a = GameDatabase_FindGame("SLUS-00001");
|
|
b = GameDatabase_FindGame("SLES-00002");
|
|
|
|
FCHECK(g_errors - errors_before == 8, "the eight bad lines are each reported");
|
|
FCHECK(a && b, "both entries load");
|
|
if (!a || !b)
|
|
return 1;
|
|
FCHECK(GameDatabase_FindGame("slus-00001") == a && GameDatabase_FindGame("sLuS-00001") == a,
|
|
"a serial is found in any case");
|
|
FCHECK(!strcmp(a->name, "Fixture One") && !strcmp(a->region, "NTSC-U"),
|
|
"the first of two serials differing in case is the one kept");
|
|
FCHECK(!strcmp(a->serial, "slus-00001"), "the serial is stored lower case");
|
|
FCHECK(GameDatabase_FindGame("SLUS-99999") == NULL && GameDatabase_FindGame("") == NULL,
|
|
"an unknown serial is not found");
|
|
FCHECK(a->eeRoundMode == FPRoundMode::Nearest && a->eeDivRoundMode == FPRoundMode::MaxCount
|
|
&& (int)a->vu0RoundMode == 3 && (int)a->vu1RoundMode == 1,
|
|
"round modes: vuRoundMode sets both, vu1RoundMode then overrides");
|
|
FCHECK((int)a->eeClampMode == 2 && (int)a->vu0ClampMode == 1 && (int)a->vu1ClampMode == 1,
|
|
"clamp modes");
|
|
FCHECK(a->nGameFixes == 2 && a->gameFixes[0] == Fix_XGKick && a->gameFixes[1] == Fix_SkipMpeg,
|
|
"game fixes in order, the repeat and the unknown one dropped");
|
|
FCHECK(a->nSpeedHacks == 2 && a->speedHacks[0].id == SpeedHack::MTVU && a->speedHacks[0].value == 0
|
|
&& a->speedHacks[1].id == SpeedHack::EECycleRate && a->speedHacks[1].value == 2,
|
|
"speed hacks in order, the unknown one dropped");
|
|
FCHECK(a->nGSHWFixes == 3
|
|
&& a->gsHWFixes[0].id == GameDatabaseSchema::GSHWFixId::TextureInsideRT && a->gsHWFixes[0].value == 2
|
|
&& a->gsHWFixes[1].id == GameDatabaseSchema::GSHWFixId::Mipmap && a->gsHWFixes[1].value == 1
|
|
&& a->gsHWFixes[2].id == GameDatabaseSchema::GSHWFixId::GetSkipCount,
|
|
"GS fixes in order, a bad value and an unknown name dropped");
|
|
FCHECK(!strcmp(a->memcardFilters, "SLUS-00001/SLUS-00002") && !strcmp(b->memcardFilters, ""),
|
|
"memory card filters joined with '/', empty when there are none");
|
|
p = GameDatabase_FindPatch(a, 0xABCD1234u);
|
|
FCHECK(p && strstr(p, "00000002"), "the CRC's own patch block, the case-repeat of its CRC dropped");
|
|
p = GameDatabase_FindPatch(a, 0x99u);
|
|
FCHECK(p && strstr(p, "00000001"), "the default block for any other CRC");
|
|
FCHECK(GameDatabase_FindPatch(a, 0) == NULL, "no patch block for a CRC of 0");
|
|
FCHECK(GameDatabase_FindPatch(b, 0x99u) == NULL, "no fallback without a default block");
|
|
p = GameDatabase_FindPatch(b, 0x11111111u);
|
|
FCHECK(p && strstr(p, "00000003"), "a CRC-only entry's block");
|
|
FCHECK(a->nDynaPatches == 1 && a->dynaPatches[0].npattern == 2 && a->dynaPatches[0].nreplacement == 1
|
|
&& a->dynaPatches[0].pattern[1].offset == 4 && a->dynaPatches[0].pattern[1].value == 2
|
|
&& a->dynaPatches[0].replacement[0].offset == 8 && a->dynaPatches[0].replacement[0].value == 3,
|
|
"a dynamic patch's pattern and replacement");
|
|
FCHECK(b->nDynaPatches == 0 && b->nPatches == 1, "the other entry has only its own");
|
|
{
|
|
Pcsx2Config on, off;
|
|
FCHECK(GameDatabase_ApplyGameFixes(a, on, true) == 7, "seven fixes apply: three clamps, two hacks, two game fixes");
|
|
FCHECK(GameDatabase_ApplyGameFixes(a, off, false) == 0, "none with the automatic fixes off");
|
|
/* the division mode alone, with the fixes off: logged by its own name */
|
|
GameDatabase_ApplyGameFixes(b, off, false);
|
|
GameDatabase_ApplyGameFixes(b, on, true);
|
|
}
|
|
|
|
GameDatabase_Unload();
|
|
FCHECK(GameDatabase_FindGame("SLUS-00001") != NULL, "a lookup after unloading loads again");
|
|
GameDatabase_Unload();
|
|
|
|
printf("dbload: fixture %s\n", s_fixture_fails ? "FAIL" : "ok");
|
|
return s_fixture_fails;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
const char* path = (argc > 1) ? argv[1] : "bin/resources/GameIndex.yaml";
|
|
FILE* f = fopen(path, "rb");
|
|
long size;
|
|
int entries = 0;
|
|
|
|
if (!f) { fprintf(stderr, "cannot open %s\n", path); return 2; }
|
|
fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET);
|
|
s_yaml.resize((size_t)size);
|
|
if (fread(&s_yaml[0], 1, (size_t)size, f) != (size_t)size)
|
|
{ fclose(f); fprintf(stderr, "short read\n"); return 2; }
|
|
fclose(f);
|
|
|
|
GameDatabase_EnsureLoaded();
|
|
|
|
/* Spot-check that entries actually came out, so an empty parse cannot
|
|
* pass by reporting nothing. WRC 4's serial is the one this suite grew
|
|
* out of; the other two are an ordinary retail entry and one carrying
|
|
* a gsHWFixes block. */
|
|
{
|
|
static const char* kSerials[] = {
|
|
"SCES-52389", /* WRC 4, gameFixes + patches */
|
|
"SLUS-20062", /* a plain entry */
|
|
"SCED-52880", /* WRC 4 demo, gsHWFixes + roundModes */
|
|
};
|
|
size_t i;
|
|
for (i = 0; i < sizeof(kSerials)/sizeof(kSerials[0]); i++)
|
|
{
|
|
const GameDatabaseEntry* e =
|
|
GameDatabase_FindGame(kSerials[i]);
|
|
if (e) entries++;
|
|
else
|
|
{
|
|
printf(" %s is not in the loaded database\n", kSerials[i]);
|
|
g_errors++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Every entry's fixes applied with the automatic fixes on and off, as
|
|
* a VM start does: a log line that indexes a name table with a mode
|
|
* the entry left unset reads past it, which the sanitizer lanes see. */
|
|
{
|
|
ryaml_t* yaml = ryaml_parse_ex(s_yaml.data(), s_yaml.size(), NULL, NULL);
|
|
int n, applied = 0;
|
|
const int errors = g_errors;
|
|
for (n = ryaml_first_child(yaml, ryaml_root(yaml)); yaml && n >= 0; n = ryaml_next_sibling(yaml, n))
|
|
{
|
|
size_t len = 0;
|
|
const char* key = ryaml_key(yaml, n, &len);
|
|
std::string serial(key, len);
|
|
const GameDatabaseEntry* e = GameDatabase_FindGame(serial.c_str());
|
|
if (!e)
|
|
continue;
|
|
{
|
|
Pcsx2Config on, off;
|
|
Pcsx2Config::GSOptions gs_auto, gs_manual;
|
|
gs_manual.ManualUserHacks = true;
|
|
GameDatabase_ApplyGameFixes(e, on, true);
|
|
GameDatabase_ApplyGameFixes(e, off, false);
|
|
GameDatabase_ApplyGSHardwareFixes(e, gs_auto);
|
|
GameDatabase_ApplyGSHardwareFixes(e, gs_manual);
|
|
}
|
|
applied++;
|
|
}
|
|
ryaml_free(yaml);
|
|
/* the "off" passes warn by design; only errors count here */
|
|
g_warnings = 0;
|
|
if (g_errors != errors)
|
|
printf(" applying the fixes logged %d errors\n", g_errors - errors);
|
|
printf("dbload: fixes applied both ways for %d entries\n", applied);
|
|
if (applied < 10000)
|
|
{
|
|
printf(" expected the whole database\n");
|
|
g_errors++;
|
|
}
|
|
}
|
|
|
|
printf("dbload: %d errors, %d warnings, %d/3 spot-checked serials found\n",
|
|
g_errors, g_warnings, entries);
|
|
if (g_errors != 0 || entries != 3)
|
|
return 1;
|
|
return fixture() ? 1 : 0;
|
|
}
|
|
|
|
std::string libretro_content;
|
|
|
|
|
|
/* Reached from Pcsx2Config's paths and the memory map; neither is entered
|
|
* by a database load. */
|
|
extern "C" {
|
|
/* path_is_valid and path_mkdir are the two file_path.c leaves out of
|
|
* this build; the rest of that file links. */
|
|
int path_is_valid(const char*) { return 0; }
|
|
int path_mkdir(const char*) { return 0; }
|
|
}
|
|
void vtlb_Alloc_Ppmap(void) { }
|