RetroSharp.Tool
0.0.1231
See the version list below for details.
dotnet tool install --global RetroSharp.Tool --version 0.0.1231
dotnet new tool-manifest
dotnet tool install --local RetroSharp.Tool --version 0.0.1231
#tool dotnet:?package=RetroSharp.Tool&version=0.0.1231
nuke :add-package RetroSharp.Tool --version 0.0.1231
RetroSharp
RetroSharp is a C#-inspired language, compiler, and zero-cost 2D framework for 8-bit game systems.
I'm making this in my free time to learn about compilers and old-school game hardware. It was supposed to make me happier; it has also managed to reach the shaved-head milestone.
Documentation
- Getting started — install the CLI, create a game, and build Game Boy and NES ROMs.
- Language reference — types, declarations, operators, control flow, functions, arrays, and zero-cost abstractions.
- Portable2D SDK reference — frames, input, worlds, cameras, sprites, animation, audio, actors, projectiles, and effects.
- Documentation home — browse guides, target references, tooling, architecture, and contributor documentation.
- Samples — runnable programs organized by feature and implementation layer.
Start without installing .NET with the standalone CLI guide. For a conceptual introduction, read the project overview.
Project shape
RetroSharp has three layers that should stay separate:
- Language: target-neutral syntax, types, storage, constants, helpers, control flow, and zero-cost source ergonomics.
- Portable 2D SDK: capability-checked game APIs for frames, input, worlds, cameras, sprites, palettes, music, animation, and actors.
- Target intrinsics and lowering: Game Boy, NES, and future hardware-specific behavior.
The language should not know what a sprite, camera, tilemap, joypad, or palette register is. Those concepts belong in the SDK or in explicit target intrinsics.
Compilation paths
RetroSharp parses and prepares source through a shared target-neutral frontend,
then lowers it directly through one of three cartridge targets. The CLI requires
an explicit --target for standalone source files and manifests that do not
declare target or targets:
--target nes: emits an iNES mapper 0 ROM for static background/tile drawing plus tick-based input, logical sprites, horizontal camera streaming, four-screen 2-axis camera movement, TiledWorld.Load(...), runtime animation helpers, camera-relative runner collision, and VGM/VGZ-sourced 2A03 BGM playback. HUD and generic world-space collision are still not implemented.--target gb: emits a 32 KiB ROM-only Game Boy cartridge when the program fits, or an MBC1 banked ROM when large music assets need more space. It supports static background/map setup and a first runtime sprite loop subset with local byte-backed variables, assignment,if/else if/else,while,for, half-open rangefor,Video.WaitVBlank(), tick-based input polling,Scroll.Set(...), position-based camera X/Y scrolling with runtime row/column streaming,Sprite.Set(...), simple source-map tile queries for collision, joypad button queries,.gbapu/.gbapu.jsonAPU trace playback, and VGM/VGZ-sourced DMG playback through the same compact on-ROM trace repack. Bank selection for banked BGM data is emitted by the runtime; source code keeps usingMusic.Asset(...),Music.Play(...), andAudio.Update().--target sms: in development. Emits a 32 KiB Master System ROM for pure-language programs with static background drawing (Palette.Set,Tilemap.Set,Tilemap.Fill), sprites (Sprite.Asset,Sprite.Draw),World.Mapworlds withCamera.*scrolling on both axes, music and sound effects from SN76489 VGM logs, or transcoded from NES and Game Boy ones (Music.*,Sfx.*,Audio.*),Video.Init(),Video.Present(),Video.WaitVBlank()and portableInput.*on the two-button pad; every other SDK operation fails with a capability diagnostic. Seedocs/MasterSystemTarget.md.--target gg: in development. Builds the Master System target for the Game Gear and writes a.ggROM: the program sees the 160x144 LCD window, colours are 12-bit, and Start is a held button. The camera scrolls inside the window. Seedocs/MasterSystemTarget.md(Game Gear).
The NES and Game Boy targets accept --runtime-abi-out <path> for a deterministic,
versioned JSON projection of their target-owned runtime layout and compiled
user-local symbols, bound to the emitted ROM content. It does not change ROM
bytes. tools/gameboy/runtime_abi.py and tools/nes/runtime_abi.py validate the
matching target contract, layout fingerprint, and ROM identity before
diagnostics consume an address. Game Boy runtime regions are fixed in
GameBoyRuntimeMemoryLayout, so its runtimeRegions collection is empty;
NES adds the ROM-specific regions allocated by its build.
Both cartridge targets accept --symbols-out <path> for a deterministic UTF-8
debugger symbol file. Each line has the MCP-compatible form AAAA name and
names may contain spaces. The export includes the target's runtime-memory
ranges and named addresses, compiled variables, and any ROM-specific runtime
regions reported by that target. It intentionally excludes ROM code labels,
fixed linker symbols, and banked segments in this first version. Because one
symbol file describes one target build, the option requires a single target
and cannot be combined with --world-budget-report.
Every target accepts --build-report-out <path> for a deterministic JSON
build report (retrosharp.build-report v1): the ROM size and SHA-256, the
bytes each owner placed in each bank, the code and data labels with their bank
and address, the static per-frame CPU estimate, and, in runtimeArithmetic,
every runtime multiply, divide and shift loop per function with its worst-case
cost in the target's CPU unit (docs/RetroSharp.Language.md §5), and, in
compilerStorage, every local the compiler declares to hold a construct, with
its function, bytes, address, source position and reason. The Master System and
Game Gear reports carry only the ROM size and hash, the runtime arithmetic and
the compiler storage so far. The report is a
diagnostic for comparing two builds with tools/roms/diff_roms.py, not a
stable contract, and it requires a single target.
When a program has runtime arithmetic, the build also prints one line such as
Runtime arithmetic: 7 loops, worst case 6,070 cpu-cycles (Average 2 loops, 2,360; Main 5 loops, 3,710). The report has the detail by function, operation
and operator; for example, with jq:
jq -r '.runtimeArithmetic.functions[] | .function as $f | .operations[]
| "\($f): \(.count) x \(.operation) (\(.operator)) = \(.worstCaseTotal)"' game.build-report.json
Loops are attributed to functions, not to source lines: a function with two
divisions lists two divide-16 loops without saying which line holds each. The
totals price each loop once; how often it runs per frame is up to the code
around it.
For example, compile the runner and export both diagnostic sidecars:
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target gb \
--out /tmp/runner.gb \
--runtime-abi-out /tmp/runner.gb.runtime-abi.json \
--symbols-out /tmp/runner.gb.sym \
games/runner/runner.retrosharp.json
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target nes \
--out /tmp/runner.nes \
--runtime-abi-out /tmp/runner.nes.runtime-abi.json \
--symbols-out /tmp/runner.nes.sym \
games/runner/runner.retrosharp.json
In the matching Game Boy or NES debug MCP, call load_rom with the generated
ROM, load_symbols with its .sym file, then resolve_symbol or read_symbol
with a canonical name such as camera.XLow, camera.X,
packed camera.FramePending, WorldPack.VisualSlot0, or player.x.
The actor acceptance slices live in samples/actor-framework and
samples/actor-contact-lifecycle. The former fills an ActorSlots pool of the
game's own union Enemy(Goomba, Bat, Koopa) from two Tiled object layers, with
runtime camera-window activation, defeat and tile contact. The latter proves
transient top-contact capture through Contact, conditional defeat, preserved
state, a three-step defeated lifetime, and the freed slot on Game Boy, NES and
Master System. Both lower without heap allocation or runtime dispatch. The
current public surface is in docs/Portable2DSdkV1.md, and ADR 0014 records
the move from the V1 actor framework.
The measured generated-code scaling work and CPU-budget plan live in
docs/history/GeneratedCodePerformanceRoadmap.md.
Language surface
Right now, RetroSharp can compile simple programs with:
- Basic arithmetic and logic operations
- Variables and assignments
- Decimal, hexadecimal (
0x2A), binary (0b1010_0000),_-separated, and width-suffixed (255u8,0x1234u16) integer literals - Type aliases normalized at compile time
- Compound assignment with
+=,-=,&=,|=, and^=, plus statement-only++and-- - Top-level and block-local constants with optional type annotations folded at compile time, including constant boolean and conditional expressions
sizeof(type)folded at compile time for primitive, reserved internal pointer-size marker, enum, and plain struct typesoffsetof(type, field)folded at compile time for direct fields of plain struct typescountof(array)folded at compile time for fixed-size local arrays- Top-level enums with qualified members folded at compile time
- Plain local structs with named and shorthand initializer lists plus field access in the current cartridge targets
- Restricted static classes with fixed-layout fields, instance methods lowered to receiver helpers, and static constants/methods lowered without heap allocation or dispatch
- Fixed-size local byte arrays with initializer lists, initializer-inferred lengths, constant index access, and byte-backed runtime index access in the current cartridge targets
- Function calls, including inline helper calls with parameters, named arguments, default parameter values, single-return expression helpers, and
=>expression-bodied helpers in the current cartridge targets - Control flow (
if/else if/else, no-fallthroughswitchwith multi-value and half-open range cases,while,do while,for, half-open rangefor,break,continue) - Half-open range membership expressions such as
tile in 1..4, lowered liketile >= 1 && tile < 4 - Short-circuit
&&/||and unary!conditions, plus byte-backed 0/1 logical value expressions, in the current cartridge targets - Conditional value expressions (
condition ? whenTrue : whenFalse) lowered to direct branches in the current cartridge targets - Byte-backed bitwise
&,|, and^, plus folded constant~masks, in the current cartridge targets - Explicit casts such as
(u8)expras validated zero-cost expression markers in the current cartridge targets - Canonical fixed-width types (
u8,i8,u16,i16,bool) plus source aliases where the current target accepts them - Immutable
letlocals with compile-time width inference:i16/u16fields, locals, helper results, and arithmetic keep two-byte storage; expressions proven byte-valued keep one byte - Public gameplay code stays at the SDK/resource level; raw buffers, hardware addresses, pointer member access, and
ptr<T>APIs remain reserved for internal SDK/backend addressability policy
let never narrows a statically word-valued expression. A width suffix on a
single literal remains authoritative, and an unsuffixed literal keeps the
existing u8 default. For non-literal expressions, an unambiguous i16 or
u16 operand makes the result use that same word type; otherwise a proven
byte-valued expression remains byte-backed. Mixing signed and unsigned word
values, or using an expression whose scalar type cannot be proven, is a
compile-time error; an explicit cast disambiguates the declaration. This rule
changes storage selection only and adds no runtime type metadata or dispatch.
Example program:
i16 Main()
{
return 2 * 3 * 4;
}
Framework surface
The portable 2D SDK gives game code a common vocabulary for tile-and-sprite
machines while keeping each target's limits explicit. Published projects pin
RetroSharp.Portable2D in manifest sdkDependencies; checkout-local projects
can select the bundled copy through libraries. Game code then uses frame/input
calls, Tiled World.Load(...) with native tileset animation and a runtime background-animation toggle, collision flags, camera-relative AABB queries,
logical palettes, logical sprites, animation, music declarations, and
game-owned ActorSlots pools with Tiled spawns. Standalone source files can still declare
import RetroSharp.Portable2D;; unknown imports are rejected. Generic compiler
and frontend APIs start with an empty SdkLibraryRegistry: a direct C# host
must compose every source library it wants to expose, including Portable2D.
The CLI is that host for normal command-line use: it supplies its bundled
Portable2D package alongside any --lib-path libraries. Pass
--no-default-libraries to build a source file or project without that bundle;
then use --lib-path and explicit imports for every library required.
A library package is a directory with retrosharp-library.json and one or more
RetroSharp source files:
{
"import": "Acme.Wait",
"rootNamespace": "Acme.Wait",
"sourceRoot": "src",
"namespaceMode": "physical",
"sources": [
"src/api.rs",
"src/timing/rules.rs"
],
"targets": [ "gb", "nes" ]
}
--lib-path may point at one package directory or at a directory containing
package subdirectories. Local paths remain the fastest editing workflow.
Versioned source libraries and SDKs
Projects can also restore source libraries and SDKs from one isolated NuGet feed.
Versions in packageDependencies are direct, exact versions; the CLI writes
the complete direct-and-transitive graph to the portable retrosharp.lock.json
and extracts packages under .retrosharp/packages. The same graph is prepared
once before a multi-target GB/NES build. A package must contain the ADR's
retrosharp-package.json with kind: "source-library" and its library
manifest at retrosharp/retrosharp-library.json.
An SDK package uses kind: "sdk-plugin" and includes that source manifest plus
its descriptor assembly under plugin/net10.0/. Declare it once to restore and
activate both parts:
{
"targets": ["gb", "nes"],
"sources": ["src/main.rs"],
"packageSources": ["./local-feed"],
"sdkDependencies": { "Acme.Probe": "1.0.0" }
}
packageDependencies remains for source-only packages. A transitively restored
SDK remains passive; declare it directly in sdkDependencies before importing
and using it. The host checks package identity/version and the source import
against the descriptor. An explicit RetroSharp.Portable2D package replaces
both bundled pieces. --no-default-libraries removes the bundle while retaining
explicit packages.
{
"targets": ["gb", "nes"],
"sources": ["src/main.rs"],
"packageSources": ["./local-feed"],
"packageDependencies": { "Acme.Timing": "1.2.3" }
}
The normal build only invokes restore if the local material is absent and never
rewrites an existing lock. Use --restore to create or intentionally update a
lock, --locked-mode to require it, and --no-restore or --offline to fail
without contacting the feed when material is absent. One feed needs no extra
mapping; multiple feeds must declare packageSourceMappings from each source
entry to its package-id patterns, so a failed feed cannot silently fall back to
another. Restore inspects archive paths and rejects build content and undeclared
executables. It never loads package DLLs, props, targets, tools, or scripts.
SDK entry points run only during explicitly activated builds.
For local development, use --lib-path and
--sdk-plugin-path <retrosharp-package.json-or-dll> together to replace one
product. External DLLs need package metadata; bare DLLs are invalid. A complete
local pair overrides a restored or bundled SDK; a partial override fails.
Sample and smoke builds that use the CLI bundle therefore pass neither path;
external SDK tests pass both paths with --no-default-libraries.
Source-only libraries cannot add new compiler semantics. When a feature needs a
new namespaced operation, resource, capability, validator, or per-target
lowering hook, the host registers a static in-process SDK plugin instead.
Pass an explicit trusted local plugin with repeatable
--sdk-plugin-path <dll-or-retrosharp-package.json>. External DLLs require
declarative package metadata; bare DLLs are rejected. --sdk-plugin <id> and
project plugin ids have no production registry. Plugin paths work with source and project builds,
resolve relative to the current working directory, and are intentionally not
stored in manifests. A loaded assembly exposes exactly one public
ISdkPluginEntryPoint with a public parameterless constructor. Unknown ids,
invalid assemblies, incompatible contracts, and duplicate ids are
rejected before compiling. Packaged plugins are checked through metadata and PE
references before loading; descriptor coherence is checked after construction.
Use --sdk-plugin-diagnostics to include technical exception details.
A registered plugin is
enforced per target: a target lowers a plugin operation only when it provides a
matching lowering hook and grants the operation's required capabilities,
otherwise the build fails before lowering with a diagnostic naming the plugin
feature and target id. The experimental reference plugin is
RetroSharp.Platformer2D (Platformer.GroundProbe()), which lowers on Game Boy
and is unsupported on NES. Path-loaded plugins execute arbitrary trusted code
inside the compiler process; there is no sandbox, implicit discovery, package
resolution, unloading, or stable cross-version binary compatibility promise.
See docs/SdkPluginBoundary.md.
Static plugins can also import declarative project resources at compile time. The project names the importer, asset identity, and path explicitly:
{
"plugins": ["RetroSharp.PushBox"],
"resources": [
{
"name": "levels",
"importer": "RetroSharp.PushBox.Levels",
"path": "assets/levels.sok"
}
]
}
Resource paths are resolved relative to the project manifest. The importer runs
inside the registered host plugin and contributes deterministic immutable byte
tables plus static u8(u8) lookup functions; Game Boy and NES retain ownership
of ROM placement and lookup emission. This is not a pre-build script or a
runtime parser.
When a library package opts into namespaceMode: "physical", folder names under
sourceRoot become compile-time namespaces just like project sources. Root
source files commonly act as the package's public facade after import, while
helper files under folders can reuse names without colliding with the game or
with other packages.
Portability is capability-based, not magic. A shared source file is portable
only when each selected target can support the requested scrolling mode, sprite
budget, palette slots, audio format, runtime tile writes, and asset shape. See
docs/Portable2DSdkV1.md for the current API
reference.
Project objectives
- Keep the language small, explicit, and independent from any one machine.
- Let the framework make small real 2D games practical without hiding hardware budgets.
- Keep raw machine access available through target intrinsics.
- Make Game Boy and NES the first compatibility pair for portable SDK work.
- Prefer zero-cost ergonomics over managed-runtime features: no heap, GC, RTTI, boxing, virtual dispatch, closures, delegates, or hidden object identity.
Which platforms does it compile for?
The active targets are Game Boy and NES, implemented under
src/RetroSharp.GameBoy and src/RetroSharp.NES. A Master System target is in
development under src/RetroSharp.MasterSystem; see
docs/MasterSystemTarget.md. Runnable samples live under
samples/; only samples marked portable-sdk in samples/manifest.json are
cross-target SDK evidence.
The retired Z80 compiler path, its exact recovery commit, and the two external
forks it used are recorded in
docs/history/LegacyZ80Compiler.md.
Installation
RetroSharp is distributed as a .NET tool:
dotnet tool install --global RetroSharp.Tool
Then use it to compile your programs:
retrosharp --target gb --out myprogram.gb myprogram.rs
Build the Game Boy runner from source:
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target gb \
--out games/runner/bin/runner.gb \
games/runner/runner.retrosharp.json
For games split across several source files, use a RetroSharp project manifest. It is a small JSON file owned by RetroSharp, not an MSBuild project:
{
"targets": [ "gb", "nes" ],
"outputs": {
"gb": "bin/runner.gb",
"nes": "bin/runner.nes"
},
"rootNamespace": "Runner",
"sourceRoot": "src",
"namespaceMode": "physical",
"sources": [
"src/Program.rs",
"src/player/State.rs",
"src/camera/State.rs"
],
"libraryPaths": [
"lib"
],
"libraries": [
"RetroSharp.Portable2D"
]
}
Then build the project file directly:
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- retrosharp.json
Inspect a Tiled world's deterministic Game Boy or NES WorldPack and target
budgets without building or writing a ROM by opting into
--world-budget-report:
dotnet run --no-launch-profile --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target gb \
--world-budget-report \
samples/tiled-cross-target-2d-scroll/cross-target-2d-scroll.tmj
The option accepts one importer-ready .tmx or .tmj, requires --target gb or
--target nes, cannot be combined with --out, writes one deterministic
retrosharp.world-budget/v1 JSON object to stdout, and writes nothing to
stderr on success. Without this option, successful GB/NES stdout and stderr
remain unchanged. --no-launch-profile prevents the .NET launcher from
prepending its own status line to the CLI's JSON stdout.
The JSON reports source and hardware dimensions, metatile placements and
unique metatiles, chunk count, the exact visual/collision stored bytes from the
real WorldPack chunk directory, total serialized bytes, generated target
tiles, staging RAM, ROM/PRG bytes and required banks, and the bounded VBlank
tile/attribute commit shape. Diagnostics are always ordered as addressing,
rom-prg, chr-tile-count, staging-ram, and vblank; each carries measured
usage, the evaluated limit/profile, and an actionable remedy when it
overflows. selectedProfile is deliberately null: the report names current
and accepted-future profiles but never selects a banker or mapper.
stagingRam.usedBytes is the pack's actual fixed-slot shape;
stagingRam.limitBytes is the accepted WorldPack v1 maximum (554 bytes on GB,
594 on NES), while physicalRamCapacityBytes separately names the machine's
total RAM capacity. Addressing permits a 32,768-pixel extent because its last
coordinate is 32,767.
NES keeps nes-mapper-0-current separate from the accepted but unimplemented
nes-mmc3-tvrom-v1-accepted-future requirements. The report likewise keeps
currentPhysicalChrCapacityBytes,
acceptedFuturePhysicalChrCapacityBytes, the 8 KiB resident CHR limit, and the
256-entry tile-index budget as separate facts. Game Boy
cartridge.romPrgBytesUsed is measured content; allocatedRomBytes stays
null, so a padded output ROM length is never misreported as bytes used.
See how full a cartridge already is, how much user-local RAM is left and what
fills it, and where the next change will land, by adding --capacity-report
to a normal build:
dotnet run --no-launch-profile --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target nes \
--capacity-report \
--out games/runner/bin/runner.nes \
games/runner/runner.retrosharp.json
The option takes a single target and cannot be combined with
--world-budget-report, which inspects a world without building. It writes one
retrosharp.nes-capacity/v1, retrosharp.gb-capacity/v1 or
retrosharp.sms-capacity/v1 (Master System and Game Gear, with target sms
or gg) JSON object to stdout and does not change a single emitted byte: the
ROM is identical with and without it. Fields added to a report are additive
within its schema version: retrosharp.nes-capacity/v1 gained locals and the
user-locals resource, which can now be the bindingConstraint, and every
report gained locals.breakdown, without a version change. There is no built-in diff: to see what a change costs, such as
one more local or a dynamic camera follow policy instead of one configured in
setup position, compare the reports of the two builds.
Every report leads with bindingConstraint, the scarce resource currently
closest to its own capacity, and shares locals and resources.
locals measures the user-local reserve that holds a program's locals and
globals (222 zero-page bytes on NES, 224 WRAM bytes on Game Boy, 224 RAM bytes
on Master System and Game Gear): usedBytes, capacityBytes,
headroomBytes, the bytes covered by named variables, and the unnamed rest,
such as the seven bytes NES arithmetic loops reserve the first time a program
needs them. locals.breakdown groups the same bytes by what they are for:
each ActorSlots pool, found by its type, with its slots,
bytesPerSlot and the spawnStateBytes the SDK keeps beside it, other SDK
state such as a camera follow's, the program's named variables, the
temporaries of inlined calls, unnamed scratch, and freeBytes
(docs/Portable2DSdkV1.md, "RAM per slot and how many slots fit"). Which
function needed a temporary is not attributed. The user-locals resource
repeats the same figures, so a program a few locals from full reports it as
the binding constraint and warns. A build that needs more than the reserve
fails with an error that names user-locals and its capacity, for example
NES user-locals is exhausted: the program needs at least 223 bytes, but its capacity is 222 bytes., followed by the allocation that did not fit and the
same groups as they stood before it, named in an author's words rather than by
the SDK's internal names that the report's sdkState keeps; the CLI places it
at the first name it quotes, an ActorSlots pool's declaration when one is
listed. With --capacity-report the CLI then prints no report and says that
the build failed. The Master System and Game Gear report has
selectedProfile (rom-only or sega-mapper), locals and the
user-locals resource only, so its bindingConstraint says nothing about the
ROM.
The Game Boy report adds program, the linker's placement of the contiguous
program from the end of the cartridge header: its layout (rom-only or
mbc1), bytes, the bytes the layout holds and the room left, its per-bank
occupancy, the headroom in its last bank, and, once it spans switchable
banks, the fixed-resident helper bytes that must stay in bank 0. banks lists
every cartridge bank's measured occupancy and owners. Its resources are
user-locals, rom-only-program on a ROM-only cartridge, and on MBC1
fixed-bank-helpers and mbc1-banks (banks used of the 32 the current
lowering addresses). Running out of either of the last two fails the build
with the same kind of error, naming fixed-bank-helpers or mbc1-banks and
its capacity. Per-frame CPU work stays in --build-report-out.
The NES report also names the selected profile and PRG/CHR
size; the fixed PRG region's used bytes, remaining headroom and capacity; the
banked program's phase-to-bank map with per-bank occupancy and remaining R6
headroom; the bytes attributed to fixed veneers, pinned R7, boot R7 and
resident CHR; the top user-function duplication holders; and per-frame CPU work
for each named window. resources repeats user locals, fixed PRG, whole R6, the hot phase's
per-bank cap, and the CPU windows with the same vocabulary: name, unit, use,
capacity, headroom, binding status, next unit cost, and named remedy. Region
capacity is used plus headroom as the build measured it, not a re-derived board constant, and
bankedProgram is null for a program that is entirely fixed-resident. Shared
SDK subroutines and outlined user functions are counted with their call sites
rather than sized, because the build measures no byte total for either.
Two figures are deliberately labelled rather than trusted. duplication is a
lower bound: pure functions the compiler answered from a generated ROM table
never reach the user-function path, so it is not total recoverable headroom.
callsPerFrame is static: it is not path sensitive and does not multiply loop
iterations, so it is a lower bound where repeatsPerFrame is set and an upper
bound over mutually exclusive branches otherwise. cpuWork is an observation
per named window — frame is the whole NTSC frame, video-safe is the VBlank
window. The video-safe figure is the full cost the NES frame plan already
imposes, including NMI entry reserve, frame-boundary bookkeeping, packed column
commit, and retained-OAM publication. Other incomplete CPU windows still mean
uncalibrated work exists outside the known figure.
When any reported resource's headroom falls below five per cent, including
user locals, the hot phase's per-bank cap or a CPU window, the report adds a
near-cliff warning and the CLI repeats it on stderr. That is a warning, never a gate: spilling into
R6 and escalating the board are designed behaviours where allowed, so the build
still succeeds and nothing here can fail it.
Project paths are resolved relative to the JSON file. --target, --out, and
additional --lib-path options still work as command-line overrides. Use
project sources for code that belongs to the game itself; use
retrosharp-library.json packages when code should behave like an external,
imported dependency. libraryPaths tells the CLI where to discover local
packages; libraries names the package import paths that this project loads.
Source-level import still works as the explicit transitional form, but
C#-style project code normally declares loaded libraries in the manifest and
uses using only for namespace lookup inside source files.
When namespaceMode is physical, RetroSharp derives zero-cost source
namespaces from the path under sourceRoot: src/player/State.rs belongs to
Runner.Player, while src/camera/State.rs belongs to Runner.Camera. The
compiler lowers those names to unique internal symbols, so folders can contain
same-named helper classes such as Rules; source can refer to static constants
with path-qualified names like Player.Rules.Start.
Export a GBS subsong into a Game Boy APU trace when you want faithful playback of the original register writes:
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
gbs-to-gbapu \
--in path/to/theme.gbs \
--subsong 1 \
--seconds 120 \
--out path/to/theme.gbapu
This writes a compact binary .gbapu (use --emit-json for a JSON debug view, or gbapu-dump <file> to print the register writes). The loop is auto-detected and the capture is trimmed to one
loop body (--no-auto-loop/--loop-cycle override). Use the result directly with
Music.Asset(...). The source keeps cycle deltas for every supported APU register write; the ROM
compiler maps them onto DMG VBlank frames, removes redundant non-trigger writes, deduplicates
repeated frame groups into a pool, and stores full Wave RAM uploads as compact block commands for
Audio.Update() playback.
The helper requires gbsplay on PATH unless --gbsplay <path> is supplied. GBS files are not loaded directly by Music.Asset(...). See docs/GameBoyApuTraceFormat.md for the format shape, runtime use, limitations, and future alternatives.
Build the first cross-target camera sample for both cartridge targets:
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target gb \
--out /tmp/cross-camera.gb \
samples/cross-target-camera/camera.rs
dotnet run --project src/RetroSharp.Cli/RetroSharp.Cli.csproj -- \
--target nes \
--out /tmp/cross-camera.nes \
samples/cross-target-camera/camera.rs
The sample sources have been migrated to the current language surface: symbolic const values and enums for static data, aliases where they clarify byte-backed values, let for immutable frame-local values, inline/pure helper contracts, SDK dot-calls, restricted static classes and receiver methods where they clarify ownership, switch expressions, pipelines, while (true) for infinite runtime loops, and compound mutation syntax where it maps directly to the old explicit assignments.
AI agent orientation
Repository-specific instructions live in AGENTS.md. For tasks that need
architecture, acceptance, or ownership context, docs/AgentContext.md routes
to the relevant owner document. Completed roadmaps are historical context, not
a startup bundle. llms.txt is the compact documentation index.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.1254 | 106 | 10/30/2026 |
| 0.0.1246 | 99 | 10/29/2026 |
| 0.0.1242 | 104 | 10/29/2026 |
| 0.0.1240 | 100 | 10/29/2026 |
| 0.0.1231 | 106 | 10/28/2026 |
| 0.0.902 | 121 | 10/13/2026 |
| 0.0.777 | 110 | 10/5/2026 |
| 0.0.769 | 111 | 10/5/2026 |
| 0.0.662 | 121 | 9/27/2026 |
| 0.0.661 | 119 | 9/24/2026 |
| 0.0.660 | 115 | 9/24/2026 |
| 0.0.659 | 116 | 9/24/2026 |
| 0.0.658 | 115 | 9/24/2026 |
| 0.0.657 | 112 | 9/24/2026 |
| 0.0.656 | 104 | 9/24/2026 |
| 0.0.655 | 122 | 9/24/2026 |
| 0.0.654 | 112 | 9/23/2026 |
| 0.0.653 | 119 | 9/23/2026 |
| 0.0.652 | 120 | 9/23/2026 |
| 0.0.651 | 118 | 9/23/2026 |
# Changelog 0.0.1231
## Changes since v0.0.902
- Docs: drop the -m:1 left over from the old gate (#2340) (#2341) (`65778d8e`)
- runner-lift: remove stray platform tiles from the overworld map (`a4d2808e`)
- Retire three duplicate paths from roadmap #2237: world pack assembly, byte VerticalScrollMax, World.Load reader (#2332) (#2333) (`ce75a3b3`)
- runner-lift: doors lead to a cave and back (#2234) (#2331) (`0247913c`)
- Tests: the implementer runs publish's architecture check before handing back (#2328) (#2329) (`2cbd4f56`)
- runner-lift: Mario climbs the vine (#2236) (#2327) (`d8e2f6bd`)
- Tiled: a trigger names the object it leads to, instead of coordinates (#2323) (#2326) (`c7b96b9d`)
- Game Boy: the camera's column plane holds each activated world at its own height (#2315) (#2324) (`abd59c10`)
- NES: one flags read for a word column, for World.Collect and World.TileKindAt (#2320) (#2321) (`6a211ddb`)
- Game Boy and NES: World.TileKindAt over worlds wider than 256 columns (#2318) (#2319) (`399642e6`)
- Tests: one grouping of the cost marks and one discovery of Python modules (#2282) (#2317) (`0c2795ef`)
- Game Boy: read the collision of activated worlds from the compiled plane (#2316) (`9421e533`)
- Tests: suite.py refuses a filter that selects a whole project (#2313) (#2314) (`88b0f8b3`)
- NES: animate the background of streamed worlds that activate (#2292) (#2311) (`a9f32bb4`)
- Master System and Game Gear: animate the tiles of each activated world (#2269) (#2309) (`450316fd`)
- runner-lift: a second enemy, the Ninji, in the same pool as the goombas (#2235) (#2308) (`5c8a4b72`)
- NES: streamed columns carry up to 60 rows for any world width (#2289) (#2306) (`b85c2154`)
- Master System and Game Gear: Camera.Follow over an activated world taller than 49 rows (#2277) (#2305) (`17d28f92`)
- runner: Mario collects the coins (#2295) (#2304) (`1b924c91`)
- NES: activate streamed worlds of different shapes (#2302) (`3987a208`)
- World.Collect: Game Boy and NES share the check of its plan (#2298) (#2300) (`fd30d1e9`)
- Game Boy: World.Collect in worlds wider than 256 columns (#2294) (#2299) (`b17ee4bf`)
- runner: every coin is the animated coin, and tile kinds work under physical namespaces (#2233) (#2296) (`35c0d704`)
- Master System and Game Gear: collect and change tiles in worlds taller than the screen (#2232) (#2291) (`6bd4e3e9`)
- Game Boy: the world pack's directory leaves the fixed bank, so runner-lift's exterior and cave fit (#2284) (#2288) (`e3113b86`)
- Master System and Game Gear activate worlds of different sizes (#2271) (#2285) (`c71fa1ea`)
- NES: a world stores the CHR pages of the frame combinations its timers show (#2268) (#2283) (`f0dc79a4`)
- Tests: three slow tests check the same thing with fewer compilations (#2275) (#2281) (`61eca6b7`)
- Tests: outside the gate, a test run is filtered (#2274) (#2280) (`e0a88927`)
- NES: collectibles and World.SetTile in worlds the camera scrolls vertically (#1520) (#2279) (`c3214a4f`)
- SDK: Camera.Follow follows a target down a Game Boy world taller than 49 rows (#2270) (#2278) (`c3a402e3`)
- Workflow: opus implementer for changes to the language's semantics (#2272) (#2273) (`e177d310`)
- Compiler: one integer range table for LoopEntry and the constant store check (#2264) (#2266) (`5b3d3681`)
- Compiler: a constant store checks the exact range of its type (#2239) (#2263) (`622854b6`)
- Language: a method of a class names its constants without qualifying them (#2255) (#2262) (`f11c313b`)
- Compiler: one type scope for LetTypeInference and BoolConversionValidator (#2257) (#2258) (`e4232345`)
- Language: a class constant names its sibling members without qualifying them (#2210) (#2256) (`3424e35d`)
- Language: a variable named like a static class hides the class's members in its scope (#2213) (#2254) (`89836c70`)
- SDK: a named constant in a follow-profile condition selects the static camera policy (#2227) (#2250) (`658cd495`)
- Language: retire the omitted-default condition that ReadInPlace already covers (#2243) (#2246) (`089034e3`)
- Compiler: retire the unreachable multi-candidate argument path in BoolConversionValidator (#2241) (#2245) (`81d0424c`)
- Language: a helper that writes its by-value parameter leaves the caller's variable alone (#2031) (#2244) (`f2a4836c`)
- Compiler: check a constant against its storage type at every store (#2217) (#2242) (`21823fa0`)
- Language core: retire the duplicates and uncalled code left by #1372 (#2230) (#2231) (`cba47718`)
- snow-bros-jr: enemy walk clip and shot initializers (#2202) (#2229) (`f1dca67e`)
- snow-bros-jr: readable without new forms (#1937) (#2228) (`c15b35fb`)
- Games: runner and runner-lift share Mario's code and name their constants (#1938) (#2226) (`5f2da9f4`)
- SDK: one check for each animation clip bound (#2220) (#2225) (`2067e408`)
- Workflow: the batch picker skips issues that share files with the running lane (#2221) (#2224) (`67754cc6`)
- Workflow: redesign a mechanism that review keeps finding defects in (#2222) (#2223) (`b36eaebe`)
- SDK: ping-pong and one-pass animation clips (#1934) (#2219) (`6f8e4b05`)
- Compiler: one lowering for struct initializers in declarations (#2214) (#2218) (`71aac375`)
- Language: compound operators *=, /=, %=, <<= and >>= (#1371) (#2216) (`0597accf`)
- Language: assign a typed struct initializer to a variable, a field, an element or a union case (#1369) (#2215) (`2b2ac5a7`)
- push-box: derive board origin and camera from Viewport and the board size (#1370) (#2212) (`e79b67a2`)
- push-box: Board.Bit as a const array (#1931) (#2209) (`189db126`)
- Language: ref locals as compile-time aliases of an element or a field (#1368) (#2208) (`6480cac3`)
- Workflow: agents load the RetroSharp language server for .rs sources (#2206) (#2207) (`32030836`)
- SDK: one CollisionFlag enum, replacing the copies in games and samples (#1936) (#2205) (`f0602047`)
- SDK: one Cadence for acting every N steps, replacing ShooterCadence (#1935) (#2204) (`05ad1387`)
- Compiler: retire the unused conditions and parameter left by roadmap #2175 (#2199) (#2200) (`31998b98`)
- Resources: reject the statement form and migrate the tests that used it (#2194) (#2198) (`0577fcbb`)
- Workflow: one gate per issue, and a new reviewer after each round of fixes (#2196) (#2197) (`08ecdcaa`)
- Resources: migrate samples and runner diagnostics to typed declarations (#1421) (#2195) (`f313e524`)
- Game Boy: retire UGE music input and its row playback (#1002) (#2193) (`a3bb35c3`)
- Game Boy: retire the raw intrinsics no program uses (#2179) (#2192) (`ffd96c75`)
- Tests: one helper for the repository root, CRC32 and PNG (#2180) (#2191) (`52c0d997`)
- Master System and Game Gear: retire the camera's row edge lists and edges taking turns (#2178) (#2190) (`e9a77169`)
- Workflow: a compiler crash or a silent miscompile is filed as a follow-up issue (#2187) (#2189) (`ab8be263`)
- NES: the frozen camera modes are retired (raw rows over 60 rows, columns that follow the camera, staged columns) (#2176) (#2188) (`3862cb18`)
- NES: retire the activation of streamed worlds (#2177) (#2186) (`a0652ca9`)
- Z80: word sums and differences are evaluated in DE instead of the word scratch (#1476) (#2185) (`619a0398`)
- Workflow: batch waits end before the Bash timeout and do not pile up (#2183) (#2184) (`ff93bc82`)
- Game Boy: the hottest runtime bytes live in high RAM and the stack moves to the top of WRAM (#1478) (#2182) (`b497db35`)
- Z80: direct jumps are JR when their target is in reach, and a rebuild promotes the others to JP (#1477) (#2181) (`c765c8fd`)
- Camera: retire the leftovers of roadmap #2087 (an unreachable Game Boy step branch, the restart rule written three times, unread StreamMap fields) (#2173) (#2174) (`6098ca0f`)
- NES: world activation writes Activating only with animations, and the docs say when the caches are cleared (#2122) (#2172) (`f21d6b61`)
- NES: retire the unused negative-offset path of the packed wide source column (#2167) (#2171) (`bc26a05d`)
- Camera plan: the Master System rings, the NES unpacked four-screen row modulo and the Game Boy streamed row width read the plan (#1798) (#2170) (`a5d76d6b`)
- Master System and Game Gear: a run-time Camera.Init redraws the world at its start (#2125) (#2169) (`6366da60`)
- NES: retire the before-the-world column round and RoundBias, and test the left trim (#2130) (#2168) (`b57092b1`)
- NES: retire the unused camera row staging and fold the two nametable seed loops (#2141) (#2166) (`318d6659`)
- Game Boy: the row setup over an activated world uses camera_active_world_column_from (#2149) (#2165) (`e45aac79`)
- Packed camera: the ring and edge literals read SdkCameraStreamPlan (#2146) (#2164) (`f76d4e0d`)
- SDK 2D: retire the raw map_stream_column and map_stream_row calls (#1420) (#2163) (`ead5d4ee`)
- Game Boy: the packed camera without diagonal goes through the diagonal path (#2157) (#2162) (`ea217ac4`)
- Master System and Game Gear: Camera.Apply commits at the frame wait, with no immediate form (#2158) (#2161) (`619aaf39`)
- NES camera: the short Y tracking goes through the general path (#2152) (#2160) (`df86d561`)
- Game Boy: the column window merges into the unpacked camera's queue (#2153) (#2159) (`9b936a01`)
- Packed camera: one stop rule and one seeded-rows rule in the plan on Game Boy and NES (#2144) (#2156) (`47739181`)
- Packed camera: the patch-or-enqueue choice of SetTile and Collect is read from the plan on Game Boy and NES (#2148) (#2155) (`1d6f407b`)
- A run-time Camera.Init redraws the world at its start on Game Boy and NES (#2143) (#2154) (`b09f583e`)
- Game Boy: the #1709 test checks that the activation turns the LCD off only in vertical blank (#2121) (#2150) (`20db5d35`)
- ADR 0018: the packed camera's policy is written once for Game Boy and NES, with its census and migration order (#2138) (#2147) (`6038ff4b`)
- Workflow: a follow-up issue only for a regression, a safety defect or what a game shows (#2140) (#2142) (`575fe38a`)
- NES: a streamed world narrower than 272 px redraws World.Collect and World.SetTile at every copy (#2129) (#2139) (`c6e3eb1a`)
- NES: a camera over a world taller than its nametables writes each row and column ahead of the screen (#1703) (#2137) (`4cf8f001`)
- NES: a world that repeats vertically shows its rows again below it without four-screen nametables (#1773) (#2133) (`8f271e23`)
- NES: a streamed world's camera counts round past the active world's edge (#1909) (#2123) (`882ce34f`)
- Workflow: batch sessions and the reviewer run on opus, whatever the user's default model (#2115) (#2117) (`0356c565`)
- NES: the tile animation pauses while a world is pending (#2114) (`5e44509b`)
- NES: a world activation clears the raw cell caches when the new world is mapped (#1729) (#2113) (`31a91336`)
- NES: the packed camera column holds the world's rows and the empty tile below a short world (#1363) (#2111) (`d9a160a6`)
- batch: a merged session without structured output counts as merged, and a session that never started is resumed once (#2109) (`6037c325`)
- Game Boy: the cursor path drops its empty hooks and one of two equal scanline constants (#2107) (#2108) (`feded62c`)
- Game Boy: la lista de fila de la cámara sin empaquetar dice qué garantiza (#2104) (#2106) (`25cee84f`)
- Game Boy: a streamed row's list holds a full row of writes (#2102) (#2105) (`b94161de`)
- Game Boy: a streamed row's list holds every cell of a diagonal span across a wide world's edge (#1915) (#2103) (`3934c2db`)
- Game Boy: the camera's vertical scroll waits for a row the vertical blank could not write (#1898) (#2100) (`4699161b`)
- Game Boy: the packed camera's diagonal pan across an active world's edge advances every frame (#1925) (#2099) (`9ee3809b`)
- Game Boy: the world activation advance waits for vertical blank (#2098) (`bebe83dd`)
- Game Boy: Camera.VerticalScrollMax counts the band's start row (#1858) (#2097) (`3585a803`)
- Game Boy: the packed camera over a world that starts below row 0 shows the world where it is and the empty tile above it (#2093) (#2096) (`a0a04c9b`)
- Game Boy: the unpacked camera over a world that starts below row 0 shows the world where it is and the empty tile around it (#1900) (#2094) (`c789c563`)
- Game Boy: the packed camera shows the empty tile below a world lower than the screen (#2047) (#2092) (`2d86f6be`)
- Game Boy: a cell World.SetTile changes while the packed camera keeps moving shows within two frames, a test guards it (#1825) (#2091) (`2ae9ab1e`)
- Game Boy: a commit too late for a yielded vertical blank leaves no credit, so a busy game never walks 8 px at (4,4) (#2051) (#2090) (`a0fda126`)
- Game Boy: one routine shares each vertical blank, so animated tiles run at (4,4) (ADR 0017 D1) (#2055) (#2088) (`f7944da3`)
- Game Boy: a test bounds each VBlank price by the measured cycles (ADR 0017 D3) (#2054) (#2086) (`2a06256d`)
- Game Boy: animated tiles upload tile by tile through a cursor (ADR 0017 D2) (#2081) (#2085) (`58f2cdbe`)
- Master System: animated tiles upload whole roots by room, in turn (ADR 0017 D2) (#2053) (#2084) (`1aa1ca37`)
- Runtime: each memory map validates only in its static constructor; drop unused ShownSpriteColors (#2079) (#2080) (`bcbb40ba`)
- Language: expression dispatch lives once for NES and Z80 through a per-CPU port (#2073) (#2078) (`873ad55f`)
- Workflow: batch lanes wait for their session in the foreground (#2076) (#2077) (`e570ade7`)
- Language: statements and loops live once for NES and Z80 through a per-CPU port (#2072) (#2075) (`d5dfe2d8`)
- Language: condition jumps live once for NES and Z80 through a per-CPU port (#1952) (#2074) (`ecf4b984`)
- SDK: NES world AABB walk emits its hit step through EmitAabbHit (#2069) (#2071) (`708def12`)
- SDK: AABB tiles and hit-top share one probe walk per coordinate space on Game Boy and NES (#1989) (#2070) (`70a8e78d`)
- Workflow: batch lanes launch with the directory written out, not in a shell variable (#2067) (#2068) (`cb29843f`)
- Tests: collision runtime lowering rules run once for Game Boy, NES and Master System (#2000) (#2066) (`ba77c557`)
- Tests: Master System and Game Gear run the shared sprite lowering cases and drop their copies (#2062) (#2065) (`c1d0832a`)
- Tests: sprite tables read once through PublishedSprites on Game Boy, NES, Master System and Game Gear (#1999) (#2064) (`b065e850`)
- Tools: a Game Boy frame-budget step also ends where a packed commit enters the vertical blank without polling (#2052) (#2063) (`76021775`)
- Tests: camera follow, registered enemy, staircase and loading snapshot once for Game Boy and NES (#1998) (#2061) (`9fdeaf64`)
- Workflow: batch publishes, launches and picks without fragile steps, and /roadmap leaves two lanes to it (#2059) (#2060) (`57066c35`)
- Workflow: batch starts each session with a command one permission rule covers (#2058) (`88038664`)
- Tests: ManagedAudio and Portable2DSpriteAssetBaseline diagnostics once for Game Boy and NES (#1997) (#2056) (`6979bc37`)
- Language: a parameter or local named like a const holds its argument (#2044) (#2046) (`5ef7d689`)
- Language: a parameter named like a const reads its argument (#2037) (#2045) (`bf23c542`)
- Workflow: batch runs each issue's cycle as its own headless session (#2043) (`77e48e09`)
- Language: a ?: of an i8 and a u8 holds each arm's own value (#2019) (#2041) (`17a93531`)
- Workflow: a batch run that schedules issues through /issue in up to two lanes (#2040) (`f81e4c34`)
- Language: a let of an i8, or of byte arithmetic over one, stays an i8 (#2018) (#2038) (`fa1ee48f`)
- Language: a u16 compared with a negative constant has a fixed answer (#2025) (#2036) (`359908de`)
- Workflow: the implementer proves each acceptance criterion and waits for its background runs (#2035) (`4ce97b47`)
- Language: an argument the helper reads after something that can change it is the value at the call (#1910) (#2033) (`267c9236`)
- Language: a comparison with a constant that fits no integer type is a frontend error (#1964) (#2030) (`6557b503`)
- Workflow: the reviewer probes each changed path once and re-runs a mutation only when in doubt (#2028) (#2029) (`ad5dc56d`)
- NES: a wide constant in byte arithmetic takes its low byte, as on the Z80 targets (#1960) (#2027) (`7224405e`)
- Compare a u8 with a negative constant as signed 16-bit values (#2009) (#2026) (`716c25f2`)
- Language: mixed words, byte masks and selections mean the same in place as through a let (#1961) (#2023) (`323c8fcd`)
- Compare signed byte arithmetic as signed, and mixed-sign byte comparisons as the bytes they give (#2020) (`5e6074e0`)
- Retire recursion guards the frontend makes unreachable and drop the target from call-binding errors (#2017) (`a89fb4d2`)
- Language core: lvalue and overlap analysis lives once in LocalStorageAccess (#1972) (#2015) (`f2d19ba4`)
- Run Master System's projectile scenarios through the shared ActorProjectile class (#2014) (`9f70525a`)
- Workflow: /issue launches opus implementers only for new lowerings, banks, interrupts or calling conventions (#2012) (#2013) (`134b3876`)
- Tests: Master System runs the shared language tests and retires its copies (#1757) (#2011) (`22d0f004`)
- Compiler: an i8 compared with an unsigned byte above 127 compares as i16 (#1947) (#2010) (`ee0fe39c`)
- Workflow: slim /issue and /roadmap to the project's contract; sonnet for implementer and scout (#2006) (#2007) (`9efebd02`)
- Tests: ActorProjectile scenarios run once over Game Boy and NES (#1996) (#2004) (`22342276`)
- Tests: capacity report and runtime ABI projection run once over the targets (#1995) (#2002) (`f157bb61`)
- Tests: SDK frame input, plugin value lowering and the merged-helper twins run once over the targets (#1981) (#2001) (`cf94dd46`)
- Tests: language functions run once over Game Boy and NES (#1954) (#1994) (`86264213`)
- Language core: language errors NES and Z80 repeat come from one frontend check (#1951) (#1992) (`56e41816`)
- Language core: no per-target one-line wrappers, and one cast refusal point (#1987) (#1988) (`5053d0c0`)
- Language core: the rest of the constant reader and operator classifiers once (#1983) (#1986) (`ba6275e1`)
- Tests: SDK sprite lowering, edge pairs and the follow rebase run once over the targets (#1978) (#1985) (`b446f849`)
- Language core: transient pops that keep A once for NES and Z80 (#1980) (#1984) (`3f6e9a85`)
- Language core: one constant reader and one set of operator classifiers (#1950) (#1982) (`cfa6f2da`)
- Language core: the transient release marker and the capture expression once (#1976) (#1979) (`143fd3c2`)
- Tests: SDK and managed-game scenarios run once over the targets (#1957) (#1977) (`e94667fc`)
- Language core: transient and element-index captures once for NES and Z80 (#1971) (#1975) (`77e48ea0`)
- One runtime memory map and one runtime ABI projection for NES, Game Boy and Master System (#1956) (#1974) (`c343a06d`)
- Language core: local scopes and SDK storage once for NES and Z80 (#1949) (#1973) (`30c1ceaa`)
- Language core: one declaration path for a top-level local and a nested field (#1967) (#1970) (`f6a4f4bc`)
- Camera tests: strict cadence up to 4 px a frame on every machine, safety and image above it (#1955) (#1969) (`c5a6ba8f`)
- Language core: one declaration and reservation rule for structs and arrays on NES and Z80 (#1948) (#1968) (`a0115b2f`)
- WorldPack: one per-cell accessor for the Game Boy and NES runtimes and the tests (#1962) (#1966) (`802ebf6a`)
- Language: byte arithmetic with a wide constant means the same in place as through a let (#1903) (#1965) (`c3d3c717`)
- Retire production code nothing calls, and the unused Parser.Model project (#1953) (#1963) (`8761b92b`)
- Compiler: one operand type rule for the Z80 and NES backends (#1942) (#1945) (`4b93ab58`)
- Compiler: an array element read at a runtime index keeps its width and sign (#1940) (#1944) (`c03a5a27`)
- Language: an enum with a value over 255 is stored in 16 bits (#1890) (#1941) (`6c67db0b`)
- Language: const arrays in ROM, and every generated ROM table lookup keeps its index (#1367) (#1939) (`b9f34129`)
- Language: an inline helper's value computed by statements can go wherever its statement evaluates it once (#1366) (#1929) (`d93101cb`)
- ADR 0017: the Game Boy packed camera shares each vertical blank in one order, within its 4-pixel guarantee (#1919) (#1928) (`da5b814e`)
- Tools: a hook blocks an unbounded Read of a large file or a saved tool output (#1921) (#1924) (`cc54307d`)
- GB camera walks the active world: screen and queries agree past its edges (#1786, #1510) (#1923) (`bacbb310`)
- Workflow: a roadmap does next the cleanups it finds, and two lanes share no file (#1920) (#1922) (`e0afde7a`)
- Language: an argument with effects runs once and in source order, through one shared argument-capture rewriter (#1918) (`7bb21730`)
- GB packed camera: both axes every frame at top speed, no stopped frames (#1714) (#1914) (`962ed1e1`)
- Build report: list the compiler's fixed storage with function, bytes, address, position and reason (#1912) (`07fb60f6`)
- Language: let infers the smallest type its value fits and never truncates a wide constant (#1867) (#1908) (`e1a94749`)
- Language: the frontend reports cost warnings, placed at their construct by build, check and the editor (#1869) (#1906) (`815ed452`)
- Camera: streamed columns cover every row the screen shows under a short band (#1800) (#1901) (`cd465786`)
- Language: a value call standing as a statement keeps the effects of what it returns (#1511) (#1899) (`4c4a7700`)
- Philosophy: the language takes C# as its model, not as a copy (#1883) (#1884) (`a180b4b8`)
- Capture: an argument passed to a word parameter is computed at the parameter's width (#1523) (#1868) (`10c32744`)
- NES: a byte context reads the low byte of every constant, also of a folded inline call (#1465) (#1866) (`8de61b35`)
- Camera: one order-independent set-up rule; boot sets the camera up on every target (#1859) (#1863) (`f33f5636`)
- Hook: reject pgrep -f and pkill -f, which match their own shell (#1861) (#1862) (`1c66b74d`)
- Camera set-up: camera_init and VerticalScrollMax rules in the plan, one text on every target (#1850) (#1860) (`0b2e528e`)
- Collision: the box query refusals come from SdkAabbQueryPlan, one text on every target (#1849) (#1857) (`4b72fba2`)
- Camera step: the set-position roots on the shared emitter, family migrated in the brake (#1848) (#1853) (`ff230abd`)
- Ledger: the real status of collision, frame boundary, camera and tilemap writes (#1846) (#1852) (`5f5befc9`)
- Brake: the walk stops at any migrated family's primitives and at the named NES expression-compiler entry points (#1845) (#1847) (`01356fa4`)
- Join duplicates the roadmap left: one runtime Tilemap.Set rule and check, GB edge descriptions, unrolled-loop arithmetic (#1841) (#1844) (`b1756328`)
- Retire what the roadmap left unused, and small duplicates (#1840) (#1843) (`b5becb74`)
- Benchmarks: retire the temporary tolerance, baselines at the measured cost (#1640) (#1839) (`6b7fce46`)
- Tile animation: timers once in the plan, one Z80 emitter, the upload per machine (#1652) (#1838) (`3d8e9bfc`)
- Frame wait: SMS and NES copies call the target's one wait helper (#1811) (#1837) (`68ba635b`)
- Static setup: one walk for GB and NES, a target sink each (#1653) (#1835) (`98994cd7`)
- Workflow: fewer implementer turns, mutation checks from one script (#1832) (#1834) (`b93c2a84`)
- Camera: pending edges take turns as a plan rule on three targets (#1791) (#1831) (`dc5b4214`)
- Input: button state once in the plan, one Z80 emitter, the poll per machine (#1651) (#1829) (`bf6cdbd2`)
- Workflow: fixes never go back to the implementer that wrote the change (#1827) (#1828) (`44be973b`)
- Runtime Tilemap.Set is written at the frame boundary on every target (#1806) (#1826) (`445f2c21`)
- Game Boy: a loop from the end of the fixed region into the first switchable bank is kept in one bank (#1816) (#1824) (`54bb6ada`)
- Camera: one Z80 walk for GB and SMS/GG, signed-order direction on three targets (#1790) (#1820) (`20ea8af3`)
- Game Boy: one emitter for every frame wait, with master's commit-VBlank reuse per path (#1802) (#1817) (`62871976`)
- Frame boundary: one moment for the step counter (#1803) (#1815) (`0dbfd04b`)
- Workflow: prune the agents' rules to what review and tools do not already catch (#1814) (`eb7fe4d9`)
- A program that never waits drains its redraw queue inside a vertical blank on all targets: its camera application waits for the next one (#1807) (#1812) (`8a31b32d`)
- Z80 family: one emitter steps the camera cursors and queues the edges on GB and SMS/GG; GB refuses a vertically scrolled world over 256 rows (#1789) (#1810) (`2a5b6757`)
- Frame boundary: one layer-1 plan for the wait, the publication and the video jobs (#1649) (#1809) (`cb093fe9`)
- Game Boy: the packed camera admits a commit only while its bounded work fits the vertical blank (#1512) (#1805) (`b0d1c51f`)
- Sprites: explicit layout follows the one piece-offset rule on four targets, checked over the orientations each drawn sprite is drawn in (#1775) (#1804) (`63362681`)
- Camera: the cursor rules come from the plan's per-axis ring, source and edges; a camera over a world that streams rows reveals the screen's bottom row (#1788) (#1801) (`252eb26c`)
- Game Boy: World.TileFlagsAt and the tile-kind query share one cell-flag read through the query plan (#1772) (#1797) (`49261950`)
- SDK brake: World.Collect migrated under R2-R6 (#1779) (#1795) (`6d236926`)
- Camera: the walk cap, the direction rule and the NES band clamp come from one step plan; the NES byte walk no longer walks the wrong way to a far target (#1650) (#1792) (`07be0aae`)
- World collision: the plan states the band row, the repeating rows and the active world's bounds and columns (#1771) (#1787) (`c29a132e`)
- Z80 family: one enqueue for the redraw queue on GB and SMS/GG, lists first, then residency, then the queue (#1778) (#1783) (`57803b46`)
- World collision: one pixel-to-tile rule in the plan for all three targets (#1770) (#1781) (`c0f785ff`)
- SDK brake: SetTile is migrated, R6 checks the plan's span in each layout, and R2 reads every method the roots reach (#1699) (#1780) (`d1d969e3`)
- World collision: box queries read collision worlds with the flags of SetTile's tiles, built once by the plan (#1769) (#1777) (`f9418cf6`)
- Sprites: one band routine for the Z80 family; the Sprites family is migrated and the brake checks R2-R5 (#1687) (#1776) (`bd5c2973`)
- World collision: the plan classifies each box query's columns, rows and answer for all three targets (#1648) (#1774) (`f169a2f6`)
- Game Boy: adding an activated world's origin keeps the local column's high byte (#1723) (#1766) (`f04bd916`)
- Word comparisons with a constant that fits no byte compare both bytes on every target (#1756) (#1764) (`6c646e36`)
- Redraw queue: the stale-write rule is plan data read by the Z80 family and the NES (#1759) (#1763) (`8ff5c597`)
- Z80 family: one redraw queue for collectibles and tile changes, with one full-queue rule and one clear (#1698) (#1761) (`bf1ab71c`)
- Game Boy lifetimes fixture: main.rs draws the Marker once so its manifest compiles again (#1303) (#1760) (`c86d795d`)
- Tests: language tests copied between Game Boy and NES are written once and run on each machine (#1748) (#1758) (`f6c903cd`)
- Workflow: split multi-part issues before implementing; review nits open no new round (#1754) (#1755) (`517e77f1`)
- Master System and Game Gear redraw rows through the shared edge lists (#1728) (#1753) (`1320bec5`)
- Production clones: one NES palette table, Game Boy sprite sheets read PNGs through Core, one portable intrinsic list (#1749) (#1751) (`4ef2e2fa`)
- Sprites: Game Boy draws pieces through the shared Z80-family emitter (#1725) (#1747) (`33b8bca2`)
- Master System and Game Gear: one vertical blank budget for edges, redraws, the write queue, sprite streaming and tile animation (#1514) (#1746) (`659ba080`)
- Game Boy: animated tiles fit the vertical blank beside the sprites, or are uploaded one root per blank (#1707) (#1745) (`a8cbbbd4`)
- NES: a late step still shows its picture; every frame boundary waits for a fresh vertical blank (#1655) (#1744) (`db3dd15c`)
- Tests: the last tests that read games/ move to frozen copies and fixtures (#1736) (#1743) (`5153add6`)
- Tests: falling-blocks, push-box, ship and the guards stop reading games/ (#1734) (#1742) (`d07f8423`)
- Edge lists: one Z80-family core for columns and rows; Game Boy streams rows through it (#1697) (#1741) (`13f50efd`)
- Sprites: Master System and Game Gear draw pieces through one Z80-family emitter (#1686) (#1740) (`c0aea4da`)
- Tests: runner and runner-lift tests run on the frozen copies or are retired (#1733) (#1738) (`4a4d1b7a`)
- Tests: snow-bros-jr tests run on the frozen copy or are retired (#1732) (#1737) (`0328aecd`)
- Workflow: no test reads games/; game tests run on the frozen copies (#1731) (#1735) (`ef4ba1dd`)
- NES: retire run-length decoding of world packs; reject RLE planes at build time (#1719) (#1730) (`cbd2fa5d`)
- Master System and Game Gear redraw columns through the shared edge lists (#1696) (#1727) (`e405b799`)
- NES: the first frame shown is where the camera is (#1713) (#1726) (`27d455b7`)
- Banking: a jump from the fixed bank never reaches a switchable bank unselected; ActorSlots.Draw drops its 48 px skip (#1718) (#1724) (`ecc8b13a`)
- Retire camera lifecycle counters, the NES pack probe and test-only hooks; camera acceptance reads what the screen shows (#1682) (#1722) (`210ac502`)
- Sprites: one clipping rule hides a piece off the screen and never wraps it (#1685) (#1721) (`8035884b`)
- Column edge lists: one Z80-family emission, Game Boy supplies primitives (#1695) (#1716) (`500024cf`)
- Tests: runner and runner-lift at their camera's top speed on NES, Game Boy and Master System (#1676) (#1715) (`cb064a5d`)
- Sprites: one rule hides what was not drawn since the last publication (#1712) (`16eca22a`)
- World activation: SetTile and Collect are refused while it is pending, on every target (#1694) (#1711) (`5eb651a7`)
- NES column lead, cost, cadence, activation and lowering tests: check what the screen shows, not the internal path (#1681) (#1710) (`6e4f2c5a`)
- Redraw refusals: the plans own every SetTile and collectible check (#1647) (#1708) (`a5ef513f`)
- NES pack reader, large world and whole-column tests: check what the screen shows, not the internal path (#1680) (#1706) (`4fa815c7`)
- Workflow: look up existing test helpers before writing one (#1702) (#1705) (`4cd8f2c1`)
- Camera follow tests: read the camera the screen shows, not the internal follow state (#1688) (#1700) (`afc62b9c`)
- Collectibles: one Z80-family emission of the per-cell routines (#1669) (#1693) (`e1d0ae3f`)
- Sprites: one layer-1 draw plan for reservation, refusals and the clip mode (#1646) (#1692) (`907d7047`)
- Camera and streaming tests: check what the screen shows, not the internal path, on Game Boy, Master System and Game Gear (#1662) (#1689) (`2c134666`)
- SetTile queries: one Z80-family emission behind a primitives port (#1668) (#1683) (`bd81dea7`)
- Semantics brake: keep R1 and the table builders, count every own error of the lowerers (#1659) (#1679) (`2f971ec3`)
- SetTile and collectibles: one contract for the Game Boy and Master System queries (#1667) (#1678) (`5cf1acea`)
- NES: retire sliced column preparation; every packed camera that streams columns prepares whole columns with a lead (#1675) (`f9b05d36`)
- Tests: observable behaviour is what the layer under test promises its client (#1673) (#1674) (`545e2cf9`)
- SetTile and collectibles: preparations for the shared Z80 emission (#1658) (#1672) (`6fb5e2b1`)
- Rules: least code in tests too, Khorikov's testing doctrine, an own temporary directory per agent (#1670) (#1671) (`f4852bbb`)
- SetTile and collectibles: shared bookkeeping in the SDK plans (#1666) (`12087aba`)
- Workflow: fight entropy in every issue, bounded so it is no rabbit hole (#1664) (#1665) (`6e937029`)
- Workflow: a technical criterion, not the owner, decides what joins a roadmap (#1660) (#1661) (`d9484337`)
- Benchmarks: temporary performance tolerance during the SDK unification (#1638) (#1657) (`98dff31d`)
- Architecture: brake against new SDK semantics in target lowerers (ADR 0016 D8) (#1639) (#1656) (`7ee3ee57`)
- ADR 0016: SDK semantics once, emission per CPU family, hardware per machine (#1637) (#1654) (`1cd09c9c`)
- NES: the packed static band writes from the world's row, and the band at the camera's row is retired (#1615) (#1645) (`fe4a4011`)
- Workflow: the scout compacts its context past 200K tokens too (#1642) (#1643) (`13e23a3c`)
- AGENTS.md: keep the language free of quirks; costly constructs warn instead of failing (#1633) (#1634) (`5275774b`)
- GB: cap the packed camera's walk at 8 pixels a call so sprites stay on the background (#1632) (`69d92007`)
- Camera: census of camera paths, remove NES packed row streaming and the unused RLE decoder, name the GB unpacked commit (#1619) (#1631) (`aa91fee5`)
- GB: a frame wait that reuses a commit's vertical blank hides the sprites its frame did not draw (#1577) (#1630) (`56c83d0c`)
- issue.py cleanup: fast-forward the main checkout so merged rules take effect (#1627) (#1629) (`c2876905`)
- Workflow: one state comment per roadmap instead of piled handoff notes (#1625) (#1626) (`aa17d68a`)
- AGENTS.md: cover most use cases with the least code, optimize last (#1622) (#1624) (`7501c537`)
- Workflow: the owner decides the product, agents decide technical choices (#1621) (#1623) (`73e1f2f7`)
- Workflow: bound the implementer's context and keep roadmaps converging (#1617) (#1618) (`8e1fbec3`)
- NES: Camera.Init must start its stream area at the world's stream row for the raw camera (#1608) (#1616) (`12a67666`)
- GB: share the column-window checks between the unpacked camera's SetPosition sites (#1603) (#1614) (`33c52b29`)
- Build: pin System.Security.Cryptography.Pkcs 8.0.2 over the vulnerable 8.0.1 (#1612) (#1613) (`7bd87372`)
- NES: the four-screen camera stays inside its band when the world is taller (#1605) (#1609) (`12f741d5`)
- GB: the unpacked camera moves only over a window of columns written ahead, with a lead (#1549) (#1607) (`7263f692`)
- NES: refuse at compile time the four-screen packed camera that cannot keep its picture (#1601) (#1606) (`618edbb4`)
- NES: walk a camera that keeps a column lead up to 16 px a frame (#1548) (#1600) (`3980f7d8`)
- Camera.Follow: place the camera near a far entry instead of walking to it (#1509) (#1598) (`00df9dd2`)
- NES: move the camera only over columns the window holds in static bands (#1547) (#1596) (`a65c0a23`)
- NES: prepare the lead's columns in pairs so a vertical blank can write two (#1589) (#1595) (`89e5cefb`)
- NES: write rows 30 and up of a four-screen band into the nametable below (#1585) (#1594) (`ceca88bf`)
_18 more changes left out: NuGet limits release notes to 35000 characters._