The SNES cartridge memory map and its transfer engine in Python: where an address lands, what reaching it costs and what an armed channel would move, held to the headers of 2,781 retail cartridges.
English | 日本語 | 简体中文 | 繁體中文(香港)
289 header combinations replayed across 2,781 retail cartridges, 0 disagreements, 6 of 6 layouts resolvable, 8 transfer channels, 947 tests, 100% statement and branch coverage, no dependencies
from mapper import LOROM, resolve
for address in (0x7E0900, 0x001000, 0x004016, 0x008000, 0x808000, 0x700000):
landed = resolve(LOROM, address, fast=True)
print(f"{address:06X}", landed.region, landed.offset, landed.cycles)7E0900 work-ram None 8
001000 work-ram None 8
004016 registers None 12
008000 rom 0 8
808000 rom 0 6
700000 save-ram None 8
The example resolves six addresses on the low layout for a cartridge whose header asks for the faster bus. Bank $7E and the bottom 8 KB of every low bank are work RAM, and that is decided before any cartridge bank is considered. $004016 is a controller port and costs 12 master clocks. $008000 and $808000 both read image offset 0, and only the copy in banks $80 and above runs at 6 clocks. $700000 is save memory, so it carries no offset into the image.
| Requirement | Version |
|---|---|
| Python | 3.12 or newer |
| git | Any |
git clone https://github.com/gufranco/snes-mapper-python.git
cd snes-mapper-python
python3 mapper/doctor.pyThe package imports nothing outside the standard library.
The doctor reports the machine, resolves a probe address through every layout, reads a header out of an image it builds, counts the manifest and the corpus, and says which cartridge library it found. A fresh checkout has none, and the doctor reports that the check against real cartridges will skip. It exits 0 when every check passes and 1 otherwise. Paste its whole output into any bug report.
15 checks, nothing to report
Releases follow Semantic Versioning and are cut from the commit history by semantic-release. Every release is tagged and listed on the releases page.
Nothing here is built and then driven, and nothing executes code. Every call is a question about an address, an image or a register.
resolve(kind, address, fast=False, banks=None, save=False) returns a Resolution. kind is LOROM, HIROM, EXHIROM, WHOLEBANK, SA1 or SPC7110. fast is the header's request for the faster bus. banks is the image size in 64 KB banks, which WHOLEBANK requires. save maps banks $70 to $7D as save memory on WHOLEBANK, where they are cartridge otherwise. Any other string for kind reads cartridge banks the way HIROM does, with no save memory window.
| Attribute | Meaning |
|---|---|
region |
ROM, WORK_RAM, SAVE_RAM, REGISTERS, OPEN_BUS or COPROCESSOR, whose values are rom, work-ram, save-ram, registers, open-bus and coprocessor. coprocessor is an address the cartridge's chip decodes: the SA-1's registers and work RAM, the SPC7110's registers and its port banks |
offset |
Where in the image the address reads, or None when the region is not rom |
cycles |
Master clocks the access costs: FAST is 6, SLOW is 8, XSLOW is 12 |
address |
The address asked about, masked to 24 bits |
is_rom |
True when region is rom |
Below $8000 in banks $00 to $3F and $80 to $BF, the console fixes the speed whatever fast says, as Figure 2-21-1 of Book I prints it: $2000 to $3FFF and $4200 to $5FFF cost 6, the controller ports at $4000 to $41FF cost 12, and work RAM and $6000 to $7FFF cost 8. fast reaches only $8000 and above in banks $80 to $BF, and banks $C0 to $FF.
MODELS maps each resolvable layout name to a Model. layout_named(name) returns the Model for a name or an alias, ignoring case, hyphens and underscores, and raises UnknownModelError for anything else. A Model carries name, aliases, header_at and summary, and model.resolve(address, fast=False, banks=None, save=False) calls resolve with its own name.
| Layout | Aliases | Header at |
|---|---|---|
lorom |
lo, mode20, 20 |
$7FC0 |
hirom |
hi, mode21, 21 |
$FFC0 |
exhirom |
exhi, mode25, 25 |
$FFC0, and $40FFC0 when the image reaches it |
wholebank |
whole, wholebanks, interleaved |
$7FC0 |
sa1 |
mode23, 23 |
$7FC0 |
spc7110 |
none | $FFC0 |
import mapper
for name in ("lorom", "hirom", "exhirom", "wholebank", "sa1", "spc7110"):
model = mapper.layout_named(name)
print(model.name, model.aliases, f"{model.header_at:#06x}")
print(mapper.layout_named("Mode-20").name)
try:
mapper.layout_named("superfx")
except mapper.UnknownModelError as error:
print(error)lorom ('lo', 'mode20', '20') 0x7fc0
hirom ('hi', 'mode21', '21') 0xffc0
exhirom ('exhi', 'mode25', '25') 0xffc0
wholebank ('whole', 'wholebanks', 'interleaved') 0x7fc0
sa1 ('mode23', '23') 0x7fc0
spc7110 () 0xffc0
lorom
superfx is not a layout this package covers; it has exhirom, hirom, lorom, sa1, spc7110, wholebank
| Call | Returns |
|---|---|
read(rom) |
The Header at the candidate place that scores highest. Raises NoHeader when no place scores 2 or more |
score(rom, at) |
The points one candidate place earns, 0 to 4, or -1 when 32 bytes do not fit there |
board(found, size) |
The layout name for a Header and an image size. A header that reads as lorom on a 12 MB image is wholebank |
has_copier_stub(rom) |
True when the image carries the 512 bytes a copier prepends |
stub_by_length(size) |
The same test on a length alone |
Header(at, raw) |
A header built from its 32 bytes and the offset they came from |
The candidate places are LOROM_HEADER, $7FC0, HIROM_HEADER, $FFC0, and $40FFC0, each moved by COPIER_BYTES, $200, when the image carries a copier stub. A place earns one point each for a title with at least 19 of its 21 bytes printable, a nonzero checksum consistent with its complement, a declared size from 8 to 13, and a mapping byte that names a layout whose header sits at that place.
| Attribute | Meaning |
|---|---|
at, shift |
Where the header was found, and how far a copier stub moved it |
raw |
The 32 bytes |
title |
The 21-byte title, trailing spaces and zero bytes removed |
mapping, declared |
The byte that names a layout, and whether its high nibble is 2 or 3, which is when it is believed |
layout |
From the byte when it is believed, and from the offset when it is not. One of lorom, hirom, exhirom, sa1, spc7110 |
rom_bytes, ram_bytes |
The declared sizes in bytes |
checksum, complement, checksum_agrees |
The pair, and whether they sum to $FFFF |
fast, battery, coprocessor |
Whether it asks for the faster bus, carries battery-backed save memory, carries a chip |
chipset, country |
The remaining declared bytes |
from mapper import HIROM_HEADER, LOROM_HEADER, read, score
rom = bytearray(0x20000)
rom[0x7FC0:0x7FD5] = b"A WORKED EXAMPLE "
rom[0x7FD5:0x7FDA] = bytes([0x30, 0x02, 0x0A, 0x03, 0x01])
rom[0x7FDC:0x7FE0] = bytes([0x34, 0x12, 0xCB, 0xED])
rom = bytes(rom)
print(score(rom, LOROM_HEADER), score(rom, HIROM_HEADER))
found = read(rom)
print(found)
print(found.at, found.shift, f"{found.mapping:#04x}", found.declared, found.layout)
print(found.rom_bytes, found.ram_bytes, f"{found.checksum:#06x}", found.checksum_agrees)
print(found.fast, found.battery, found.coprocessor, found.chipset, found.country)4 1
<Header 'A WORKED EXAMPLE' lorom at 0x007fc0>
32704 0 0x30 True lorom
1048576 8192 0xedcb True
True True False 2 1
Mapping byte $30 declares the low layout with the fast bit set. Chipset $02 is ROM with battery-backed RAM. Size bytes $0A and $03 declare 1 MB of ROM and 8 KB of RAM.
| Name | Content |
|---|---|
bank_count(size) |
Whole 64 KB banks in an image of that size. Raises NotWholeBanks otherwise |
interleave(logical), deinterleave(built) |
Between the order the console reads and the order an interleaved dump stores: every upper half first, then every lower half |
snes_to_file(bank, addr, banks), file_to_snes(offset, banks) |
Between a bank and address and a position in an interleaved file |
window_to_file(bank, addr, banks) |
The same for banks WINDOW_FIRST_BANK, $C0, and above |
address_to_file(bank, addr, banks) |
Whichever of the two applies to that bank |
BANK, HALF |
$10000 and $8000 |
WHOLEBANK_BANKS, WHOLEBANK_BYTES |
192 banks and 12 MB, the only size WHOLEBANK resolves |
from mapper import bank_count, file_to_snes, snes_to_file, stub_by_length
banks = bank_count(4 * 1024 * 1024)
print(banks, snes_to_file(0x00, 0x8000, banks), file_to_snes(0, banks))
print(file_to_snes(banks * 0x8000, banks))
print(stub_by_length(0x100200), stub_by_length(0x100000))64 0 (0, 32768)
(0, 0)
True False
A 4 MB interleaved file holds the upper half of bank $00 at offset 0 and its lower half 64 half-banks later.
| Name | Content |
|---|---|
Engine() |
The eight channels, channels, and the two arming registers, enable and enable_indirect |
engine.write(address, value) |
One byte to ENABLE, $420B, to $420C, or to a channel register. Any other address is ignored |
engine.enabled |
The channels ENABLE selects |
engine.plans() |
A Plan for each of those channels and none for the rest |
Plan |
channel, source, length, step, destination, to_cpu, and addresses(), every source address in order, wrapping inside the bank |
Channel(index) |
One channel: index, its 16 registers, write(offset, value), and plan() |
channel.parameters, channel.to_cpu |
Register $43x0, and its bit 7, set when the transfer runs toward the processor |
channel.step |
1, then -1 when bit 4 of the parameters is set, and 0 whenever bit 3 is set |
channel.destination, channel.source |
$2100 plus register $43x1, and the 24-bit address in $43x2 to $43x4 |
channel.count, channel.length |
The 16-bit count in $43x5 and $43x6, and the bytes it moves: the count, or 65536 when the count is 0 |
channel_of(address) |
The channel a register address belongs to, or None outside CHANNEL_BASE, $4300, to $437F |
CHANNEL_COUNT |
8 |
from mapper import ENABLE, Engine, channel_of
engine = Engine()
written = {0x4310: 0x01, 0x4311: 0x18, 0x4313: 0x80, 0x4314: 0x7E, 0x4315: 0x04}
for register, value in written.items():
engine.write(register, value)
print(engine.plans())
engine.write(ENABLE, 0x02)
plan = engine.plans()[0]
print(plan, f"{plan.destination:#06x}", plan.step, plan.to_cpu)
print([f"{one:06X}" for one in plan.addresses()])
print(channel_of(0x4370), channel_of(0x4380))[]
<channel 1: 4 bytes from 7E8000> 0x2118 1 False
['7E8000', '7E8001', '7E8002', '7E8003']
7 None
Channel 1 holds a complete four-byte transfer from $7E8000 to $2118, and plans() reports nothing until ENABLE selects it.
| Error | Raised when |
|---|---|
NoHeader |
read finds no candidate place scoring 2 or more |
NotWholeBanks |
An image size is not a whole number of 64 KB banks |
NeedsBankCount |
WHOLEBANK is resolved without banks, or with fewer than 192 |
UnknownModelError |
layout_named is given a name no layout goes by |
from mapper import WHOLEBANK, NeedsBankCount, NoHeader, NotWholeBanks, bank_count, read, resolve
for attempt in (
lambda: read(bytes(0x10000)),
lambda: bank_count(1000),
lambda: resolve(WHOLEBANK, 0x008000, banks=64),
):
try:
attempt()
except (NoHeader, NotWholeBanks, NeedsBankCount) as error:
print(type(error).__name__, str(error).split(". ")[0])NoHeader no header found in 65536 bytes
NotWholeBanks 1000 is not a whole number of 65536 byte banks
NeedsBankCount wholebank needs 192 banks and this image has 64
Each message carries more after its first sentence: NeedsBankCount goes on to say why a smaller image cannot be on this map, and without banks it names the call that counts them.
mapper.__version__ is the installed version. The layout names LOROM, HIROM, EXHIROM, WHOLEBANK, SA1 and SPC7110 are the strings lorom, hirom, exhirom, wholebank, sa1 and spc7110. The submodules header, layout, image, transfer, models and errors are reachable as attributes, and layout.EXHIROM_BYTES is 8 MB.
Each runs from the repository root. Everything under conformance/ runs as a module.
| Command | Does | Exit codes |
|---|---|---|
python3 mapper/doctor.py |
Reports the machine and the checkout | 0 clean, 1 a check failed |
python3 mapper/environment.py |
The machine half of that report alone | 0 clean, 1 a check failed |
python3 -m conformance.corpus [corpus] |
Replays every recorded header combination | 0 all agree, 1 one did not, 2 no corpus |
python3 -m conformance.against_cartridges [directory] |
Reads every cartridge present and holds each to its case | 0 all agree, 1 one did not, 2 no cartridge found |
python3 -m conformance.census <directory> <out> [limit] |
Tallies the headers of a library | 0 written, 1 no cartridge, 2 bad arguments |
python3 -m conformance.record <directory> [out] [limit] |
Rebuilds the corpus from a library | 0 written, 1 no cartridge, 2 bad arguments |
python3 -m conformance.documents [--check] [--only SUBSTRING] |
Fetches the manual and verifies its digest, or verifies the copy present | 0 verified, 1 refused, no match or bad arguments |
python3 -m conformance.quotes |
Finds every quoted sentence in the manual | 0 found or no manual present, 1 a quote is missing or a citation points at the wrong place |
python3 -m conformance.speed |
The throughput floor | 0 above it, 1 below it |
python3 -m conformance.links |
Requests every address this README cites and prints each status as JSON | 0 every address still exists, 1 one is gone with status 404 or 410 |
python3 -m conformance.package <wheel> |
Checks a built wheel carries every module and no test file | 0 clean, 1 a fault or no wheel there, 2 no argument |
from mapper import deinterleave, interleave
image = bytes(range(256)) * 1024
print(deinterleave(interleave(image)) == image)True
Some dumps store every bank's upper half first and every lower half afterwards. A patch written at a known address into an interleaved image lands half a bank away, in another bank's data.
from mapper import LOROM, resolve
print(resolve(LOROM, 0x7E0900).region)
print(resolve(LOROM, 0x001000).region)work-ram
work-ram
They sit in the middle of the bank numbering and read like any other address. The bottom 8 KB of every low bank mirror the same work RAM, which is the second line.
from mapper import CHANNEL_BASE, ENABLE
print(f"{ENABLE:#06x} {CHANNEL_BASE:#06x}")0x420b 0x4300
A hook on the display's $21xx window sees every graphics write and no transfer.
from mapper import channel_of
print(channel_of(0x4370))
print(channel_of(0x4380))7
None
Masking with & 7 folds $4380 onto channel 0 instead of reporting that it lies outside the window.
from mapper import ENABLE, Engine
engine = Engine()
engine.write(0x4304, 0x7E)
print(engine.enabled)
engine.write(ENABLE, 0x01)
print(engine.enabled)[]
[0]
Registers hold whatever they last held, so walking all eight channels reports transfers that never happened.
from mapper import layout_named
print(f"{layout_named('lorom').header_at:#06x}")
print(f"{layout_named('hirom').header_at:#06x}")
print(f"{layout_named('exhirom').header_at:#06x}")
print(f"{layout_named('wholebank').header_at:#06x}")
print(f"{layout_named('sa1').header_at:#06x}")
print(f"{layout_named('spc7110').header_at:#06x}")
print(layout_named("mode20").name)
print(layout_named("hirom").resolve(0xC00000).region)0x7fc0
0xffc0
0xffc0
0x7fc0
0x7fc0
0xffc0
lorom
rom
| Layout | Shape |
|---|---|
lorom |
A 32 KB page per bank, in the upper half of each bank, with save memory filling banks $70 to $7D and $F0 to $FD |
hirom |
A whole 64 KB bank per bank, with save memory at $6000 to $7FFF in banks $20 to $3F and $A0 to $BF |
exhirom |
hirom with its two 4 MB halves swapped |
wholebank |
A whole 64 KB per bank below $C0 from an interleaved 12 MB image |
sa1 |
Four 8 Mbit blocks in order: a 32 KB page per bank in $00 to $3F and $80 to $BF, a whole bank per bank from $C0, backup RAM in $40 to $4F and at $6000 to $7FFF |
spc7110 |
Program ROM, the image's first megabyte, in $00 to $0F, $80 to $8F and $C0 to $CF, and data ROM after it in every other ROM bank |
from mapper import SA1, SPC7110, resolve
for kind, addresses in (
(SA1, (0x002200, 0x003000, 0x008000, 0x808000, 0x406000, 0xD00000)),
(SPC7110, (0x004800, 0x500000, 0x008000, 0x108000, 0xD00000)),
):
for address in addresses:
landed = resolve(kind, address)
where = "-" if landed.offset is None else f"{landed.offset:#08x}"
print(kind, f"{address:06X}", landed.region, where)sa1 002200 coprocessor -
sa1 003000 coprocessor -
sa1 008000 rom 0x000000
sa1 808000 rom 0x200000
sa1 406000 save-ram -
sa1 D00000 rom 0x100000
spc7110 004800 coprocessor -
spc7110 500000 coprocessor -
spc7110 008000 rom 0x008000
spc7110 108000 rom 0x108000
spc7110 D00000 rom 0x100000
The SA-1 map is the one Book II prints for the console's processor, at the initial values of the bank registers $2220 to $2223 and the backup RAM register $2224. Banks $80 to $BF carry the third and fourth blocks rather than mirroring the first two. A program that writes those registers moves the map, and resolve does not follow it. No Nintendo document prints the SPC7110 board, so its map comes from ares's board database and SPC7110 source at a pinned commit, and the record marks it unverified. Until the game selects its data banks, every data bank reads the first megabyte after the program ROM.
Banks $80 to $FF carry the first 4 MB of an exhirom image. Banks $00 to $7D, which hold the reset vector, carry everything past it.
from mapper import layout
print(f"{layout.resolve('exhirom', 0x00FFC0).offset:#08x}")
print(f"{layout.resolve('hirom', 0x00FFC0).offset:#08x}")0x40ffc0
0x00ffc0
That is why an extended cartridge keeps a header 4 MB in. The ceiling is layout.EXHIROM_BYTES, 8 MB, and the top of it is reachable only through banks $3E and $3F, because $7E and $7F are work RAM.
The whole-bank map spends nothing below its window on anything but cartridge. Banks $00 to $BF each carry 64 KB, so 192 banks fit, and the image holds all of them interleaved, as mapper/image.py converts.
from mapper import WHOLEBANK, WHOLEBANK_BYTES, bank_count, resolve
banks = bank_count(WHOLEBANK_BYTES)
print(banks)
print(f"{resolve(WHOLEBANK, 0x008000, banks=banks).offset:#08x}")
print(f"{resolve(WHOLEBANK, 0x400000, banks=banks).offset:#08x}")
print(f"{resolve(WHOLEBANK, 0xC04D6A, banks=banks).offset:#08x}")192
0x000000
0x800000
0xa04d6a
The first is the upper half of bank $00, where the low layout puts it. The second is the lower half of bank $40. The third comes through the window the banks from $C0 open.
The smallest size this map accepts comes from an equation. The window reads its lower halves from the run belonging to bank $80, so its top byte sits 191 + banks half-banks into a file 2 * banks half-banks long. The two meet at exactly 192 banks, so resolve refuses any smaller bank count.
board() picks this map from the image size and never from the chipset byte. An expansion built by removing a coprocessor often still declares it, and a six megabyte cartridge that declares S-DD1 is not on this map.
from mapper import Header, board
plain = Header(0x7FC0, bytes(32))
print(board(plain, 12 * 1024 * 1024))
print(board(plain, 4 * 1024 * 1024))wholebank
lorom
Read out of 2,781 retail cartridges and nothing else. A modified release, a translation and a prototype can each carry an edited header, so none of them is counted.
| Declared layout | Cartridges |
|---|---|
lorom |
2,157 |
hirom |
572 |
sa1 |
42 |
exhirom |
6 |
spc7110 |
4 |
1,701 ask for the faster bus, 957 carry battery-backed save memory, and 2,780 carry a checksum consistent with its complement. Headers sit at $7FC0 in 2,200, at $FFC0 in 579 and at $40FFC0 in 2. A further 8 files carry no header read accepts and are counted as refused.
8 distinct in-range mapping bytes occur, all $2x or $3x. 13 cartridges carry one outside that range, 11 of them a capital letter: a 22-character title overflows its 21-byte field and writes its last letter over the mapping byte.
from mapper import Header
found = Header(0x7FC0, b"CONTRA3 THE ALIEN WARS" + bytes(10))
print(f"{found.mapping:#04x}")
print(found.declared)
print(found.layout)0x53
False
lorom
0x53 is S, the title's last letter. An out-of-range byte is not consulted, and the layout comes from where the header sits. Reading the low nibble of such a byte gave 51 retail cartridges in a wider library the wrong layout, the wrong bus speed, or both. An in-range byte is believed even where it disagrees with the offset, because exhirom puts its header where hirom does whenever the far copy would sit past the end of the image.
Across 7,314 cartridges, the size byte read at the right offset falls between 8 and 13 in 98.85% of them, while a byte read at a wrong offset falls in that band only 3.18% of the time, which is why score gives that band a point.
A header is 32 bytes in which a cartridge describes how it is built.
| Field | What it is | Shipped |
|---|---|---|
| Mapping, chipset, ROM and RAM size, country | Facts about a physical object | Yes |
| How many cartridges share a combination | A measurement | Yes |
| The title | A name | No |
| Anything outside the header | The game | Never read |
Facts and functional elements sit outside what copyright reaches, per 17 U.S.C. 102(b) and Feist. conformance/census.py reads only those 32 bytes and records no title. This is how the repository is built, not legal advice.
conformance/corpus.json carries every distinct combination with the header offset, the layout it produces, and what 14 probe addresses resolve to under it.
python3 -m conformance.census /path/to/roms census.jsonOn the library behind the corpus it prints 2781 cartridges read, 8 refused and 287 distinct header combinations. The census does not key on the header offset, so it finds 2 fewer combinations than the corpus.
python3 -m conformance.record /path/to/roms conformance/corpus.jsonAnyone holding the same cartridges rebuilds the file and compares it byte for byte. Two cartridges with identical fields at different offsets are two cases, because the offset names the layout whenever the mapping byte is a stray letter.
Put copies you own in cartridges/, in ../cartridges beside the checkout, or wherever SNES_CARTRIDGE_DIR points. A named directory wins, then the one beside the checkout, then the one inside it. Subdirectories are walked, and .sfc and .smc files are read.
Every file is checked against all four digests in cartridges.manifest.json before it is read: sha256 decides, and crc32, md5 and sha1 must match too. The manifest names 2,778 cartridges and 2 bad dumps. cartridges/README.md lists every filename and its digests, and nothing in the directory is committed.
| Evidence | What it settles | What it cannot |
|---|---|---|
Nintendo's manual, with arithmetic in conformance/hardware.json |
The three CPU clock speeds and the addresses each applies to, so the 6, 8 and 12 clock counts | The master clock, which the manual does not print |
| Book II, section 1, chapters 3 and 4 | The SA-1 map at its registers' initial values | The map once a program writes them |
| ares's board database and SPC7110 source at a pinned commit | Nothing; it is a lead, marked unverified | The SPC7110 board, which no Nintendo document prints |
| Header combinations read out of 2,781 cartridges | What a combination means on shipped cartridges | Combinations no cartridge carries |
| Digests of every cartridge read | That the file read is the file named | Whether that release is the one you meant |
Not modelled: the processor, so how many accesses an instruction makes; refresh and transfer stalls; and anything that happens once an address is reached.
Every distinct header combination is replayed against the layout and the addresses it produces: 289 combinations, 0 disagreements. The corpus carries the measurements, not the cartridges, so this runs anywhere.
python3 -m conformance.corpusIt prints 289 agreed, 0 did not and exits 0.
With a library present, the sweep reads every file and holds each cartridge to the case that recorded it. On the library behind the corpus it prints 2781 agreed, 0 did not. With no library it prints why and exits 2, and CI annotates that skip.
python3 -m conformance.against_cartridgesconformance/hardware.json quotes the three CPU clock speeds Nintendo printed in section 21.1 of Book I, 3.58 MHz, 2.68 MHz and 1.79 MHz, and shows the arithmetic from each to 6, 8 and 12 master clocks. Figure 2-21-1, read off the scanned page, puts the 1.79 MHz region at $4000 to $41FF. conformance/divergences.json holds every place two sources part, and OPEN-QUESTIONS.md holds every place fidelity here is a claim rather than a measurement, with what would settle it.
| Need | Where |
|---|---|
| Every gate and the order to run them | CONTRIBUTING.md |
| The same material for a coding agent | AGENTS.md |
| The standard this repository shares with the rest of the family | FAMILY.md |
| Bug reports | GitHub Issues |
| Security reports | SECURITY.md |
uvx ruff@0.16.3 format --check .
uvx ruff@0.16.3 check .
uvx --python 3.12 --with types-setuptools==84.0.0.20260812 mypy==1.14.1
pnpm install --frozen-lockfile && pnpm run format:check
cov=(uvx --python 3.12 --from coverage==7.6.10 --with setuptools==84.0.0 coverage)
"${cov[@]}" erase
for file in $(find mapper conformance -name '*.test.py' | sort); do
"${cov[@]}" run -a "$file"
done
"${cov[@]}" report
python3 -m conformance.corpus
python3 -m conformance.speedEach module has a test file beside it, named <module>.test.py, and each runs on its own. Coverage is a hard gate at 100% statement and branch, set in pyproject.toml. The throughput floor in conformance/speed.py is 200,000 resolve calls per second and runs outside the coverage step. Tests that need a cartridge live in conformance/on_disk.test.py, the one file kept out of the coverage count.
| Document | Publisher | Read on | Bytes | SHA-256 |
|---|---|---|---|---|
| SNES Development Manual, Book I, full text as archived | Nintendo of America Inc. | 2026-08-27 | 298,926 | 100beabe8e29d6e02b60b9a0f057a40063a4e33a83478f480ecc2e8696e77972 |
| SNES Development Manual, Book I, scanned pages with their text layer | Nintendo of America Inc. | 2026-10-19 | 21,022,582 | f1405241000046abba7849376d2ec53cf85f1ad2e9950d69a7c2cf1f3ff6b791 |
| SNES Development Manual, Book II, full text as archived | Nintendo of America Inc. | 2026-10-19 | 463,516 | 546f8e38caf6cb19c8aa9d5a1fef090d8cfc4a0a13b54c3b56d704975ab8398a |
| SNES Development Manual, Book II, scanned pages with their text layer | Nintendo of America Inc. | 2026-10-19 | 29,575,127 | 05197923d9b73b63845fc5c2d609e753ec634dc1e4b403ce7a57326e1259c3ed |
None of the four is redistributable and none is in this repository. python3 -m conformance.documents downloads all four from the archive.org item https://archive.org/details/SNESDevManual into docs/, which git ignores, and refuses any file whose digest differs. --check verifies the copy already present. conformance/documents.json carries the digest and the address. The sentences taken from it are in conformance/hardware.json, each with the table and page it came from.
The 2,781 headers behind conformance/corpus.json come from a retail cartridge library the author owns. Nothing from it is committed beyond the digests in cartridges.manifest.json.
CITATION.cff is updated with each release by the script that stamps the package version.
MIT. See LICENSE.