RetroSharp.Tool
0.0.902
See the version list below for details.
dotnet tool install --global RetroSharp.Tool --version 0.0.902
dotnet new tool-manifest
dotnet tool install --local RetroSharp.Tool --version 0.0.902
#tool dotnet:?package=RetroSharp.Tool&version=0.0.902
nuke :add-package RetroSharp.Tool --version 0.0.902
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, hUGETracker.ugeBGM playback,.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). The Master
System and Game Gear reports carry only the ROM size and hash and the runtime
arithmetic 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.CommitCount, 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.902
## Changes since v0.0.777
- NES: the committed column window evicts whole attribute blocks (#1541) (#1556) (`b6b18a40`)
- GB: redraw a coin taken or a cell changed at the right end of a scrolling world (#1521) (#1555) (`065d073e`)
- Docs: ADR 0015, the camera moves only over columns written ahead (#1336) (#1553) (`5e7786dd`)
- GB: two columns per vertical blank with a cheaper column copy (#1338) (#1539) (`3cd5f8a4`)
- GB: Sprite.Draw with 16-bit coordinates never wraps onto the screen (#1189) (#1537) (`ee6970d4`)
- Tests: CompileCostBudgetTests reports the CLI's exit code when a compile fails (#1533) (#1535) (`097d2b1a`)
- GB: Sprite.DrawWorld draws in worlds whose origin in the combined map is not 0 (#1527) (#1532) (`260dc40c`)
- Samples: platformer-showcase combines vines, springs, spikes, coins and doors between worlds with their music (#1526) (#1529) (`f2bdd258`)
- Collectibles and World.SetTile with row streaming on GB and SMS/GG (#1503) (#1525) (`188f4a0a`)
- NES: World.Collect and World.SetTile on packed and streamed worlds (#1504) (#1519) (`3db76aad`)
- World: change background tiles at run time with World.SetTile (#1502) (#1516) (`53671ce8`)
- Camera.Follow: show the new world with the camera already on the player (#1507) (#1513) (`693cbc72`)
- World: World.Activate from anywhere under Camera.Follow (#1497) (#1508) (`445c1fa2`)
- World: collectibles on the map with World.Collect (#1444) (#1506) (`e760f498`)
- NES: switch between wide scrolling worlds (#1446) (#1505) (`ef40e618`)
- Kinematics: climb, spring and spike tile pieces (#1443) (#1500) (`eef08229`)
- Tiled: game-defined triggers and asset selectors (#1445) (#1499) (`5dcd19af`)
- Tiled: game-defined tile kinds with touch handlers (#1442) (#1496) (`a23eb07d`)
- Workflow: agents use the RetroSharp language server and know its limits (#1493) (#1494) (`b39aaac1`)
- World activation: empty the sprite table and its shadow at the commit on every target (#1492) (`35db65e3`)
- SDK: remove the V1 actor pool (#1438) (#1490) (`c0a5d8cf`)
- Compiler: locals whose lifetimes do not overlap share RAM (#1433) (#1489) (`939ba75c`)
- Move the remaining V1 fixtures and the runner benchmark to ActorSlots (#1472) (#1487) (`a0053b3d`)
- ActorSlots: empty union slots and small pools with the zero form (#1463) (#1471) (`2ae91171`)
- runner: move the enemies from V1 to ActorSlots (#1376) (#1470) (`7180d726`)
- ActorSlots: spawn loops stop at what they look for (#1458) (#1468) (`344331c0`)
- Language: nested aggregates inside union cases (#1449) (#1467) (`4f337ac6`)
- Samples: move the enemy samples from V1 to ActorSlots (#1440) (#1464) (`c80a2eb5`)
- Workflow: remove the daily suite, which never ran (#1462) (`9340f663`)
- Tests: suite.py without its own scheduler and without orphaned processes (#1460) (`4759255e`)
- Templates: move the platformer template from V1 to ActorSlots (#1439) (#1457) (`3332bf8e`)
- Samples: move the projectile samples from V1 to ActorSlots (#1441) (#1455) (`12201907`)
- Language: an internal zero form so ActorSlots can clear library, union and array fields (#1448) (#1453) (`a501acca`)
- Sprites: report draws that would show only the last pass of a loop (#1316) (#1452) (`c1c1be86`)
- Kinematics: V1 enemy behaviors as source pieces, with a sample (#1315) (#1451) (`a6075a1d`)
- ActorSlots: rotate the pool's draw order so a full line drops a different actor each frame (#1437) (`091d5391`)
- ActorSlots: report RAM per pool and slot on every target, and name the pool when it does not fit (#1375) (#1436) (`4908767b`)
- SDK 2D: reset ActorSlots Tiled spawns when a world activates (#1435) (`682a57eb`)
- ActorSlots: camera-window query, Retire, and an off-screen skip in Draw (#1314) (#1431) (`4e9a5606`)
- SDK 2D: declare a new operation in one neutral place plus each target's emission (#1429) (#1430) (`50c01b83`)
- SDK: split the requirement-lifetime analyzer by responsibility (#1401) (#1428) (`cde00233`)
- Game Boy: drop the fixed demo tiles from every ROM (#1398) (#1426) (`04af2e74`)
- SDK: split SdkPluginDescriptor.cs by contract family (#1400) (#1425) (`d3775bf9`)
- NES/GB: split the video programs and count calls with SyntaxWalker (#1399) (#1424) (`a9dbd90b`)
- SDK: delete dead code and name the owners of the legacy paths (#1422) (`21c9acd9`)
- Tests: shared cases for the twin NES and Game Boy tests (#1395) (#1419) (`d94bd267`)
- Targets: static setup steps through a visitor every target must cover (#1396) (#1418) (`2a8fe5e6`)
- NES: split NesRomBuilder and select the cartridge by a typed capacity limit (#1394) (#1417) (`8f3f81e6`)
- Tests: check by result or by symbol instead of fixed bytes and addresses (#1392) (#1416) (`4a70d459`)
- Targets: one sprite budget, metasprite geometry and cell slicing (#1393) (#1415) (`d2dc2b50`)
- Targets: neutral planning of box queries and Camera.Init (#1391) (#1414) (`86ee367f`)
- SDK 2D: one classifier for the collector's streams and frame budgets (#1390) (#1413) (`cb8b9f46`)
- Agent workflow: split AgentWorkflow.md and pass context to subagents (#1389) (#1412) (`7f96ef9d`)
- SDK 2D: one catalog of operations and categories in the neutral model (#1388) (#1411) (`b0f4b25b`)
- SDK 2D: World.Aabb names the activated world past its limit (#1387) (#1410) (`510bef79`)
- Tests: suite.py adapts to the machine's memory and cores (#1385) (#1409) (`c6fbf888`)
- Tests: NES.Tests without Game Boy or Cli dependencies (#1386) (#1408) (`e2ee453b`)
- ADR 0014: remove V1 actors once ActorSlots covers them, with no compatibility layer (#1407) (`bdf48448`)
- SDK 2D: spawn ActorSlots actors from Tiled object layers (#1313) (#1406) (`a97f48ec`)
- Codegen: keep a runtime-indexed element's address across more of a looped body (#1374) (#1405) (`590fccbf`)
- Tests: filtered runs share the machine test lock (#1385) (#1403) (`05a7d491`)
- Sprites: write each pass of a looped draw straight into its band (#1310) (#1384) (`8f4b7d95`)
- Agent workflow: /roadmap opens a second lane when work is free and the machine has room (#1382) (#1383) (`14f9ef65`)
- SDK 2D: accept expressions in every operand read in place, as Sprite.Draw does (#1358) (#1379) (`a9bc4541`)
- Agent workflow: /roadmap skill that works through a roadmap's sub-issues (#1377) (#1378) (`1949737d`)
- Language: nested aggregates in array elements (#1365) (`eb0c7999`)
- GB: prepare an edge per cell where the packed plane would read past it (#1065) (#1362) (`54a03b39`)
- Language: copy a whole struct element of an array field field by field (#1318) (#1359) (`0b7f80d8`)
- Sdk2D: store expression draw arguments in a local and place operand errors at the argument (#1317) (#1356) (`421d1027`)
- Agent workflow: priority labels and parked issues decide the order of work (#1355) (`a8fae4ba`)
- Compiler: type parameter names are local to their template (#1347) (#1351) (`b0c40e36`)
- Docs: real game tests go on the frozen copies; live-game tests stay general (#1350) (`5f08954b`)
- Portable2D: contact queries between ActorSlots actors (#1312) (#1349) (`2726dc38`)
- Docs: AGENTS.md names every target; drop a roadmap reference from a workflow rule (#1348) (`c114b20d`)
- Tools: issue.py claim indexes the new worktree with CodeGraph (#1344) (#1346) (`2da7f10e`)
- Compiler: a trivial pure accessor costs what reading its field costs (#1343) (#1345) (`0cd1a9a6`)
- GB: rows of maps wider than 255 columns fit one VBlank (#1339) (#1342) (`9efecf7c`)
- Master System, Game Gear: pool draw-and-contact shows and touches actors at the left edge (#1178) (#1341) (`8cfd7474`)
- GB: commit camera edges only when they fit the vertical blank (#1329) (#1340) (`19dc713c`)
- NES: stream the entering column unless the nametable ring holds it (#1334) (#1337) (`2043e8f6`)
- Tests: frozen runner and runner-lift copies check what a player sees on every target (#1301) (#1335) (`a8dda62b`)
- Benchmarks: check unsafe video writes on Master System and Game Gear (#1331, #1332) (#1333) (`06e63105`)
- Benchmarks: frozen copies of the reference games guard CPU and VBlank (#1326) (#1330) (`67f9146a`)
- Tools: measure CPU and VBlank margin per game and per label (#1300) (#1328) (`7ccce754`)
- Tests: check codegen by its result, not by exact opcode sequences (#1299) (#1325) (`53206150`)
- Tests: cross ROM banks with a test-only program capacity, not filler programs (#1324) (`dbac395c`)
- Agent workflow: project subagents, issue script and /issue skill (#1323) (`91a18ae9`)
- Tests: focus and the ROM diff compare against origin/master (#1321) (`fb8ed839`)
- Tests: target and assembly lists come from a registry, not literals (#1297) (#1320) (`0deb01ea`)
- Actors: game-owned actors in ActorSlots, sprite bands for draws in loops (#1262) (#1319) (`3fd815d8`)
- Tests: fail fast on runaway memory and exploding compiles (#1296) (#1309) (`37dbdf5d`)
- Docs: the agent workflow's rules live in the repository (#1295) (#1308) (`2bb920a6`)
- Tests: the whole suite runs daily on master and reports failures as an issue (#1305) (#1307) (`8292dfea`)
- Tests: one script for every run, focus as the merge gate, and a hook that requires it (#1294) (#1306) (`62f8fdd5`)
- GB: give every call of a drawing helper sprites of its own (#1293) (#1304) (`f97c973c`)
- Master System: test a diagonal camera with three streamed draws (#1292) (`1d08b63e`)
- Master System: snow-bros-jr steps on every frame (#1162) (`c0ff9e5a`)
- Z80: leave out loads of what A already holds (#1285) (`b113a7b4`)
- Master System: copy and hide the frame's sprites in unrolled passes (#1165) (`8dc8394b`)
- GB: keep the collision plane in a 32 KB ROM (#1287) (`cb02ac23`)
- Z80: keep the element address where paths meet (#1284) (`14f2d1cb`)
- Master System: find a collision query's tiles once (#1282) (`5449b189`)
- Master System: draw sprites on screen without clipping each piece (#1281) (`ef042cb5`)
- Portable2D: report World.Activate in a plain Main with follow (#1280) (`66f5a27f`)
- Language: size a field's class wherever it is declared (#1279) (`5477152b`)
- Codegen: close most of the CPU cost of actor arrays against unrolled fields (#1278) (`060ef966`)
- Runner Lift: keep enemies in an array (#1271) (`0296b5c0`)
- Codegen: compute an array element's address once per method call (#1276) (`19c38615`)
- Codegen: rely on bool being 0 or 1 as a value (#1275) (`724c5ea6`)
- Z80: reuse an array element's address between accesses (#1273) (`6e6bbcfc`)
- Build report: price runtime multiply, divide and shift (#1274) (`d0117309`)
- CLI: capacity report shows locals and bank slack on Game Boy and NES (#1270) (`c0dcf0a6`)
- Language: reject constant zero divisors and negative shift counts in the frontend (#1269) (`8111f47f`)
- Runtime-indexed fields take the same right sides as fields, and every 16-bit compound (#1266) (`ee1a8831`)
- Language: report an unqualified static call in a pure helper as unresolved (#1265) (`3fec8ac1`)
- Language: call the element type's method on an array element (#1264) (`5c81f8a5`)
- Portable2D: named arguments and constants on camera calls (#1263) (`c7882103`)
- Runner Lift: simpler, more readable game source (#1257) (`ad106a2e`)
- CLI: place build errors at file, line and column and print each once (#1260) (`58c1fc99`)
- VS Code extension: install and update the CLI, and create games from templates (#1254) (`d8f2743b`)
- VS Code extension: F5 builds the game and runs it in an emulator (#1253) (`7deea95d`)
- VS Code extension: build tasks for every project and target (#1251) (`bab1d1a7`)
- Kinematics: DiscreteMotion falls on the apex cadence after a ceiling hit (#1246) (`17a5627c`)
- SDK: document every public declaration of Portable2D and Kinematics (#1245) (`d91f45b0`)
- VS Code extension: make publish.sh executable (#1242) (`e9c4a387`)