This document captures the principles that guide design decisions in OpenSNES. When a feature, API, or refactor is on the table, the question is "does it respect these five principles?" If yes, ship it. If no, keep talking.
It is intentionally short. Long manifestos die unread.
OpenSNES is a 2D game engine for the Super Nintendo, written for game developers who know C but don't want to learn 65816 assembly, PPU register layouts, or NMI handler timing to ship a game.
The hardware is small (3.58 MHz, 32 KB banks, 128 KB WRAM). The engine respects that — every abstraction is opt-in and pays for itself in code size.
OpenSNES is forked from PVSnesLib and shares its lineage. The two projects sit at different layers of the same stack and are complementary rather than adversarial:
| Concern | PVSnesLib | OpenSNES |
|---|---|---|
| Audience | SNES hardware enthusiasts who want a thin C layer over the metal | C developers who want to ship a SNES game without learning the metal first |
| Compiler | tcc816 (right-to-left push) | cproc + QBE (left-to-right push, modern C11) |
| NMI / VBlank | User-managed | Auto-orchestrated (auto-flush hook, scroll sync, OAM DMA) |
| Hardware quirks | Exposed (Y-position +1 scanline, etc.) | Hidden behind APIs, documented in docs/hardware/ |
| Font / asset model | Bring-your-own (caller provides tile + palette pointers) | Battery-included (default font shipped, asset pipeline opt-in) |
| Test infrastructure | Lightweight | luna-driven CI suite (corpus liveness + visual regression + functional probes; chips run natively), plus static analysis + compiler-pattern checks |
| Scope | Library | Library + framework opt-ins (sprite engine, scene system, …) |
A user moving from one to the other should feel a change in altitude, not a change in vocabulary. We deliberately keep names compatible (oamSet, bgSetMapPtr, WaitForVBlank) to ease migration in either direction.
WaitForVBlank() is the one function of the library that starts with a capital, and it stays that way on purpose: it is the name every PVSnesLib port arrives with and the most called function of any game. Every other function is camelCase with its module's prefix.
The 90% case is a one-liner; the 10% case is still possible.
A high-level API does the right thing without ceremony. The low-level primitive remains accessible for users who need it.
Example — sprite drawing:
Both are public API. We do not remove the low-level primitive in the name of "API surface reduction". We document and recommend the high-level path; the low-level path is there when the high-level isn't the right shape (a sprite-heavy benchmark, a quirky port, an experiment).
The user does not have to discover hardware traps the hard way. The person who wants to understand can find the explanation.
Example — sprite Y +1 scanline quirk:
The SNES PPU draws sprite OAM_Y at scanlines N+1..N+8. oamSet and oamSetY subtract 1 internally so the user's y argument is the rendered top scanline. Direct oamMemory[] writers must subtract 1 manually. Both behaviours are documented in docs/hardware/OAM.md.
Implication: every piece of "magic" comes with a doc entry that explains what is being hidden, why, and what changes if you go around it. Magic without docs is a debugging trap waiting to fire.
The user pays only for the features the project consumes.
Example — LIB_MODULES:
A project that doesn't draw text never links text or its bundled font. A project that uses static sprites never links sprite_dynamic. A project that doesn't use the dynamic sprite engine pays only ~25 cycles per VBlank for the (no-op) NMI hook indirection.
Implication: when adding a feature, the question is "can this be its own module?" before "where does this live in the text or sprite module?". A module is a clean cut between linkage and complexity; a function added to an existing module forces every consumer of that module to ship the new code whether they want it or not.
Compile-time errors beat runtime mysteries. Magic strings and magic positional arguments rot.
Example — engine init:
Implication: a new public function does not take more than five arguments that change from call to call; what stays the same between calls goes in a struct (MetaspriteStyle, Dsp1Camera). The threshold was three arguments of any kind until 2026-10-03: at four and five, a struct filled on every call costs more than the positional form it replaces, and putting per-call values in a struct means writing it before each call. The functions inherited from PVSnesLib keep their signature whatever their length. Sentinel values (OBJ_HIDE_Y = 240, OAM_Y_OFFSCREEN = 224) get named macros. Magic numbers in tutorials are replaced by the matching constant.
The user can reason about timing without guessing. The engine never hides "lazy" allocations or background work that fires at unexpected moments.
Examples:
There is no garbage collector, no lazy initialisation pattern, no implicit job queue. If something runs every frame, it is visible in the source and quantified in the docs.
When a new feature lands, walk through these five questions:
A feature that fails one of these is not blocked from merging — but the gap is identified, named, and tracked. We do not hide trade-offs to make a PR look cleaner.
These principles are not law — they are a snapshot of what we currently believe makes OpenSNES distinctive. They will evolve as the project does. Updates land via PR with the rationale in the commit message; significant shifts get an entry in CHANGELOG.md under a "Philosophy" subsection.
Cross-references: