RetroSharp.Tool 0.0.1240

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global RetroSharp.Tool --version 0.0.1240
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local RetroSharp.Tool --version 0.0.1240
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=RetroSharp.Tool&version=0.0.1240
                    
nuke :add-package RetroSharp.Tool --version 0.0.1240
                    

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

With only the CLI installed, run retrosharp guide: the getting-started guide, the language and SDK references and runnable examples are inside the tool.

  • 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, scenes, 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, scenes, 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, Tiled Scene.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 range for, 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.json APU 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 using Music.Asset(...), Music.Play(...), and Audio.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), Scene.Map scenes with Camera.* 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 portable Input.* on the two-button pad; every other SDK operation fails with a capability diagnostic. See docs/MasterSystemTarget.md.

  • --target gg: in development. Builds the Master System target for the Game Gear and writes a .gg ROM: the program sees the 160x144 LCD window, colours are 12-bit, and Start is a held button. The camera scrolls inside the window. See docs/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 types
  • offsetof(type, field) folded at compile time for direct fields of plain struct types
  • countof(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-fallthrough switch with multi-value and half-open range cases, while, do while, for, half-open range for, break, continue)
  • Half-open range membership expressions such as tile in 1..4, lowered like tile >= 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)expr as 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 let locals with compile-time width inference: i16/u16 fields, 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 Scene.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 scene'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 scene 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

# Changelog 0.0.1240

## Changes since v0.0.1231

- Templates teach the user and their agent: four-direction minimal with a scene, guide pointers, warning-free builds (#2337) (#2365) (`ad2222a6`)
- Game Boy: the screen backdrop takes the scene's backgroundcolor (#2360) (#2363) (`3daa955b`)
- A program without a scene shows the same white backdrop on every target (#2346) (#2361) (`28f286b5`)
- SDK: World is now Scene for developers (#2352) (#2359) (`e692a58a`)
- retrosharp guide debug: the agent knows it needs the debugging MCP servers and how to install them (#2356) (#2358) (`d8894003`)
- templates create accepts an existing empty destination, including . (#2345) (#2357) (`63d14f0e`)
- Target diagnostics name the call as written, its line and target, and what to write instead (#2344) (#2355) (`3a93431d`)
- retrosharp guide: the game-maker docs and art-free examples from the installed CLI (#2351) (`34e75162`)
- Static methods of a class can have target variants (#2343) (#2349) (`011f06f2`)