Skip to content

scan: match arcade sets by a fingerprint of their member CRCs - #19948

Open
Davey-Hughes wants to merge 3 commits into
libretro:masterfrom
Davey-Hughes:scan-fingerprint
Open

Davey-Hughes wants to merge 3 commits into
libretro:masterfrom
Davey-Hughes:scan-fingerprint

Conversation

@Davey-Hughes

@Davey-Hughes Davey-Hughes commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Guidelines

  1. Rebase before opening a pull request
  2. If you are sending several unrelated fixes or features, use a branch and a separate pull request for each
  3. If possible try squashing everything in a single commit. This is particularly beneficial in the case of feature merges since it allows easy bisecting when a problem arises
  4. RetroArch codebase follows C89 coding rules for portability across many old platforms check using C89_BUILD=1

Description

A follow-up to #19944. The arcade databases key each set on the CRC of the whole TorrentZipped archive, so the same ROMs packed any other way (zstd or 7z, other member names or order, a merged parent) never match. On a NAS share of 2592 merged FBNeo sets packed as RVZSTD zips, master writes no FBNeo entry; with this PR and a keyed FBNeo database it writes 2309.

Two optional record keys carry a fingerprint of the set's member CRCs. set_fp1 is the CRC-32 of a set's ROM CRCs, de-duplicated, sorted and taken as big-endian bytes; set_fpm1 is the same for a merged parent. The scan's one directory walk per archive already has the CRCs, so a fingerprint match reads no member data. The keys come from libretro/libretro-database#1752; until both land, nothing changes.

Commit 1 maps the keys in c_converter and the Lua converter. Commit 2 makes the change, with its tests. Commit 3 gives no fingerprint to a rar that leaves out members it cannot unpack.

What changes

  • An archive of several members is looked up by its fingerprint first, before scan: read an archive whole only when a database may hold its size #19944's size gate. An archive of one member is looked up by it only after its member's CRC missed, so a first-member hit held for a cartridge database still wins.
  • Each database whose cores open the archive's type and that carries the keys is asked, through an index built like the crc one. A hit writes the entry a whole-CRC match would, a bare set.zip, with no whole read.
  • Folders, empty members and cue or gdi sheets are left out of the fingerprint. An archive with a member that stores no CRC has no fingerprint, nor does a rar that leaves out members it cannot unpack (solid, encrypted, older packers). So that folders do not look like such gaps, 7z and rar now name them as zip does, ending in /, when the scan's walk asks.
  • Where records share a fingerprint, as a set's split and non-merged records can, the one with the file's size is taken; with several of that size the archive's own CRC decides; with none (a repack), the one named like the archive, else the first.
  • scan: read an archive whole only when a database may hold its size #19944's readers read TorrentZip sets whole without a grant; now only while no database that opens zip carries fingerprints, so those databases' fingerprint indexes are built, under the scan's budget, before the readers start. An empty one is freed, a keyed one is charged to the index budget, and one that cannot be built counts as carrying none, so the readers still read whole.
  • Only FBNeo is keyed: MAME and HBMAME databases are matched by members, never by the archive's name, so keys there would be inert.

Numbers

A Linux desktop, master 3195d95 (#19944 as merged) against this PR. "Keyed" means FBNeo - Arcade Games.rdb built from the keyed dats. All runs were under load (1-minute load 13.9-25 on 32 cores), so times are rough; counts and playlists are the firm result.

Merged RVZSTD zips on a NAS, over NFS, 2592 files:

build, rdb FBNeo entries whole reads wall
master, keyed 0 35 34.05 s cold, 2.18 s re-run
this PR, stock 0 35 3.37 s
this PR, keyed 2309 5 3.59 s

All 2309 are fingerprint matches, each a bare set.zip. These runs predate the name step above; at the final head the counts are the same (2.42 s, warm), and countrun.zip and fightfev.zip move from a clone's record to their own.

596 TorrentZipped FBNeo parents on local disk, 3 runs each:

build, rdb by fingerprint by whole CRC whole reads scan
master, stock - 596 596 101.0-111.0 ms
master, keyed - 596 596 99.1-108.5 ms
this PR, stock 0 596 596 101.5-109.7 ms
this PR, keyed 595 1 1-2 88.8-90.1 ms

These runs predate the shared-fingerprint rule. At the final head, three keyed runs give the same matches and 2 whole reads in 90.7-91.0 ms, and the playlist equals the stock rdb's, 596/596 including crc32.

50 FBNeo sets as 7z, solid and non-solid: 50 of 50 by fingerprint, 0 whole reads, each a bare set.7z.

Compatibility

  • RetroArch 1.22.2, 91dbd31 (before scan: read an archive whole only when a database may hold its size #19944) and master skip the keys: each writes the same playlist for the 596 parents with the keyed rdb as with the stock one. This PR with the stock rdb writes master's playlist.
  • An older c_converter drops the keys. This one keeps them, byte-equal to a separate Python writer's rdb, and builds the stock rdb from a dat without them. The Lua rows are checked by parse and syntax-compile only: lua_converter does not link at master either.
  • FBNeo - Arcade Games.rdb grows from 2215401 to 2421986 bytes (+9.3 %).

Notes for review

  • 473 fingerprints in the keyed FBNeo rdb are shared; size separates 471 (drgw2100j and drgw2101j have equal sizes, so the whole read decides).
  • The first database that hits ends the archive, so a later keyed database holding the same fingerprint would win over a tie's whole read. Only FBNeo is keyed.
  • Six records (biomtoyc, bioplayc, bjtwinp, snowboara, sf2re, sf2mkot) serve both dats from one zip line and keep the member dat's keys, so a repack of their split content misses by fingerprint, with no false match.
  • Under "scan without core match" every database opens zip, so the index walk at scan start covers every rdb, MAME's included.

Later, each its own change: a BIOS key (set_fpb1), clone entries for merged zips, subset matching for incomplete sets (205 of the 2586 merged sets on the NAS), one index walk for both keys (FBNeo is walked twice per scan today), and MAME and HBMAME keys.

Testing

  • Linux: database_scan_test (392 checks; 329 on master) and database_index_test (45; 38 on master) pass, also under ASan, TSan, HAVE_THREADS=0 (369 checks), readers off (370), with two runs in one root and in 5 runs beside 12 busy loops. New cases cover zip, 7z and rar sets, repacks, one-member and cartridge-first archives, a database matched by members, shared fingerprints, read-ahead and a refused index.
  • Windows, mingw-w64 (MSYS2 UCRT64): the same 392 scan and 45 index checks and the archive-zip test pass, built without warnings.
  • C89_BUILD=1 compiles the touched objects without warnings. Default and --disable-threads builds link, and the libretro-db parser test passes.
  • Before the shared-fingerprint rule (378 scan checks), and not run since: with tools/ci_local.py, the samples-tasks, libretro-db, libretro-common, cdfs-io and settings jobs, the xenial i386 build, linux-nomenu and 12 tree-wide checks. All passed. Five more fail on this host only, as at master (missing mingw compilers, ps4/ps5 units, the menu hot-function budget, pipewire headers under C89).
  • Not run here: Android, the GPU and display jobs (samples-gfx, vulkan-resize, overlay, wayland-input, samples-audio, wine-input/menu) and vulkan-validation-harness. They first run in this PR's CI.

Related Issues

[Any issues this pull request may be addressing]

Related Pull Requests

[Any other PRs from related repositories that might be needed for this pull request to work]

Reviewers

[If possible @mention all the people that should review your pull request]

libretro-database can give the arcade records a set fingerprint: the
CRC-32 of a set's ROM CRCs, de-duplicated, sorted and taken as
big-endian bytes, and the same for a merged parent.  Map both keys in
c_converter and the Lua converter so the rdb keeps them.  A converter
without these rows drops them, as it drops every key it does not map,
and readers that do not know them skip them.
The arcade databases key each set on the CRC of the whole
TorrentZipped archive, so the same ROMs packed any other way - zstd
or 7z, other names or order, a merged parent - never match.

An optional set_fp1 key holds the CRC-32 of the set's ROM CRCs,
de-duplicated, sorted and taken as big-endian bytes; set_fpm1 holds
the same for a merged parent.  The archive's one directory walk gives
its fingerprint, leaving out folders, empty members and cue or gdi
sheets; a member that stores no CRC leaves it without one.  Each
database whose cores open the archive's type and that carries the
keys is looked up through an index built like the crc one.  A hit
writes the record's entry, as a match on the archive's own CRC does,
and no archive is read whole for it.

An archive of several members is looked up by its fingerprint before
its size is.  An archive of one member is looked up by it only after
its member's CRC missed, so a first-member hit held for a cartridge
database still wins.

Where several records of a database share the fingerprint, as a set's
split and non-merged packings can, the one whose size is the archive's
is taken; where several have that size the archive's own CRC decides,
and where none does, the one named like the archive, else the first.

The readers read a TorrentZip set whole with no grant only while no
database that opens zip carries fingerprints, so the fingerprint
indexes of those databases are built before the readers start.  One
that cannot be built counts as carrying none: that database can match
the set only by its own CRC.  A database without the keys costs one
walk per scan; older versions skip the keys.
rar names no member it cannot unpack (solid, encrypted, an older
packer), so a fingerprint taken over the members it does name would
claim a set that may not be all there.  Such an archive now has none
and goes on to its other passes.

7z and rar also name no folder, which would look the same, so the
scan's directory walk asks them to name one as zip does, ending in
'/'.  Nothing else sets the flag.
@RobLoach

Copy link
Copy Markdown
Member

This is a big amount of changes. Would this recognize romsets across split, non-merged, full non-merged, and merged? There are existing .dat files that change regularly that currently work with manual scan always, so I'm worried that the archived sets would get out of date quickly.

@Davey-Hughes

Davey-Hughes commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

The only thing that this should change is that FBNeo roms that are archived with different formats (7z, zstd) are able to be scanned now.

Basically the existing database just stored a CRC of the entire torrentzip archive, so if you had exactly that archive, it would match. With fingerprinting, it instead is able to match based on the CRC of each item in the archive.

Also when I updated the dat to the latest one in the database repo, it had like 99.99% overlap from the last time it was updated. So yes the dats are updated often but in very small ways, and the script in the PR for the database repo can keep the fingerprint field up to date when those dats are updated.

The new fields for fingerprint CRCs are optional and only additive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants