Files
libretro/wiki/verification-modes.md
T

26 KiB

level
level
reference

How each platform checks files

Each platform verifies BIOS files differently. verify.py replicates the native behavior of each platform so that verification results match what the platform itself would report.

Existence mode

Platforms: RetroArch, Lakka, RetroPie, MinUI, NextUI, MyMinUI

Source: RetroArch core_info.c, function path_is_valid(); minarch for the MinUI family

The most straightforward mode. A file is OK if it exists at the expected path. No hash is checked. Any file with the correct name passes, regardless of content.

Condition Status Severity (required) Severity (optional)
File present OK OK OK
File missing MISSING WARNING INFO

RetroArch does not distinguish between a correct and an incorrect BIOS at the verification level. A corrupt or wrong-region file still shows as present. This is by design in the upstream code: core_info.c only calls path_is_valid() and does not open or hash the file.

Lakka and RetroPie inherit this behavior through platform config inheritance (inherits: retroarch in the platform YAML).

MinUI, NextUI and MyMinUI check nothing at all: minarch returns the folder Bios/<TAG>/ for RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY and never opens a file in it (MinUI workspace/all/minarch/minarch.c:1889-1893 and :2943). The core decides what it loads, so the files are the ones its profile documents, placed below each folder a pak hands it (core_system_dirs in the platform YAML).

MD5 mode

Platforms: Batocera, RetroBat, Recalbox, EmuDeck, RetroDECK, RomM, ROCKNIX, KNULLI, AmberELEC, EmuELEC, MiSTer FPGA

All MD5-mode platforms compute a hash of the file and compare it against an expected value. The details vary by platform.

Standard MD5 (Batocera, RetroBat)

verify.py replicates Batocera's md5sum() function. The file is read in binary mode, hashed with MD5, and compared case-insensitively against the expected value.

Condition Status Severity (required) Severity (optional)
Hash matches OK OK OK
File present, hash differs UNTESTED WARNING WARNING
File missing MISSING CRITICAL WARNING

If the resolve_local_file step already confirmed the MD5 match (status md5_exact), verify.py skips re-hashing and returns OK directly.

MD5 shorter than 32 characters (Batocera)

Batocera's script compares the full string (batocera-systems:1088), so an MD5 shorter than 32 characters matches no file there. The Batocera scraper completes such a value when exactly one MD5 in database.json starts with it (_resolve_truncated_md5 in scripts/scraper/batocera_scraper.py) and keeps it as written otherwise. resolve_local_file matches a value still shorter than 32 characters by prefix, and only for a file whose name the entry also names.

md5_composite (Recalbox ZIP verification)

Recalbox hashes a ZIP with Zip::Md5Composite (es-core/src/utils/Zip.cpp:71-96 in recalbox-emulationstation at f4cd0ae4): one MD5 over the contents of every member, read in filename order. The members' own MD5s are not involved, and neither is the MD5 of the ZIP file itself: Bios::Scan takes the composite for every .zip (es-app/src/bios/Bios.cpp:64-68). verify.py computes the same value with md5_composite() (scripts/hashing.py).

The registry gives Recalbox zip_digest: composite. For a platform so marked, a .zip entry is checked against its composite alone, by the resolver (resolve_local_file) and by the pack check (digest_verdict in scripts/validation.py). On other platforms the ZIP file's direct MD5 is tried first and the composite after it.

Multi-hash (Recalbox)

Recalbox allows comma-separated MD5 values for a single file entry, accepting any one of them as valid. verify.py splits on commas and tries each hash. A match against any listed hash is OK.

Mandatory levels (Recalbox)

Recalbox colours each entry of es_bios.xml from two attributes, mandatory and hashMatchMandatory, both true unless written "false" (es-app/src/bios/Bios.cpp:162-163 in recalbox-emulationstation at f4cd0ae4, colours set by Bios::Scan at lines 84-108). The platform YAML carries them as required and, when the attribute is written, hash_match_mandatory.

File mandatory hashMatchMandatory Recalbox verify.py
hash matches (any) (any) GREEN OK
missing true (any) RED CRITICAL
missing false (any) YELLOW WARNING
hash differs true true RED WARNING
hash differs true false YELLOW WARNING
hash differs false (any) YELLOW WARNING

verify.py applies the MD5-mode table above and does not read hash_match_mandatory, so a present file whose hash differs is a WARNING where Recalbox shows RED for a mandatory entry with hashMatchMandatory.

checkInsideZip (Batocera zippedFile)

When a platform entry has a zipped_file field, the expected MD5 is not the hash of the ZIP container but of a specific ROM file inside the ZIP. verify.py replicates Batocera's checkInsideZip():

  1. Open the ZIP.
  2. Find the inner file by name (case-insensitive via casefold()).
  3. Read its contents and compute MD5.
  4. Compare against the expected hash.

If the inner file is not found inside the ZIP, the status is UNTESTED with a reason string.

RomM verification

Source: backend/models/firmware.py, Firmware.verify_file_hashes

RomM reads the file's bytes (verification_mode: md5) but does not compare one digest. Its platform YAML carries hash_rule: size_and_any_hash: the file's size must equal the declared size, then any one of MD5, SHA1 and CRC32 must match. An entry without a size never verifies, since RomM reads it as 0. A wrong hash beside a right one does not withhold the file. verify.py, the pack builder and the install manifest apply that rule. ZIP files are not opened, only the container is checked.

verification_mode answers whether the frontend reads the bytes, and hash_rule answers which comparison it makes. A frontend whose acceptance is not a single digest gets a new hash_rule value, never a new mode.

ROCKNIX verification

Source: rocknix-systems, function checkBios

ROCKNIX derives its BIOS checker from Batocera's, so the logic has the same shape: md5sum() on the file, checkInsideZip() for archive entries, and an altmd5 field that accepts a second hash for the same entry. Every entry the platform declares is mandatory in its own data, so the platform YAML marks all 38 files required: true and none of them use zippedFile.

KNULLI, AmberELEC and EmuELEC verification

Source: knulli-systems (KNULLI) and batocera-systems (AmberELEC, EmuELEC), function checkBios

All three keep Batocera's systems dict and its checker, run by their EmulationStation's BIOS screen.

  • KNULLI runs Batocera's checkBios on files under /userdata: md5sum(), checkInsideZip() for zippedFile entries, and the emulator/core gates read from es_systems.cfg.
  • AmberELEC runs the same checker on files under /storage/roms. Its list sets no gate, and names neogeo/neogeo.zip and neogeo/aes.zip beside bios/, so the pack extracts into /storage/roms.
  • EmuELEC hashes the whole file under /storage/roms and never reads zippedFile: an entry that names a member inside an archive compares the archive's MD5 with the member's and reports UNTESTED whatever the archive holds. Its list also names game data under ports/.

A key written twice in the dict (naomi and Dinothawr in EmuELEC, msx1 and msx2+ in KNULLI) keeps its later value, as when Python runs the script.

MiSTer FPGA verification

Source: Downloader_MiSTer, jobs/process_db_index_worker.py

The MiSTer downloader hashes the file at its destination path and compares it against the MD5 recorded in the BIOS database. There is no ZIP inspection and no required/optional distinction: an entry either matches or it does not.

Scope note: only the BiosDB entries are in scope. They carry an explicit url because MiSTer cannot host them, which is exactly what the user has to supply. The 208 games/ files shipped by the Distribution_MiSTer repository carry no URL and install themselves, so they are not part of the pack.

SHA1 mode

Platforms: BizHawk

BizHawk firmware entries use SHA1 as the primary hash. verify.py computes SHA1 via compute_hashes() and compares case-insensitively.

Condition Status Severity (required) Severity (optional)
SHA1 matches OK OK OK
File present, SHA1 differs UNTESTED WARNING WARNING
File missing MISSING CRITICAL WARNING

Emulator-level validation

Independent of platform verification mode, verify.py runs emulator-level validation from validation.py. This layer uses data from emulator profiles (YAML files in emulators/), which are source-verified against emulator code.

Validation index

_build_validation_index() (validation.py) builds a per-filename index of validation rules from the profiles it is given. A platform report, its pack and its install manifest pass only the profiles resolve_platform_cores resolves for that platform (verify.py, verify_platform). A profile that does not run on the platform has nothing to say about its pack: standalone ZEsarUX reads a 48 KB cpc6128.rom of its own, and its size check reported cap32's 32 KB file in the RetroArch pack as a mismatch while every profile was indexed. The same holds for hle_fallback, which is read from the same set of profiles. A report or pack for --emulator or --system indexes the profiles it selected. When multiple emulators reference the same file, checks are merged (union of all check types). Conflicting expected values are kept as sets (e.g., multiple accepted CRC32 values for different ROM versions).

Each entry in the index tracks:

  • checks: list of validation types (e.g., ["size", "crc32"])
  • sizes: set of accepted exact sizes
  • min_size, max_size: bounds when the code accepts a range
  • crc32, md5, sha1, sha256: sets of accepted hash values
  • adler32: set of accepted Adler-32 values
  • crypto_only: non-reproducible checks (see below)
  • per_emulator: per-core detail with source references

Check categories

Validation checks fall into two categories:

Reproducible (_HASH_CHECKS): crc32, md5, sha1, adler32. These can be computed from the file alone. verify.py calculates hashes and compares against accepted values from the index.

Non-reproducible (_CRYPTO_CHECKS): signature, crypto. These require console-specific cryptographic keys (e.g., RSA-2048 for 3DS, AES-128-CBC for certain firmware). verify.py reports these as informational but cannot verify them without the keys. Size checks still apply if combined with crypto.

Size validation

Three forms:

  • Exact size: size: 524288 with validation: [size]. File must be exactly this many bytes.
  • Range: min_size: 40, max_size: 131076 with validation: [size]. File size must fall within bounds.
  • Informational: size: 524288 without validation: [size]. The size is documented but the emulator does not check it at runtime.

Complement to platform checks

Emulator validation runs after platform verification. When a file passes platform checks (e.g., existence-mode OK) but fails emulator validation (e.g., wrong CRC32), the result includes a discrepancy field:

file present (OK) but handy says size mismatch: got 256, accepted [512]

This catches cases where a file has the right name but wrong content, which existence-mode platforms cannot detect.

Severity matrix

compute_severity() maps the combination of status, required flag, verification mode, and HLE fallback to a severity level.

Mode Status required hle_fallback Severity
any OK any any OK
any MISSING any true INFO
existence MISSING true false WARNING
existence MISSING false false INFO
md5/sha1 MISSING true false CRITICAL
md5/sha1 MISSING false false WARNING
md5/sha1 UNTESTED any false WARNING

HLE fallback: when an emulator profile marks a file with hle_fallback: true, the core has a built-in high-level emulation path and functions without the file. Missing files are downgraded to INFO regardless of platform mode or required status. The file is still included in packs (better accuracy with the real BIOS), but its absence is not actionable.

Firmware, game data, unread: a platform's list can name more than firmware. EmuELEC lists the data files of the games its ports run (Diablo's diabdat.mpq, the Sonic Data.rsdk files), and AmberELEC lists a hardwareconfig.xml in each blueMSX machine folder, a file blueMSX never opens. Each missing entry gets one of three categories, read from the profiles of the cores the platform runs:

Category When Severity
firmware a core reads it as firmware, or nothing settles the question from the matrix above
game data only category: game_data entries of those profiles name it none, counted apart
unread the platform declares its cores, the cores of the entry's system all have a profile, at least one of them emulates it, and no examined core opens the entry none, counted apart

The cores of a system are the ones the platform's file lists for it (cores: on the system, read from the platform's own emulator menu: KNULLI, AmberELEC and EmuELEC carry it). Where a system has no such list, every core the platform names counts.

Every entry, held or missing, carries what establishes its category: the profile entries that read it, with their source reference, or the cores of its system that were examined and found no reader, or the reason the question stays open. A reader whose core runs the entry's system is named first. A core of another system that declares a file of the same name is listed after it as "also named by". --verbose prints it under a missing entry, --json carries it as category_evidence on each entry, and each platform page shows it under every file. Only a missing entry's category changes its severity.

The report counts the last two on a line of their own (Missing, not firmware: ...) and --verbose names them. They stay in the platform's file and in the totals, but --strict fails on firmware gaps only, and the pack check does not count them as pack errors. A core of the system that has no profile yet makes nothing unread. The same goes for a system none of the examined cores emulates: in both cases the absence of a reader proves nothing.

The same category filters: --no-game-data builds, verifies and installs the firmware without the game data, and the install manifest marks each game data file "category": "game_data" (details).

File resolution chain

Before verification, each file entry is resolved to a local path by resolve_local_file(). The function tries these steps in order, returning the first evidence-compatible match:

Step Method Returns When it applies
1 SHA1 sha1_exact A declared SHA1 identifies a database record, and every other declared hash must agree with that same record
2 SHA256 sha256_exact A declared SHA256 identifies a record, again requiring all declarations to agree
3 CRC32 plus size crc32_exact Used only when no stronger hash is declared. Size confirms the weaker checksum when available
4 MD5 md5_exact Direct lookup. An explicitly supported truncated MD5 also needs a compatible name
5 Path suffix path_exact or a hash-exact status Disambiguates regional paths. With any declared hash, the path is accepted only when the content matches it
6 Name or alias name_exact Existence/size resolution only when no content hash is declared. Prefers primary files and supports casefold matching
7 Named candidate inspection md5_composite_exact, md5_exact, or hash_mismatch Checks composite ZIP MD5 or direct MD5. A matching name with wrong content is surfaced, never treated as exact
8 ZIP contents index zip_exact zipped_file with MD5. Searches the inner-ROM index only after name-based resolution fails
9 MAME clone mame_clone Resolves a deduplicated clone to its canonical set only when no content hash was declared
10 Data directory data_dir or data_dir_hash_exact Searches exact path then case-insensitive basename, then computes every declared hash before accepting an unindexed candidate
11 Agnostic fallback agnostic_fallback Size/path constrained lookup only when no content hash was declared

If no step matches, the result is (None, "not_found").

Two kinds of entry skip every answer given by a name alone: steps 6 and 9, the hash_mismatch of step 7, and the name lookup of step 10 when no hash is declared.

  • An entry whose profile carries unsourceable:. The collection cannot hold that file, so a file answering to its name belongs to something else: kanji.rom names an FPseNG font, an openMSX font and a 3DO ROM.
  • An entry whose full destination path is in the by_path_suffix index while no indexed copy is on disk. That file is a release asset the checkout has not restored (scripts/restore_large_files.py), and another file of the same name is not it.

The content steps still apply to both, so collecting the real bytes, or restoring the asset, makes the entry resolve.

The hash_mismatch status means a file with the right name or path exists but its hash does not match. This still resolves to a local path (the file is present), but verification will report it as UNTESTED with a reason string showing the expected vs actual hash prefix.

A path or filename is never allowed to mask a declared strong hash: the identity steps run first, and a path or name is accepted only when no content hash was declared or when the content agrees with the one that was.

What the pack does with a hash_mismatch is set by the platform's mode, as discrepancy detection describes.

Discrepancy detection

When platform verification passes but emulator validation fails, the file has a discrepancy. This happens most often in existence-mode platforms where any file with the right name is accepted.

A local file that contradicts a declared hash is handled by the platform's own mode, never by one global rule. An existence frontend never reads the bytes, so the file is packed and the difference reported: a hash in System.dat that differs from the local dump must not remove a file RetroArch would have loaded. An md5 or sha1 frontend would reject it, so it is left out of the pack and counted as unsafe excluded, which is distinct from missing. The pack report and the installer manifest both record it. The pack builder and the pack verification apply the same rule.

validated_choice() (scripts/validation.py) decides the file a platform destination ships when the emulators reading that destination check it. The pack, verify and the install manifest all read it, so they name the same file.

  1. Only the cores that read this destination judge it: a core checking the same name at another path has no say (ZEsarUX's 48 KB cpc6128.rom check does not judge ep128emu/roms/cpc6128.rom).
  2. The resolved file is kept when every check of those cores passes.
  3. Otherwise find_validated_variant() looks for a held file that passes them all, warning-level checks included. Candidates come first from the hashes the emulator declares, which finds a dump stored under another name, then from the files sharing the name, .variants/ included, in a stable order. On a md5 or sha1 platform a candidate must also carry a value the platform entry declares, or the frontend would refuse it.
  4. When only a warning-level check fails (on_mismatch: warn), the file itself stands: the core runs it.
  5. Then a held file the core at least runs.
  6. Failing all of these, the platform-verified file is kept and the disagreement is reported (" says ..."), never as compatible.

Practical example

A scph5501.bin file passes Batocera MD5 verification (hash matches upstream declaration) but fails an emulator profile's size check because the profile was verified against a different revision. The search looks for a held file carrying a Batocera-accepted md5 that also passes the size check, under that name or any name the emulator's hashes reach. If found, it ships. If not, the Batocera-verified file ships and the discrepancy stays visible in verification and gap reporting.

Hash mismatch / UNTESTED

This section and the two after it are about this repository's own tools, run from a clone. scripts/verify.py checks the collection in bios/ against each platform, never your BIOS folder: for that, use the installer's --check (When a game does not start).

verify.py reports UNTESTED for a file of the collection that was hashed and does not match what the platform declares. The line gives the reason:

  UNTESTED (required): nintendo-ds/dsifirmware.bin: declared hash contradicted by the local file

The system page on the site lists every known version of the file with its expected MD5 and SHA1.

Existence-mode platforms never report UNTESTED

On RetroArch, Lakka and RetroPie, the platform check only asks whether the file is there, so the only outcomes are OK and MISSING. verify.py adds the emulator ground truth from the profiles and flags a DISCREPANCY when a file passes the platform check but fails the emulator's own size or hash validation.

The .variants/ directory

When multiple versions of the same BIOS exist (different revisions, regions, or dumps), the primary version lives in the main directory and alternatives live in .variants/. verify.py checks the primary file first, then falls back to variants when resolving by hash.

If your file matches a variant hash but not the primary, it's a valid BIOS -- just not the preferred version. Some cores accept multiple versions.

Pack is missing files

A generated pack doesn't contain all the files you expected.

Severity levels

verify.py assigns a severity to each issue, counted under severity_counts in its --json output. Not all missing files are equally important:

Severity Meaning Action needed
CRITICAL Required file missing on an MD5 or SHA1 platform Must fix. Core won't function.
WARNING Optional file missing on an MD5 or SHA1 platform, hash mismatch (UNTESTED) on any hash platform, or a required file missing on an existence platform Core works but with reduced functionality, or runs on content nobody verified.
INFO Optional file missing on an existence-only platform, or a missing file the core covers with an HLE fallback Core works fine, BIOS improves accuracy.
OK File present and verified No action needed.

The full mapping, including how hle_fallback overrides the platform mode, is in severity matrix.

Focus on CRITICAL issues first. WARNING files improve the experience but aren't strictly necessary. INFO files are nice to have.

Large files (release assets)

Files like PS3UPDAT.PUP, PSVUPDAT.PUP, and Switch firmware are too large for the git repository. They are stored as GitHub release assets under the large-files release and downloaded at build time.

If a pack build fails to include these, check your network connection. In offline mode (--offline), large files are only included if already cached locally in .cache/large/.

Data directories

Some cores need entire directory trees rather than individual files (e.g. Dolphin's Sys/ directory, PPSSPP's assets/). These are fetched by refresh_data_dirs.py from upstream repositories.

In offline mode, data directories are only included if already cached in data/. Run python scripts/refresh_data_dirs.py to fetch them.

verify.py reports errors

How to read and interpret verify.py output.

Status codes

Status Meaning
OK File present, hash matches (or existence check passed)
UNTESTED File present, hash not confirmed against the expected value
MISSING File not found in the repository
DISCREPANCY The platform check passes, an emulator's own check fails

Reading the output

The first line counts the platform's files and names its check. Under it come the entries that need attention, one per line as STATUS (required|optional): system/file, followed by a reason where there is one, then the counts of the files the emulator cores add. From python scripts/verify.py --platform retrodeck:

RetroDECK: 2007/2008 OK, 1 untested [md5]
  UNTESTED (required): nintendo-ds/dsifirmware.bin: declared hash contradicted by the local file
  DISCREPANCY: nintendo-snes/sgb2.boot.rom: RetroDECK says OK but supersnes9x says size mismatch: got 524288, accepted [256]
    supersnes9x [size]
  Core files: 1495 in pack, 0 required missing, 1 optional missing

--verbose also lists the passing entries that an emulator validates, with the check and the source lines behind it:

Recalbox: 351/351 OK [md5]
  OK (required): macintosh/MacII.ROM
    minivmac validates size=262144 [minivmac/cfg/CNFGRAPI.h:32, minivmac/src/OSGLUERETRO.c:781]

Cross-reference section

After per-file results, verify.py prints a cross-reference report. This lists files that emulator cores need but that the platform YAML doesn't declare. These files are still included in packs automatically, but the report helps identify gaps in platform coverage data.

The cross-reference uses resolve_platform_cores() to determine which emulator profiles are relevant for each platform, then checks whether each profile's files appear in the platform config.

Filtering output

# By platform
python scripts/verify.py --platform batocera

# By emulator core
python scripts/verify.py --emulator beetle_psx

# By system
python scripts/verify.py --system sony-playstation

# By hardware target
python scripts/verify.py --platform retroarch --target switch

# JSON for scripted processing: the entries that need attention
python scripts/verify.py --platform retroarch --json
# every entry, passing ones included, with its ground truth
python scripts/verify.py --platform retroarch --json --verbose