# GB# - complete documentation > This file concatenates the entire GB# user manual for LLM consumption: every documentation page in navigation order, the generated diagnostics reference, and the framework API reference. --- # GB# A statically compiled, hardware-aware C# development environment for the Game Boy and Game Boy Color. GB# is **not** ".NET for Game Boy". There is no CLR, no JIT, and no garbage collector on the target. You write a constrained subset of C#; GB# analyses it with Roslyn, lowers it to a small intermediate representation, emits conservative C, and hands that to **GBDK-2020** to produce a `.gb` or `.gbc` ROM. ``` C# → Roslyn → GB# validation → GB# IR → C → GBDK-2020 / SDCC → ROM ``` The bet is that modern language ergonomics do not require a modern runtime. ```csharp using GB; using static GB.Hardware; public static class Program { public static void Main() { Display.Enable(); byte x = 80; while (true) { if (Input.Right) x++; Sprites[0].X = x; Game.WaitVBlank(); } } } ``` That compiles to C where the whole `Sprites[0].X` chain erases to a single OAM store and the arithmetic stays 8-bit. GB# abstracts boilerplate, not hardware. ## Start here - **[Installation](getting-started/installation.md)**: the .NET SDK, `dotnet tool install --global gbsharp`, and `gbsharp doctor --fix` for everything else. - **[Your first game](getting-started/first-game.md)**: `gbsharp new` to a running ROM in minutes. - **[Tutorials](tutorials/move-a-sprite.md)**: worked walkthroughs of the sample games, from one moving sprite to a banked 64 KB cartridge. ## Find your way - **[Guides](guides/language-subset.md)**: the C# subset, assets, banking, memory, diagnostics, profiling, publishing. - **[CLI reference](reference/cli.md)**: every `gbsharp` command and option. - **[gbsharp.json reference](reference/gbsharp-json.md)**: every project file key. - **[Diagnostics reference](reference/diagnostics/index.md)**: all `GBSxxxx` ids, generated from the compiler's own definitions. - **[Framework API](api/index.md)**: the `GB` namespace your game code compiles against, generated from the source XML docs. ## For language models This site publishes an [llms.txt](https://skytech6.github.io/GBSharp/llms.txt) index and an [llms-full.txt](https://skytech6.github.io/GBSharp/llms-full.txt) containing the complete manual in one file, for use by LLM-based tools working with GB#. ## Design and internals The user documentation lives here. The design rationale (why a compiled subset instead of a runtime, why the IR looks the way it does, and what GB# refuses to become) lives in [GBSharp_Thesis_and_Architecture.md](https://github.com/SkyTech6/GBSharp/blob/main/GBSharp_Thesis_and_Architecture.md) in the repository, alongside the [roadmap](https://github.com/SkyTech6/GBSharp/blob/main/ROADMAP.md). --- # Installation GB# has one requirement you install yourself: the [.NET 10 SDK](https://dotnet.microsoft.com/download). Everything else (the GBDK-2020 C toolchain and the emulator runtime) is fetched for you. ## Install the tool GB# ships as a global `dotnet` tool on nuget.org: ```bash dotnet tool install --global gbsharp ``` Then acquire the toolchain and check the install: ```bash gbsharp doctor --fix ``` `doctor --fix` downloads the pinned GBDK-2020 and the emulator runtime into a per-user cache, checksum-verified against the lock files that ship inside the tool, and then reports what it found. That is the whole install: no checkout, no PowerShell, no submodule. Skip to [your first game](first-game.md). Upgrading later is `dotnet tool update --global gbsharp`, and the full command set is in the [CLI reference](../reference/cli.md). Three more packages go up with the tool, and you never install them by hand: `GBSharp.Sdk`, `GBSharp.Framework` and `GBSharp.Analyzers` are what the design-time `.csproj` that `gbsharp new` scaffolds restores, so your editor binds and analyses the code. `gbsharp build` uses none of them; the compiler carries its own copy of the framework inside the tool. See [GB# in the editor](../guides/ide-analyzers.md). ## Working from a checkout The rest of this page is for building GB# itself, or for running the sample games the [tutorials](../tutorials/move-a-sprite.md) read. Inside a checkout the toolchain is fetched into the tree rather than the per-user cache, by two scripts in `tools/`, and the vendored copies win over the cache so the compiler you built is the one that runs. If you have not installed the global tool, every command in these docs runs as `dotnet run --project GBSharp.CLI -- ` from the repository root instead; `gbsharp doctor` means `dotnet run --project GBSharp.CLI -- doctor`. ## Fetch the toolchain GB# emits C and hands it to GBDK-2020 to produce the ROM. Fetch the pinned toolchain: ```bash pwsh tools/get-gbdk.ps1 ``` The script reads `tools/gbdk.lock.json`, downloads the GBDK-2020 4.5.0 archive for your OS and architecture, verifies its SHA256 against the lock file, and extracts it to `tools/gbdk`, which is gitignored. The pin is the point: every machine that runs the script gets the same bytes, and an archive that changes upstream (or gets tampered with in transit) fails the hash check loudly instead of quietly building against something different. The script also checks that every tool GB# shells out to (`lcc`, `bankpack`, `romusage`) actually exists in the extracted archive. A tool missing from one platform's archive would otherwise be discovered as a link failure on that platform alone, which is the most expensive place to find it. Re-running the script is a no-op when the pinned version is already installed and intact; pass `-Force` to re-download and re-extract. It runs under both Windows PowerShell 5.1 and PowerShell 7+ (`pwsh`) on Linux and macOS, so CI uses the same script on every platform, and so can you. ## Fetch the emulator runtime The emulator runtime is what `gbsharp run` launches by default, what `gbsharp profile` measures with, and what the tests use to run the ROMs they build. Fetch it the same way: ```bash pwsh tools/get-emulator.ps1 ``` This script is deliberately the same shape as `get-gbdk.ps1`: same host detection, same SHA256 verification against a lock file (`tools/emulator.lock.json`), same version stamp, same install into a gitignored directory (`tools/emulator`). There is one acquisition story to learn rather than two. The archive ships two flavours of the runtime library (a regular one and an instrumented one that `gbsharp profile` needs) and the C header they both implement. The runtime is built from [gbsharp-emulator](https://github.com/SkyTech6/gbsharp-emulator), a fork of [binjgb](https://github.com/binji/binjgb) that adds a stable C ABI and drops SDL from the core. That repository is a submodule here at `extern/gbsharp-emulator`, but **only for building the emulator itself**: the fetch script is how everyone else gets it, and you never need to clone the submodule. ## Build and test Build everything: ```bash dotnet build GBSharp.slnx ``` Run the tests: ```bash dotnet test GBSharp.slnx ``` The test suite includes ROMs that are built and then run on the emulator. Tests that need the emulator runtime skip themselves when it is absent, so a bare checkout (no fetch scripts run at all) still runs green. The skips are telling you what was not exercised, not that something is broken. ## Troubleshooting: gbsharp doctor If a build fails in a way that smells like a missing tool rather than a wrong program, ask the toolchain to describe itself: ```bash gbsharp doctor ``` Doctor reports the GB# version, the .NET runtime, whether the framework assembly is where the compiler expects it, the GBDK root, version and compiler driver, and which emulator `gbsharp run` would launch. When everything is in place it ends with `Ready to build.` and exits zero. When GBDK cannot be found, doctor lists the locations it searched and exits nonzero. Run `gbsharp doctor --fix` to fetch the pinned toolchain into the per-user cache (or `pwsh tools/get-gbdk.ps1` to vendor it into a checkout), or set `GBDK_HOME` to point at an existing GBDK-2020 installation. A `--gbdk-path` option on `doctor` (and on every command that builds) overrides `GBDK_HOME`, the vendored copy and the cache, for checking an install without committing to it. With the toolchain in place, the next step is [your first game](first-game.md). --- # Your first game This page goes from an empty directory to a ROM running in an emulator. It assumes the `gbsharp` tool and its toolchain from [installation](installation.md): `dotnet tool install --global gbsharp` then `gbsharp doctor --fix`. Working from a checkout instead, read every `gbsharp ` below as `dotnet run --project GBSharp.CLI -- `. ## Create a project ```bash gbsharp new MyGame --template sprite ``` `new` takes a template with `--template` (or `-t`), a machine with `--target` (`gb` for the original Game Boy, the default, or `gbc` for Game Boy Color), and a directory with `--out` (defaulting to one named after the project). It refuses a directory that already has files in it unless you pass `--force`: the one thing this command must never do is overwrite work. There are three templates: - **empty** (the default) is a `Main` that enables the display and runs the canonical GB# frame loop: `while (true)` with `Game.WaitVBlank()` at the bottom, and a comment marking where everything else goes. - **sprite** is a sprite you move with the d-pad. Its tile data is written by hand as bytes in a `static readonly` array, which puts it in the cartridge rather than work RAM; the build report shows what it cost. - **background** is a full-screen image loaded through the asset pipeline and scrolled. This is the only template that needs art, and rather than shipping a checked-in binary nobody can review, the CLI synthesises a placeholder PNG (`Assets/tiles.png`) when it writes the project. The sprite template's `Program.cs`, in full: ```csharp using GB; using static GB.Hardware; public static class Program { // Two tiles of 2bpp data: 16 bytes each, in the cartridge because it // is 'static readonly'. The build report shows what it cost. private static readonly byte[] Shape = { 0x3C, 0x3C, 0x42, 0x7E, 0x81, 0xFF, 0xA5, 0xFF, 0x81, 0xFF, 0xBD, 0xFF, 0x42, 0x7E, 0x3C, 0x3C, }; public static void Main() { Tiles.LoadSprite(0, 1, Shape); Display.Enable(); Display.ShowSprites(); byte x = 80; byte y = 72; Sprites[0].Tile = 0; while (true) { if (Input.Right) x++; if (Input.Left) x--; if (Input.Down) y++; if (Input.Up) y--; Sprites.Move(0, x, y); Game.WaitVBlank(); } } } ``` That is a complete game: it boots, draws a sprite, and responds to input. What the C# subset does and does not include is covered in [the language subset](../guides/language-subset.md). ## What new scaffolds Every template writes the same frame around `Program.cs`: - `gbsharp.json` is the project file, holding just the name and target to start with. See [project layout](project-layout.md) for what else can go in it. - `MyGame.csproj` is for your **editor**, not the build. It references the GB# framework and analyzers through `GBSharp.Sdk`, so you get completion, navigation and GB# diagnostics as you type. Building it is an error by design; `gbsharp build` makes the ROM. - `.gitignore` is one line, ignoring `build/`. - `.vscode/tasks.json` has tasks for `gbsharp: build` (wired as the default build task, so Ctrl+Shift+B builds the ROM), `run`, `analyze` and `clean`. They shell out to the same `gbsharp` pipeline documented here, with the path to the CLI you ran `new` from baked in, so the editor and the terminal reach the exact same compiler. - `.vscode/launch.json` makes F5 build and launch the emulator, with `Build only` and `Analyze (lint)` configurations beside it. This is not a real debug session: there is no GBZ80 debugger wired into VS Code, so the configurations run the CLI in a terminal. Source-level debugging happens in the emulator itself, from the `.sym` file written beside the ROM. ## Build it ```bash gbsharp build MyGame ``` The build prints its stages (parsing, GB# validation, lowering, C generation, GBDK compilation, linking) and ends with the build report: ``` GB# Build Report ──────────────────────────────── Target Game Boy Color ROM 32.0 KB WRAM used 63 B / 4.0 KB Static objects (declared) 38 B ROM Banks Bank 0 2.5 KB / 16.0 KB ███░░░░░░░░░░░░░░░░░ Cycle estimates Frame budget 70,224 cycles @ 59.7 Hz Frame loop 130 cycles ░░░░░░░░░░░░░░░░░░░░ 0% Call stack Deepest path 3 calls Program.Main() -> Program.Setup() -> FixedList.Add Work RAM free 3.9 KB for stack and locals ``` Three things worth knowing on first read. **WRAM used** and **static objects declared** are different numbers on purpose: the first is what the linker actually placed, and the difference is the stack, shadow OAM and GBDK's own state. Reporting one number would be a useful-sounding lie. The **cycle estimates** are computed statically from the IR, so read them as ceilings for comparing changes, not as measurements; the frame budget is printed exactly because it is the only figure that is a fact. And the **call stack** depth is exact: GB# rejects delegates and has no function pointers, so the call graph is the complete account of what can reach what. ## Run it ```bash gbsharp run MyGame ``` `run` builds and then launches the ROM in the bundled GB# Player, unless the project's `"emulator"` setting or `--emulator` says otherwise. `--emulator` takes `player` for the bundled Player, the id of a known debugging emulator, or a path to any executable. The emulators in the catalog load the `.sym` file GB# writes beside the ROM, so naming one is a first-class choice for source-level debugging rather than a workaround. A missing emulator has never failed a build and still does not: the ROM is the deliverable and running it is a convenience, so `run` warns, tells you where the ROM is, and exits successfully. ## The loop From here the loop is edit, then `gbsharp run` again. Two commands are worth knowing alongside it: ```bash gbsharp analyze MyGame ``` checks the project without building a ROM (it needs no C toolchain at all, which is what makes it a fast CI lint job) and ```bash gbsharp build MyGame --emit-c ``` keeps the generated C next to the ROM so you can see exactly what your C# became. ## Where everything lands The ROM is written to `MyGame/build/MyGame.gb` (or `.gbc` for a Game Boy Color target), with the linker's map and symbol files beside it and, with `--emit-c`, the generated C under `build/c/`. The full inventory of the build directory is in [project layout](project-layout.md). When the template stops being interesting, [move a sprite](../tutorials/move-a-sprite.md) builds a game up from the empty template, and [publishing](../guides/publishing.md) turns the result into something people without an emulator can run. --- # Project layout A GB# project is a directory of C# files with an optional `gbsharp.json` beside them. This page walks through what `gbsharp new` writes and what a build adds. A freshly scaffolded background-template project, after one build, looks like this: ``` MyGame/ gbsharp.json the project file MyGame.csproj for the editor; the build never reads it Program.cs the game Assets/ tiles.png art, found by [Asset] declarations .vscode/ tasks.json Ctrl+Shift+B -> gbsharp build launch.json F5 -> gbsharp run .gitignore ignores build/ build/ everything a build produces ``` ## gbsharp.json The project file is deliberately minimal, and deliberately optional: with no `gbsharp.json` at all, everything is inferred from the directory: the ROM is named after the folder and the target is the original Game Boy. A new project starts with just: ```json { "name": "MyGame", "target": "gb" } ``` The keys, at a glance: - `name` is the ROM name and the cartridge title. Defaults to the directory name. - `target` is `"gb"` or `"gbc"`. Anything else is an error rather than a silent default, because a typo that quietly built for the wrong machine would only be discovered when the palettes are missing on hardware. - `emulator` is a path to an emulator executable for `gbsharp run` to launch instead of the bundled Player. - `exclude` lists directories, relative to the project, to leave out of compilation. - `assets` lists extra directories to search for `[Asset]` images. - `mbc`, `romBanks`, `ramBanks` describe the cartridge: which mapper, and how many ROM and save-RAM banks. Only consulted when something is banked; left unset, a banked project gets MBC5 with battery-backed RAM, a bank of save RAM to sit behind that battery, and a ROM sized to fit. - `libraries`, `includes` are external object files to link into the ROM and C headers to include in the generated C, for reaching code the framework does not wrap. - `player` describes how a published game presents itself: window title, scale, volume and the rest. - `diagnostics` lists severity overrides, by id or by whole category. Every relative path in the file resolves against the project directory, and every value with a fixed set of legal values is validated up front: a misspelled mapper or an out-of-range bank count is an error against `gbsharp.json` before anything compiles. The full key-by-key reference is at [gbsharp.json](../reference/gbsharp-json.md). ## Source files, and what exclude controls A build compiles every `.cs` file under the project directory, recursively, in a deterministic order. There is no file list to maintain. Three directory names are always skipped: `bin`, `obj` and `build`, so the editor's output and GB#'s own output never get compiled back in. `"exclude"` adds your own names to that list; a path is skipped when any of its segments matches an entry, case-insensitively. The `.csproj` exists so an editor can bind and analyse the code; `gbsharp build` never reads it and always enumerates sources itself. That is why the two can drift (MSBuild's default `**/*.cs` glob knows nothing about `"exclude"`), and why drift is reported as a warning rather than an error: a wrong `.csproj` compile set cannot produce a wrong ROM. ## Assets, and what assets controls When a declaration says `[Asset("tiles.png")]`, the image is looked for first in the directory of the file that declared it, then in `Assets/`, then in the project root, then in each directory listed under `"assets"`. The `Assets` folder is a convention rather than a requirement, and the project root is the fallback so a small game needs no folder at all. `"assets"` is for art that lives somewhere else (a directory shared between projects, say) without copying it in. How the pipeline turns a PNG into tiles, maps and palettes is covered in [assets](../guides/assets.md). ## The build directory Everything a build produces lands in `build/` (or wherever `--out` points), which is why the scaffolded `.gitignore` is one line. After a full-featured build it contains: - `MyGame.gb` or `MyGame.gbc` is the ROM. Which extension you get follows the target. - `MyGame.map`, `MyGame.sym`, `MyGame.noi` are the linker's map and symbol files, written beside the ROM on every build. The `.sym` is what debugging emulators pick up for source-level debugging, and together with `MyGame.functions.json`, it is the symbol chain `gbsharp profile` resolves measured cycles through. - `c/` is the generated C, kept when you pass `--emit-c`. With `--annotate-source`, every generated statement carries a comment naming the C# line that produced it, and `c/sourcemap.json` holds the same mapping as data; one code path produces both, so they cannot disagree. - `MyGame.gbir` is the GB# intermediate representation, written with `--emit-ir`. - `report.json` is the build report as JSON, written with `--report-json`. It carries the same numbers the terminal report shows, unrounded, plus the GB# and GBDK versions that produced them, which is what a CI script should read. `gbsharp clean` deletes the directory. Published games are separate output: they land under `publish/` and are covered in [publishing](../guides/publishing.md). --- # Move a sprite You will build the smallest complete GB# program (display on, one hardware sprite, joypad input, a frame loop) by reading `Samples/MoveSprite` in the repo, which is the thesis MVP verbatim. The samples are in the [GB# repository](https://github.com/SkyTech6/GBSharp), so clone it once to follow along and to run them; the installed `gbsharp` tool builds them from the clone with no further setup. Every tutorial here works the same way. This is the program the whole compiler architecture was aimed at. Every line of it exercises something real: turning the LCD on, reading the joypad, writing to OAM through a typed indexer, and pacing the loop against the hardware's own frame rate. ## The whole program ```csharp using GB; using static GB.Hardware; public static class Program { public static void Main() { Display.Enable(); byte x = 80; while (true) { if (Input.Right) x++; Sprites[0].X = x; Game.WaitVBlank(); } } } ``` That is the entire sample. Walk it top to bottom. ## The two usings ```csharp using GB; using static GB.Hardware; ``` `GB` is the framework namespace: `Display`, `Input`, `Game` and everything else live there. The second line is what makes `Sprites[0]` legal: C# has no static indexers, so `Sprites` has to be a value rather than a type for indexing to bind, and `Hardware.Sprites` is that value. It costs nothing, since the handle types erase entirely during lowering, leaving only the sprite index in the generated C. ## Turning the screen on ```csharp Display.Enable(); ``` The Game Boy's LCD controller starts wherever the boot ROM left it. `Display.Enable()` sets the bit that turns the LCD on; it compiles to a single register write. There is no window to create and no surface to acquire, because the hardware has exactly one screen and it is always the same 160x144 pixels. ## Position as a byte ```csharp byte x = 80; ``` The screen is 160 pixels wide, so a `byte` holds any position on it with room to spare. GB# keeps this arithmetic 8-bit all the way down: the SM83 is an 8-bit CPU, and every promotion to 16 bits is work it has to do one half at a time. Choosing `byte` here is not a style preference; it is the difference between one instruction and several. ## The frame loop ```csharp while (true) { if (Input.Right) x++; Sprites[0].X = x; Game.WaitVBlank(); } ``` `while (true)` is the canonical GB# game loop. A Game Boy game never returns from `Main`; it runs until the power switch says otherwise. `Input.Right` reads the joypad register directly at the point of use and answers as a `bool`. Each property read is one register read, so a loop that tests many buttons can sample them all at once with `Input.Read()` instead; this loop tests one, so the property is the right tool. `Sprites[0].X = x` is the line the thesis set as the target. It looks like an indexer into a collection followed by a property assignment, and in most C# that chain would allocate a bounds check, a temporary, and two calls. Here the whole chain erases to a single OAM store: ```c gbs_sprite_set_x(0U, x); ``` Sprite 0 is the first of the 40 hardware sprites in OAM (object attribute memory), the small table the video hardware walks every scanline to decide what to draw. Writing a sprite's X is writing one byte of that table. Note the hardware's convention: screen X plus 8, so a sprite at X = 0 is fully off the left edge, and the starting value of 80 puts it a little left of centre. `Game.WaitVBlank()` blocks until the next vertical blank, the gap between one frame being drawn and the next starting, which is the only time OAM and VRAM are safe to touch. It is also what paces the loop: the hardware refreshes at 59.7 Hz, so one pass through this loop is one frame, and holding Right moves the sprite exactly one pixel per frame. Without the wait, this would be a spin loop running the CPU flat out and racing the video hardware for memory it is not allowed to win. ## Run it ``` gbsharp run Samples/MoveSprite ``` The GB# Player opens straight into the ROM. This sample is deliberately minimal: it loads no tile artwork and never sets the sprite's Y, because its job is to prove the input-to-OAM chain, not to draw a character. Hold Right and sprite 0's X register climbs one pixel per frame; the repo's own integration tests run this exact loop and read the movement back out of OAM. For a sprite you can watch walk around, the metasprite tutorial is the next step. Build it with `--emit-c` to see the generated C, and read the build report at the end of every build: this program's frame loop costs a rounding error against the 70,224 cycles a frame gives you. ## Where to go next - [Backgrounds and tilemaps](backgrounds-and-tilemaps.md): put artwork behind the sprite. - [Metasprites](metasprites.md): a character made of several sprites, animated. - [The language subset](../guides/language-subset.md): what GB# accepts and why. --- # Backgrounds and tilemaps You will put artwork on the background layer three ways: a PNG on Game Boy Color, the same pipeline on an original Game Boy, and tile data written by hand, by reading `Samples/Background`, `Samples/BackgroundDmg` and `Samples/Tilemap` in the repo. The background is what a Game Boy game is mostly made of. The hardware keeps a 32x32 grid of tile indices, of which 20x18 is on screen at a time; tile pixel data is loaded once into VRAM, and the map then names tiles by index, so a screen of artwork costs one byte per cell rather than one byte per pixel. Everything in this tutorial is a way of filling that grid. ## A PNG on Game Boy Color `Samples/Background` targets the Game Boy Color; its `gbsharp.json` is one line: ```json { "name": "Background", "target": "gbc" } ``` The artwork enters the program as a named field: ```csharp [Asset("forest.png")] private static TileMap Forest; ``` Nothing here converts anything at runtime. While the project builds, `forest.png` is decoded, checked against the hardware's limits, reduced to 2bpp tiles, deduplicated, turned into a map, and (on Game Boy Color) split into palettes with a matching attribute map. The field is a name for that data in ROM; there is no conversion at runtime and no tool to run first. Anything wrong with the image is a compile error against your C#, pointing at this declaration. Getting it on screen is one call, bracketed by the display: ```csharp Display.Disable(); // Tiles, map, colour palettes and the attribute map, in one call. The // sizes come from the image, so there is nothing here to keep in sync. Background.Load(Forest); Palettes.SetBackgroundShades(Shade.White, Shade.LightGray, Shade.DarkGray, Shade.Black); Display.Enable(); Display.ShowBackground(); ``` The `Disable`/`Enable` pair is a hardware rule, not a convention: VRAM is only safely writable with the LCD off, or during VBlank. A load this size will not fit in one VBlank, so the sample turns the screen off, copies everything, and turns it back on. `Background.Load(Forest)` is one C# argument and eight C ones: tiles, map, attributes, palettes, and the counts, all filled in by the compiler from the image itself, plus the ROM bank the data lives in. The colour parts are skipped at runtime on an original Game Boy, which is what lets the next sample reuse the same call. The loop scrolls with the d-pad: ```csharp while (true) { if (Input.Right) { scroll++; } if (Input.Left) { scroll--; } Background.Move(scroll, 0); Game.WaitVBlank(); } ``` `Background.Move` writes the scroll registers to an absolute position. The map is 32 tiles wide and the screen shows 20, so scrolling past the edge wraps around to the other side of the same map: the hardware wraps, there is no larger world behind it. `scroll` is a `byte` and the horizontal scroll register is a byte, so the overflow arithmetic and the hardware agree by construction. ## The same image on an original Game Boy `Samples/BackgroundDmg` runs the same pipeline against `"target": "gb"`. The program is nearly identical; the differences are all in what the converter produces. From the sample's own header: ```csharp // cave.png is drawn in four greys, which is all a DMG can show. The converter // orders them lightest first to match the hardware's default palette, so the // image is right before SetBackgroundShades is ever called, and rearranging // that call is how you invert the picture without touching the artwork. // // No attribute map and no colour tables are generated for this target, so the // same image costs less ROM here than it would on Game Boy Color. ``` On DMG the background has one palette of four shades, set as a register: ```csharp Palettes.SetBackgroundShades(Shade.White, Shade.LightGray, Shade.DarkGray, Shade.Black); ``` Reorder those four arguments and the whole picture remaps instantly: that register-level remapping is how original hardware did fades and flashes. On Game Boy Color the call is ignored and the image's own palettes, generated at build time, apply instead. ## Tiles written by hand `Samples/Tilemap` builds its background from data in the source, which is worth reading once even if you never do it again: it is what the asset pipeline is generating for you. ```csharp // Four 8x8 tiles, 2 bits per pixel, 16 bytes each. Each row is two bytes: // the low bit of all eight pixels, then the high bit. private static readonly byte[] TileData = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: empty 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 1: solid 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00, // 2: dither 0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00, 0xFF, 0x00, 0x81, 0x00, 0x81, 0x00, 0x81, 0x00, // 3: box 0x81, 0x00, 0x81, 0x00, 0x81, 0x00, 0xFF, 0x00, }; ``` That layout (two bytes per row, low bitplane then high) is the hardware's native 2bpp format, exactly what a PNG becomes at build time. The `static readonly` matters as much as the bytes: it is what places the array in ROM. Drop the `readonly` and the build report moves it into the 8 KB of work RAM and charges you for it. Build with `--emit-c` and the array is there as a `const uint8_t` table. The map names those tiles by index: ```csharp // 10x9 of the 32x32 map. The rest stays tile 0. private static readonly byte[] Map = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 2, 2, 2, 2, 0, 0, 3, 3, 0, 1, 1, 0, 0, 1, 1, 0, 3, 3, 2, 1, 0, 0, 0, 0, 1, 2, 3, 3, 2, 0, 0, 1, 1, 0, 0, 2, 3, 3, 2, 1, 0, 0, 0, 0, 1, 2, 3, 3, 0, 1, 1, 0, 0, 1, 1, 0, 3, 3, 0, 0, 2, 2, 2, 2, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, }; ``` Loading is the two halves of what `Background.Load` did in one: ```csharp Background.LoadTiles(0, 4, TileData); Background.LoadMap(0, 0, 10, 9, Map); ``` `LoadTiles` copies pixel data into VRAM, 16 bytes a tile; `LoadMap` writes a rectangle of indices into the 32x32 grid. The sample only fills a 10x9 rectangle, and the rest of the grid stays tile 0, the empty tile, which is why it deliberately defines one. The sample targets `gbc` but runs on both machines, and handles colour the honest way: by asking: ```csharp // Set the DMG shades either way: on colour hardware they are ignored, // and on an original Game Boy they are all there is. Palettes.SetBackgroundShades(Shade.White, Shade.LightGray, Shade.DarkGray, Shade.Black); if (Palettes.IsColorHardware) { Palettes.LoadBackgroundColors(0, 1, Colors); } ``` `Colors` is one Game Boy Color palette, four 15-bit colours as `ushort`s in ROM: ```csharp private static readonly ushort[] Colors = { 0x7FFF, 0x35AD, 0x1A73, 0x0000 }; ``` The rest of the sample scrolls on both axes with `Background.Move(scrollX, scrollY)` and plays a tone on the edge of the A button (press, not hold), which is worth reading as the standard pattern for turning a level into an event. ## Maps larger than one screen The hardware map is 32x32 cells; a world larger than that does not fit in it. None of these three samples needs more, but the primitive for when you do is `Background.DrawRegion`, which copies a window of a larger converted map into the hardware map; you track the camera position yourself and call it when the position moves. GB# does not keep a camera for you: that would be per-frame state you cannot see the cost of. The [assets guide](../guides/assets.md) covers large maps in full. ## Run it ``` gbsharp run Samples/Background ``` A forest scene fills the screen in colour; hold Left or Right and it scrolls, wrapping at the map's edge. Then try the other two: ``` gbsharp run Samples/BackgroundDmg gbsharp run Samples/Tilemap ``` `BackgroundDmg` is the same program in four greys. `Tilemap` shows the hand-built pattern in its 10x9 rectangle, scrolls on both axes, and beeps when you press A. Build any of them with `--emit-c` to see the data tables, and read the build report to see what each image cost: the DMG image is measurably cheaper, because no attribute map and no colour tables were generated for it. ## Where to go next - [Drawing text](drawing-text.md): a font is background tiles too. - [The asset pipeline](../guides/assets.md): every attribute, limit and diagnostic. - [Memory and budgets](../guides/memory-and-budgets.md): where `static readonly` goes, and how to hold the line. --- # Drawing text You will draw a static label and a counter that ticks once a second, from one font sheet, by reading `Samples/Text` in the repo. Text on a Game Boy is not a console. There is no cursor, no scrolling, and no `printf`: a string is a row of tiles like any other background artwork. One call uploads the glyphs once, and another writes them into the map wherever the game wants them. GBDK's own `font.h` and `console.h` exist for the opposite choice (a cursor and scrolling state the game does not control), which is exactly the kind of hardware-hiding layer GB# does not add. GB# abstracts boilerplate, not hardware. ## A font is an asset ```csharp [Font("font.png", Characters = "0123456789HI")] private static FontAsset Digits; ``` `font.png` is one row of 8x8 glyphs, one tile per character in `Characters`, left to right, so this image is exactly twelve tiles wide and one tile tall. At build time the sheet becomes background tiles and a character-to-tile lookup; like every asset, the field is a name for data in ROM, and anything wrong with the image is a compile error pointing at this declaration. `Characters` declares which character each glyph is, in sheet order. This font carries only what the sample draws: ten digits and the two letters of its greeting. A font pays for exactly the glyphs it declares, in ROM and in VRAM, which is why there is no default character set to fall back to. ## Strings are byte arrays ```csharp // "HI", as the character codes gbs_font_draw indexes the glyph table with. private static readonly byte[] Greeting = { 72, 73 }; private static byte[] counter = { 48, 48 }; // "00" ``` GB#'s subset has no `string` (GBS0043): a string is heap-allocated, immutable, UTF-16 data, none of which exists on this machine. So text is bytes: 72 and 73 are 'H' and 'I', 48 is '0'. Each byte is a character code that `Text.Draw` looks up in the font's glyph table. Sugar that turns a string literal into an array like this at compile time is real, separate work (a language-subset change with its own review), and is deliberately not part of this. The two declarations differ in one word, and that word is where the data lives. `Greeting` is `static readonly`, so it is placed in ROM and can never change. `counter` is mutable, so it lives in the 8 KB of work RAM, which it must, because the program rewrites it every second. The build report itemises both. ## Load once, draw where you like ```csharp Display.Disable(); Text.Load(Digits, 0); Display.Enable(); Display.ShowBackground(); Text.Draw(Digits, 0, 1, 1, 2, Greeting); Text.Draw(Digits, 0, 1, 3, 2, counter); ``` `Text.Load` uploads the font's glyph tiles into the background tile region, starting at tile 0, done with the LCD off, because VRAM is only safely writable then or during VBlank. It happens once; the glyphs are just tiles from here on, shared with everything else the background draws. `Text.Draw(font, firstTile, x, y, length, text)` writes `length` bytes of `text` as tiles, left to right from the map cell (x, y). It is the same map write `Background.SetTile` makes, just several in a row. The coordinates are tile coordinates on the 32x32 background map, and nothing advances on its own: drawing "HI" at (1, 1) leaves the hardware exactly as it was except for two map cells. That is the whole reason there is no cursor: a cursor is state, state costs WRAM and cycles, and this way the only text state that exists is what your game chose to keep. ## The counter ```csharp while (true) { Game.WaitVBlank(); frames++; if (frames == 60) { frames = 0; value++; if (value == 100) { value = 0; } counter[0] = (byte)(48 + (value / 10)); counter[1] = (byte)(48 + (value % 10)); Text.Draw(Digits, 0, 1, 3, 2, counter); } } ``` `Game.WaitVBlank` returns once per frame at 59.7 Hz, so sixty of them is close enough to a second. The counter is formatted by hand (tens digit, ones digit, each offset from 48 ('0')) and redrawn only on the seconds it changes. The other fifty-nine frames, the loop draws nothing: the two map cells still hold their tiles, and the hardware keeps showing them. Text you have not touched costs nothing per frame, which is the property a console-style text layer would have taken away. ## Run it ``` gbsharp run Samples/Text ``` You should see "HI" near the top-left, a two-digit counter below it counting up once a second, and it rolling over from 99 to 00. Build with `--emit-c` to see `Greeting` as a `const` table in ROM and `counter` as two bytes of WRAM. ## Where to go next - [Text in full](../guides/text.md): the window layer, fonts and their limits. - [Backgrounds and tilemaps](backgrounds-and-tilemaps.md): the layer text is drawn on. - [API reference](../api/index.md): `Text`, `FontAttribute` and `FontAsset`. --- # Metasprites You will draw a character bigger than one hardware sprite and animate it, by reading `Samples/Metasprite` in the repo. A hardware sprite is 8x8 pixels, and there are 40 of them. Anything that reads as a character is several of them moved together: a metasprite. GB#'s `Metasprites` class is a thin wrapper over GBDK's own `move_metasprite_ex` family: which hardware sprites and which tiles a frame uses are arguments on every call, never implicit state kept for you. ## The sheet ```csharp [Metasprite("hero.png", FrameWidth = 2, FrameHeight = 2)] private static MetaspriteAsset Hero; ``` `hero.png` is a grid of frames, each `FrameWidth` by `FrameHeight` tiles: here 2x2 tiles, 16x16 pixels. The sheet is 32x16, so it holds two frames, read left to right. At build time each frame becomes a list of sub-sprite placements plus the deduplicated tiles they use. The interesting part is what a frame does not contain. From the sample's own header: ```csharp // A 32x16 sheet: two 2x2-tile frames, each missing one sub-sprite - blank, // palette index 0, the colour real hardware never draws for a sprite. That // sub-sprite costs no OAM entry and no frame-table byte; --emit-c and read // Program_Hero_frames to see it: three metasprite_t records per frame, not // four, each ended by GBDK's own terminator. ``` Palette index 0 is transparent for sprites on real hardware, so a tile that is entirely index 0 can never be seen. The converter drops it: no ROM for the placement record, no hardware sprite spent on it, no OAM write at runtime. A 2x2 frame with a blank corner is three sub-sprites, not four. This is why frames of the same metasprite can use different numbers of hardware sprites, and that fact drives the one piece of bookkeeping this sample has to do. ## Load, then move ```csharp Display.Enable(); Display.ShowSprites(); Metasprites.Load(Hero); ``` `Display.ShowSprites()` flips the LCD controller bit that makes the sprite layer visible at all: the background and sprites are independently switchable layers. `Metasprites.Load` uploads the sheet's tiles and, on Game Boy Color, its palettes. Once, before the loop; the per-frame work is only OAM writes. ```csharp byte used = Metasprites.Move(Hero, frame, 0, 0, x, 80); ``` `Move(sheet, frame, baseTile, baseSprite, x, y)` positions one frame at an absolute screen position by writing OAM entries for each of its sub-sprites, starting at hardware sprite `baseSprite` and drawing tiles relative to `baseTile`. It returns how many hardware sprites the frame used (three for these frames), and that return value is not decoration. ## Hiding what the last frame drew ```csharp // Frames can use different numbers of sub-sprites; hide whatever // the last frame drew that this one did not reuse. if (used < usedLastFrame) { Metasprites.HideRange(used, usedLastFrame); } usedLastFrame = used; ``` `Move` writes as many OAM entries as this frame needs and does not touch the rest. If the previous frame used four sprites and this one uses three, the fourth entry still holds whatever the previous frame put there, and it stays on screen: a stale limb hanging in the air. `HideRange(from, to)` moves hardware sprites `from` up to (not including) `to` off screen, so hiding from this frame's count up to the last frame's count cleans up exactly the leftovers and nothing else. The hardware forgets nothing on its own; a frame boundary is a concept the game keeps, not one OAM has. ## Animation ```csharp if (Input.Right) { x++; } if (Input.Left) { x--; } ``` ```csharp Game.WaitVBlank(); frame = frame == 0 ? (byte)1 : (byte)0; ``` The sheet's two frames put their blank corner in different places (frame 0's is bottom-right, frame 1's is top-right), so alternating between them every frame reads as a two-frame step animation. Animation on this hardware is exactly this: choosing a different frame index on the next `Move` call. There is no animation system underneath, because a frame index and a toggle is the whole mechanism, and anything wrapped around it would be state you could not see the cost of. ## Run it ``` gbsharp run Samples/Metasprite ``` You should see a 16x16 character mid-screen, stepping in place, and it walks left and right with the d-pad. Build with `--emit-c` and read `Program_Hero_frames` to see the per-frame placement records: three per frame, each list ended by GBDK's own terminator. ## Where to go next - [Many objects](many-objects.md): eight of these, updated data-oriented. - [The asset pipeline](../guides/assets.md): sheets, deduplication and limits. - [Profiling and cost](../guides/profiling-and-cost.md): what a frame of OAM writes costs. --- # Many objects You will run eight enemies from fixed storage with no allocation and no object graph, by reading `Samples/Enemies` in the repo, the sample that exercises the language core: structs, enums, fixed collections, arrays, `ref` parameters, `for`, `switch` and 8-bit arithmetic. The style matters as much as the API. This sample is written the way GB# expects games to be written: plain structs held in explicitly bounded storage, with systems that operate over them. The memory layout is visible in the source. ## Bounded storage ```csharp [Capacity(8)] private static FixedList enemies; ``` `FixedList` is the answer GB# gives when you reach for `List` (GBS0042): storage reserved up front, with a live count. It cannot grow, which is the point: the memory it occupies is decided when it is declared, not while the game is running, because there is no allocator to grow into. The thesis writes this as `FixedList`, but C# has no value type parameters, so the capacity travels as an attribute instead. What matters is preserved: the capacity sits at the declaration, in the source, where you can see exactly what it costs. The capacity also does work the syntax does not advertise. `enemies.Count` is a runtime field, but a `FixedList` refuses to grow past its capacity, so 8 is a ceiling the compiler can prove, and that is what lets the build put a total cost on the update loop below rather than shrugging at it. ## Plain data ```csharp public enum EnemyKind : byte { Walker = 0, Flyer = 1, Turret = 2, } public struct Enemy { public byte X; public byte Y; public byte Sprite; public EnemyKind Kind; } ``` An `Enemy` is four bytes, and you can see all four. No base class, no virtual dispatch, no reference to anything: a struct in GB# is a layout, and eight of them in the list is 32 bytes of work RAM plus the count, which the build report will state exactly. The enum is `: byte` for the same reason every position is: this is an 8-bit machine, and the natural word size should be the default, not an optimisation. ## Systems, not methods ```csharp public static class EnemySystem { public static void Update(ref Enemy enemy, byte frame) { switch (enemy.Kind) { case EnemyKind.Walker: enemy.X++; break; case EnemyKind.Flyer: enemy.X++; // A shift, not a divide: the cost should be obvious from the source. enemy.Y = (byte)(64 + ((frame >> 3) & 7)); break; case EnemyKind.Turret: break; } if (enemy.X > 160) { enemy.X = 0; } } } ``` `Update` is a static function taking `ref Enemy`: in the generated C, a function taking a pointer to a four-byte struct. Passing by `ref` means no copy in and no copy out; the function works on the enemy where it lives, inside the list's storage. Behaviour switches on `Kind`, a byte compare, rather than on a vtable that does not exist. This is the house style rather than instance-heavy OO, and the reason is visibility. An object graph hides its layout, its lifetime and its indirections; this hides nothing: every byte is in a declaration, every call is a plain function, and the cost model can price all of it. Instance members exist in GB# for when a type genuinely owns its behaviour, not as the default shape of a program. ## Driving it ```csharp public static void Main() { Display.Enable(); Display.ShowSprites(); Setup(); while (true) { UpdateEnemies(); Draw(); frame++; Game.WaitVBlank(); } } ``` Setup fills the list once: ```csharp for (byte i = 0; i < 4; i++) { Enemy enemy = new Enemy(); enemy.X = spawnTable[i]; enemy.Y = 64; enemy.Kind = i < 2 ? EnemyKind.Walker : EnemyKind.Flyer; enemy.Sprite = i; enemies.Add(enemy); } ``` `Add` copies the struct into the list's storage and returns `false` when the list is full, since there is nowhere to grow into, so the caller decides what full means. Note each enemy remembers which hardware sprite is its own; nothing maps objects to sprites for you. Update and draw are each one loop over the same storage: ```csharp private static void UpdateEnemies() { for (byte i = 0; i < enemies.Count; i++) { EnemySystem.Update(ref enemies[i], frame); } } private static void Draw() { for (byte i = 0; i < enemies.Count; i++) { Sprites.Move(enemies[i].Sprite, enemies[i].X, enemies[i].Y); } } ``` The list's indexer returns by reference, so `ref enemies[i]` hands `Update` a pointer straight into the array, with no copies anywhere in the frame. `Sprites.Move` sets a sprite's X and Y in one call, one OAM write cheaper than assigning the two properties separately. And because the capacity bounds `Count`, the build can tell you what the whole loop costs: this shape of loop is exactly what the GBS0410 estimate prices, up to 8 iterations at a stated cost each. Updating and drawing are separate loops on purpose. Update touches game state; Draw touches OAM. Keeping the OAM writes together, next to the `WaitVBlank`, is the habit that scales: when a game grows enough that VRAM timing matters, the code that must land in VBlank is already in one place. ## Run it ``` gbsharp run Samples/Enemies ``` Four enemies drift rightward and wrap at the screen edge; two of them, the flyers, bob vertically on a period set by the frame counter. As with `MoveSprite`, the sample loads no artwork, so watch the motion rather than the pixels; the build report is the other half of the output. Read it: the WRAM line itemises the list's 33 bytes, and the cycle estimates price `UpdateEnemies` and `Draw` against the 70,224-cycle frame. ## Where to go next - [Structs in GB#](../guides/structs.md): constructors, properties and what they compile to. - [The language subset](../guides/language-subset.md): why `List` is refused and what stands in for it. - [Banking a big game](banking-a-big-game.md): when the game outgrows 32 KB. --- # Banking a big game You will build a cartridge larger than 32 KB and control where every piece of it goes, by reading `Samples/Banking` in the repo: a working 64 KB MBC5 cartridge using both `[Bank]` forms, explicit and automatic, plus banked assets. The Game Boy addresses 32 KB of cartridge: 16 KB permanently mapped at the bottom (bank 0), and 16 KB at a time in a switchable window above it. A game without banking stops at 32 KB. A memory bank controller (the MBC chip on the cartridge) swaps which 16 KB bank sits in that window, and banking is the discipline of deciding what lives where and paying for the switches. ## The cartridge is configuration `Samples/Banking`'s `gbsharp.json`: ```json { "name": "Banking", "target": "gbc", "mbc": "mbc5+ram+battery", "romBanks": 4 } ``` `"mbc"` names the controller chip and its extras: MBC5 with cartridge RAM and a battery to keep it alive when the power is off. `"romBanks": 4` is four 16 KB banks: a 64 KB ROM. Left unset, a banked project gets MBC5 with battery-backed RAM and a ROM sized to fit; this sample pins both so the numbers in this tutorial stay put. ## What stays in bank 0 From the sample's own header: ```csharp // Bank 0 is always mapped and is where everything starts. It holds the frame // loop and anything that runs every frame, because reaching a banked function // costs a bank switch each way. Everything else is worth moving out: bank 0 is // the one space no larger cartridge can give you more of. ``` `Program` carries no `[Bank]` attribute, so it stays in bank 0: ```csharp public static class Program { // Not banked. Read every frame, so it stays where it is already mapped. private static byte scroll; public static void Main() { Display.Enable(); // A banked call. GB# reports the cost of this line at build time. ForestLevel.Load(); while (true) { scroll++; Background.Scroll(1, 0); Game.WaitVBlank(); } } } ``` The frame loop runs sixty times a second and calls nothing banked, which is the shape to aim for. The one banked call happens once, at setup, and the build prices it anyway (GBS0301): a banked call goes through a trampoline that saves the current bank, switches, calls, and switches back, roughly thirty cycles more than a local call. Thirty cycles once at startup is nothing; thirty cycles inside the frame loop is a tax on every frame, and a separate diagnostic (GBS0440) exists precisely to catch banked calls reached from the frame loop. ## An explicit bank ```csharp [Bank(2)] public static class ForestLevel { [Asset("forest.png")] private static TileMap Art; public static void Load() => Background.Load(Art); } ``` One attribute moves the whole class out of the permanent half: the code, the tiles, the map, the attributes and the palettes all go to bank 2 together. The generated C says `#pragma bank 2` on its first line, and the build report lists what landed there. The asset follows its declaring class, and `Background.Load` is handed the bank alongside the pointers, so it maps the data in before reading and restores the previous bank afterwards; writing that switch-read-restore sequence yourself is what `[Bank]` is instead of. It also restores the bank for a reason a single call site cannot see: without that, loading banked art would silently change which bank the caller returns into. Keeping data with the code that loads it is the pattern to copy. Data outside bank 0 can only be read while its bank is mapped, so reading `ForestLevel.Art` from a class in another bank is a compile error (GBS0303) rather than a silently wrong load. ## An automatic bank ```csharp [Bank] public static class Credits { private static readonly byte[] Text = { 0x47, 0x42, 0x23, 0x20, 0x53, 0x41, 0x4D, 0x50, 0x4C, 0x45, }; public static byte Initial() => Text[0]; } ``` Written without a number, `[Bank]` says "not bank 0, anywhere else" and lets the linker choose. The build then tells you what it chose: ``` Program.cs(7,24): info GBS0309: 'Credits.Initial()' was placed automatically in bank 1. GB# left this to GBDK's bankpack rather than choosing itself. Write [Bank(n)] on the declaration, with the bank named above, to pin it there instead. ``` That is the intended workflow: let placement float while the game is growing, then pin banks once the layout settles, so a later change cannot silently reshuffle what a save file or a level table depends on. ## Reading the placement report Every build ends with the layout. For a cartridge shaped like this one it reads: ``` Cartridge MBC 0x1B, 4 banks ROM Banks Bank 0 754 B / 16.0 KB ░░░░░░░░░░░░░░░░░░░░ Bank 1 14 B / 16.0 KB ░░░░░░░░░░░░░░░░░░░░ Bank 2 918 B / 16.0 KB █░░░░░░░░░░░░░░░░░░░ (888 B declared) Placement Bank 2 ForestLevel.Load(), ForestLevel_Art_map, ForestLevel_Art_tiles, … Bank (chosen) Credits.Initial(), Credits_Text ``` `MBC 0x1B` is the cartridge-header byte for MBC5 with RAM and battery, the `"mbc"` string, as the hardware spells it. Each bank shows what the linker actually placed against its 16 KB, with the declared figure beside it where they differ: the difference is the code in that bank, and reporting one number would be a useful-sounding lie. The Placement section is the answer to "where did it go": explicit placements listed under their bank, automatic ones under `Bank (chosen)` with the bank the linker picked, which is the number to copy into `[Bank(n)]` when you pin. One failure mode deserves its own mention: bank 0 filling up is a note, but bank 0 *overflowing* is a GB# error (GBS0310), because the linker does not treat it as one: spilled code lands where the switchable bank appears, the ROM builds, and then dies when it reaches the part that moved. GB# reads the real linker map and refuses instead. ## Run it ``` gbsharp run Samples/Banking ``` You should see the forest artwork (loaded out of bank 2 by a banked call) scrolling steadily leftward one pixel per frame. The screen is the least of it: run `gbsharp build Samples/Banking` and read the report. Find the GBS0309 line naming the bank `Credits` was given, find the GBS0301 line pricing the `ForestLevel.Load()` call, and match the Placement section against the three classes in the source. ## Where to go next - [Banking in full](../guides/banking.md): trampolines, diagnostics and layout advice from the call graph. - [Memory and budgets](../guides/memory-and-budgets.md): budgets that fail the build when a bank fills. - [The CLI](../reference/cli.md): `build`, `--report-json` and reading reports in CI. --- # The language subset GB# compiles a constrained subset of C#. The constraint is not a limitation waiting to be lifted: it is the design. Every construct in the subset lowers predictably to C an 8-bit CPU can run, with no runtime underneath it: no CLR, no JIT, no garbage collector. Anything that would need one is refused at compile time, in GB#'s own words, with an alternative. This page is the map of that boundary: what is inside, what is outside and why, and the two places where GB#'s syntax deviates from the thesis that motivated it. ## What is supported **Types.** `byte`, `sbyte`, `ushort`, `short`, `bool`, enums, structs, and fixed-size arrays. These are the types the SM83 can work with honestly: 8-bit values are native, 16-bit values cost more and the build says so, and anything wider is refused ([GBS0002](../reference/diagnostics/language.md#gbs0002-unsupported-type)). `int` arithmetic that survives into the output is reported as a performance cost ([GBS0007](../reference/diagnostics/language.md#gbs0007-32-bit-arithmetic)): consider `ushort` if values cannot exceed 65,535, or `byte` if they cannot exceed 255. **Control flow.** `if`, `while`, `do`, `for`, `switch`, and the full operator set. `while (true)` is the canonical GB# game loop. **Structure.** `ref` parameters, static classes, struct methods, properties, and constructors. A struct is a layout and its methods are functions that take a pointer to one: see [Structs](structs.md) for what that model buys and what it asks of you. **Data.** `static readonly` arrays are placed in ROM rather than work RAM: see [Data in ROM](rom-data.md). Array lengths must be compile-time constants ([GBS0052](../reference/diagnostics/language.md#gbs0052-array-size-must-be-constant)), because GB# reserves the storage at compile time. `Samples/Enemies` exercises the whole core in one small program that covers structs, enums, fixed collections, arrays, `ref` parameters, `for`, `switch` and 8-bit arithmetic: ```csharp public static class EnemySystem { public static void Update(ref Enemy enemy, byte frame) { switch (enemy.Kind) { case EnemyKind.Walker: enemy.X++; break; case EnemyKind.Flyer: enemy.X++; // A shift, not a divide: the cost should be obvious from the source. enemy.Y = (byte)(64 + ((frame >> 3) & 7)); break; case EnemyKind.Turret: break; } if (enemy.X > 160) { enemy.X = 0; } } } ``` ## What is refused, and why Everything on this list needs machinery the Game Boy does not have: a heap, an allocator, an object header, a dispatch table, a scheduler, unwinding. GB# refuses each one by name, pointing at your C#, with the alternative in the message: ``` Program.cs(12,29): error GBS0042: List requires dynamic allocation. public static List Items = new List(); ^^^^^^^^^^ Use FixedList or FixedArray instead. Capacity stays visible in the source, and the storage is reserved at compile time. ``` | Refused | Id | Why | |---|---|---| | `List` and other dynamic collections | [GBS0042](../reference/diagnostics/language.md#gbs0042-dynamic-collection) | Requires dynamic allocation. Use `FixedList` or `FixedArray`, below. | | `System.String` | [GBS0043](../reference/diagnostics/language.md#gbs0043-systemstring-is-unavailable) | Requires heap allocation. Use a fixed byte array, and the tile-based text APIs to draw it: see [Drawing text](text.md). | | Exceptions | [GBS0044](../reference/diagnostics/language.md#gbs0044-exceptions-are-unavailable) | There is no unwinding machinery on the target. Return a status value instead. | | Delegates and events | [GBS0045](../reference/diagnostics/language.md#gbs0045-delegates-and-events-are-unavailable) | Require runtime dispatch. Call the target directly, or switch on an enum to choose between behaviours. | | Interfaces | [GBS0046](../reference/diagnostics/language.md#gbs0046-interfaces-are-unavailable) | Require virtual dispatch. Use a struct with an enum tag and a switch, which lowers to a jump you can see. | | `async` / `await` | [GBS0047](../reference/diagnostics/language.md#gbs0047-asyncawait-is-unavailable) | There is no scheduler on the target. Drive work from the frame loop instead. | | Boxing | [GBS0048](../reference/diagnostics/language.md#gbs0048-boxing) | Boxing puts a value on a heap GB# does not have. Keep the value in its own type. | | LINQ | [GBS0049](../reference/diagnostics/language.md#gbs0049-linq-is-unavailable) | Write the loop. On an 8-bit CPU the loop is what you want to be able to read anyway. | | Reference type allocation | [GBS0050](../reference/diagnostics/language.md#gbs0050-reference-type-allocation) | `new` on a class needs a heap. Declare it as a struct, or make the type static if it holds no per-instance state. | The refusal of delegates has a payoff beyond simplicity: with no function pointers, the call graph is the complete account of what can reach what, which is what makes GB#'s call-depth report exact and its bank-layout advice possible. One construct is legal but warned about rather than refused: recursion ([GBS0058](../reference/diagnostics/language.md#gbs0058-recursive-call)). SM83 has no stack limit check. The stack starts at the top of work RAM and grows down through the same 8 KB the static fields grow up through, so a recursion that goes one level too deep overwrites them: the failure looks like a variable changing value on its own rather than like a crash. Rewriting the recursion as a loop over a `FixedList` is the usual fix, and a program with recursion in it gets no call-depth report, since the depth is whatever the data makes it. ## Fixed collections `FixedArray` and `FixedList` are the subset's answer to `List`: storage reserved at compile time, at a capacity written in the source. `FixedArray` is a fixed-length array; `FixedList` adds a live `Count`, an `Add` that returns `false` when the list is full (there is nowhere to grow into, so the caller decides what that means), a swap-remove `RemoveAt`, and `Clear`. ```csharp [Capacity(8)] private static FixedList enemies; ``` The capacity is compile-time for two reasons. The first is memory: the storage is reserved when the game builds, so the declaration is the complete statement of what the collection costs, and a missing capacity is an error rather than a default ([GBS0054](../reference/diagnostics/language.md#gbs0054-capacity-required); a capacity outside 1–255 is [GBS0055](../reference/diagnostics/language.md#gbs0055-invalid-capacity)). The second is analysis: a `FixedList` refuses to grow past its capacity, so the capacity is a ceiling the compiler can prove, which is what lets the cycle estimator put a total on the most ordinary loop in a GB# program, `for (byte i = 0; i < enemies.Count; i++)`, even though `Count` is a runtime field ([GBS0410](../reference/diagnostics/cycle-cost.md#gbs0410-loop-cost)). Each distinct element type and capacity specialises into its own emitted C struct, so there is no runtime generic machinery and no indirection. The [Many objects](../tutorials/many-objects.md) tutorial builds a game around one. ## Two deviations from the thesis Both are places where the thesis's illustrative syntax is not valid C#. GB# keeps the substance and stays real C#: it will not invent a dialect that only looks like C#. **`Sprites[0].X`** needs `using static GB.Hardware;`. C# has no static indexers, so `Sprites` has to be a value rather than a type for indexing to bind. It costs nothing: the handle types erase entirely, and the whole chain compiles to a single OAM store. **`FixedList`** is written `[Capacity(8)] static FixedList enemies;`. C# has no value type parameters. The capacity still sits at the declaration, in the source, where you can see what it costs. ## Where the boundary is enforced The subset is checked twice with one definition. The Roslyn analyzers report these diagnostics in the editor, before any build, and the compiler reports them again when you build; both read `GBSharp.Rules`, so an id means the same thing in both places. `gbsharp analyze` runs the same checks with no C toolchain at all, which makes it the CI lint job: see the [CLI reference](../reference/cli.md). --- # Structs A struct is a layout, and its methods are functions that take a pointer to one. That is the whole model: no object header, no vtable, no hidden copy, and a C output you can read next to the C# that produced it. ## The model ```csharp public struct Player { public byte X; public byte Y; public Player(byte x, byte y) { X = x; Y = y; } public byte Middle => (byte)(X + 4); public void Update() { if (Input.Left) X--; if (Input.Right) X++; } } ``` `--emit-c` shows what each member becomes: one C function per method, each taking `self` as a plain pointer: ```c void Player__ctor(Player* self, uint8_t x, uint8_t y) { self->X = x; self->Y = y; } uint8_t Player_get_Middle(Player* self) { return (self->X + 4U); } void Player_Update(Player* self) { if (gbs_input_left()) { self->X--; } if (gbs_input_right()) { self->X++; } } ``` Using the struct reads like ordinary C#, and the C underneath stays one-to-one: ```csharp Player p = new Player(80, 72); p.Update(); Sprites.Move(0, p.Middle, p.Y); ``` ```c Player__ctor((&p), 80U, 72U); Player_Update((&p)); gbs_sprite_move(0U, Player_get_Middle((&p)), p.Y); ``` Nothing is inlined and no temporary appears, so a constructor costs one visible call. A property getter is a call too (`Middle` above), which is worth knowing before putting one inside a loop that runs every frame. ## Why `new` must be assigned to a variable A GB# constructor writes through a pointer to storage that already exists. That is what `Player__ctor((&p), …)` is: a call against the variable it fills. Constructing straight into an argument or a return value would need a temporary GB# invented (stack you cannot see in the build report), so it is refused by name: ``` Program.cs(14,18): error GBS0059: A 'Player' constructor cannot be used here. A GB# constructor writes through a pointer to storage that already exists, so it needs a variable to fill. Assign it to one first, like 'Point p = new Point(3, 4);', and pass that. ``` Assign first, then pass the variable ([GBS0059](../reference/diagnostics/language.md#gbs0059-constructor-needs-somewhere-to-construct)). ## The one-constructor rule A struct carries at most one constructor. Two would mangle to the same C name, and the usual fix, a generated suffix, is a name you cannot find again in a linker map. GB#'s output is meant to be traceable from C# to C to the map file and back, and invented names break that chain. If a type genuinely has two ways to be initialised, write a static factory-style method with a name of your own choosing; the name then survives into the C and the map. ## Passing structs around Small structs pass by value cheaply. A large one passed by value is copied through the stack, and GB# points it out ([GBS0202](../reference/diagnostics/memory.md#gbs0202-large-struct-passed-by-value)). Pass it by `ref` to copy a 2-byte pointer instead. `ref` parameters are in the subset for exactly this reason: they are how systems operate on structs stored elsewhere without copying them. ## Data-oriented code is the house style Instance members are there when a type genuinely owns its behaviour, not as the default. The more natural GB# style is data-oriented: plain structs held in explicitly bounded storage, with static systems that operate over them. `Samples/Enemies` is written that way: a `FixedList` with `static Update(ref Enemy)` systems over it: ```csharp [Capacity(8)] private static FixedList enemies; private static void UpdateEnemies() { for (byte i = 0; i < enemies.Count; i++) { EnemySystem.Update(ref enemies[i], frame); } } ``` There is no object graph and no allocation, and the memory layout is visible in the source: eight enemies at four bytes each, plus the list's count, is 33 bytes of WRAM, and the build reports exactly that against the declaration. The [Many objects](../tutorials/many-objects.md) tutorial walks through building a game in this style, and [The language subset](language-subset.md) covers the fixed collections it rests on. --- # Assets Drop a PNG next to your code and name it: ```csharp [Asset("forest.png")] private static TileMap Forest; public static void Main() { Background.Load(Forest); } ``` While the project builds, the image is decoded, checked against the hardware's limits, reduced to 2bpp tiles, deduplicated, turned into a map, and (on Game Boy Color) split into palettes with a matching attribute map. The field is a name for that data; there is no conversion at runtime and no tool to run first. `Background.Load(Forest)` is one C# argument and eight C ones, with the sizes filled in from the image, so there is nothing to keep in sync: ```c gbs_background_load(Program_Forest_tiles, Program_Forest_map, Program_Forest_attributes, Program_Forest_palettes, 9U, 20U, 18U, 3U, 0U); ``` The last argument is the ROM bank the data lives in; `0` means it is always mapped and no bank switch is needed. Assets placed in other banks travel with their bank: see [Banking](banking.md). ## The attributes Each attribute converts a file at build time and gives the result a shape the loaders understand. Paths resolve relative to the file that declares them, then to the project's `Assets` folder, then to the project root. **`[Asset]`** is background artwork. The field is a `TileMap` (tiles plus a map) or a `TileSet` (tiles only, for code that builds its own maps). Tiles are deduplicated; on Game Boy Color, mirrored tiles can share one copy too (`DedupeFlips`, on by default there), because the attribute map carries flip bits. An original Game Boy's map has one byte per cell and no room for them, so `DedupeFlips` there is an error rather than a silently wrong image ([GBS0614](../reference/diagnostics/assets.md#gbs0614-flip-deduplication-unavailable)). The [Backgrounds and tilemaps](../tutorials/backgrounds-and-tilemaps.md) tutorial starts here. **`[Sprite]`** is a sprite sheet, sliced into 8x8 tiles row-major. Flipped duplicates always share one copy, because OAM carries flip bits. Set `TallSprites` for hardware 8x16 mode: the pairing that mode requires (each sprite's top and bottom tiles adjacent and even-aligned) is not what a row-major slice produces, so it is an explicit property rather than a guess. **`[Metasprite]`** is an animated character made of several sub-sprites. The sheet is a grid of frames, `FrameWidth` by `FrameHeight` tiles each; a frame's sub-sprites are whichever of its tiles are not entirely transparent, so a frame spends no hardware sprite, no ROM, and no OAM write on empty space. It is a different declaration from `[Sprite]`, not an option on it, because the converted data has a different shape: a per-frame list of placements, not just a tile array. See the [Metasprites](../tutorials/metasprites.md) tutorial. **`[Font]`** is one row of 8x8 glyphs plus a `Characters` string naming them in sheet order. Drawing text is covered in [Drawing text](text.md). **`[Binary]`** is a file copied into ROM unchanged, for data GB# has no opinion about: level layouts, a table another tool produced. See [Data in ROM](rom-data.md). ## What every build tells you Assets cost ROM, and the build says exactly what each one cost, every time: ``` Assets Forest forest.png 20x18 360 -> 9 tiles, 3 palettes 888 B ``` The same figure is reported as a diagnostic against the declaration ([GBS0620](../reference/diagnostics/assets.md#gbs0620-asset-rom-cost)), so the cost lives where the field does: ``` Program.cs(6,28): resource GBS0620: Program.Forest places 888 bytes in ROM: 360 tiles (9 unique), 20x18 map. ``` ## Image problems are C# compile errors Anything wrong with the image is a compile error against **your C#**, not against the image: ``` Program.cs(6,28): error GBS0601: 'player.png' contains 6 colours. A 2bpp palette holds 4. private static SpriteAsset Player; ^^^^^^ Reduce the image to 4 colours, or target Game Boy Color, where each 8x8 tile can use its own 4-colour palette out of 8. ``` The decoder is part of GB# rather than a dependency, which is what lets every rejection be a diagnostic that names the fix: too many colours ([GBS0601](../reference/diagnostics/assets.md#gbs0601-too-many-colours)), dimensions off the 8-pixel grid ([GBS0605](../reference/diagnostics/assets.md#gbs0605-dimensions-not-tile-aligned)), a file that is not where the path says ([GBS0606](../reference/diagnostics/assets.md#gbs0606-asset-not-found)), and the rest of the [asset diagnostics](../reference/diagnostics/assets.md). One consequence of the field being a name for ROM data: an asset is not a value. It can be passed to the loader that understands it (`Background.Load`, `Text.Load`) but not copied or stored ([GBS0613](../reference/diagnostics/assets.md#gbs0613-asset-used-as-a-value)). ## DMG and GBC palettes The two machines want different things from an image, and the pipeline handles both. On the **original Game Boy** (DMG), a tile is four shades. An image with at most four colours converts directly, brightest to darkest. Colours beyond that are refused ([GBS0601](../reference/diagnostics/assets.md#gbs0601-too-many-colours)); colours the hardware cannot show are converted by brightness, with a warning that names the alternative ([GBS0612](../reference/diagnostics/assets.md#gbs0612-colours-on-an-original-game-boy)). Set `"target": "gbc"` in [gbsharp.json](../reference/gbsharp-json.md) to keep them. On **Game Boy Color**, every 8x8 tile draws from one of 8 four-colour background palettes. The pipeline splits the image's colours into palettes and emits an attribute map assigning one to each tile: that is the `attributes` and `palettes` pair in the generated call above. A single tile using more than four colours cannot be split and is an error at that tile's coordinates ([GBS0602](../reference/diagnostics/assets.md#gbs0602-tile-uses-too-many-colours)); an image needing more than 8 palettes is refused with the observation that colours appearing in the same tile must live in the same palette, so moving one colour can free a whole palette ([GBS0603](../reference/diagnostics/assets.md#gbs0603-too-many-palettes)). `Samples/Background` and `Samples/BackgroundDmg` are the same PNG built for both machines. --- # Data in ROM A Game Boy has two places to keep data: the cartridge, which is large and read-only, and 8 KB of work RAM, which is neither. In GB# the keyword decides: `static readonly` means the cartridge; anything else means WRAM. The build report keeps the two apart, because they are different budgets with different failure modes. ```csharp private static readonly byte[] TileData = { 0x00, 0x18, 0x24, 0x42, /* … */ }; private static byte frame; ``` The generated C annotates every static with where it went and what it cost: ```c const uint8_t Program_TileData[16] = { 0x00, 0x18, 0x24, 0x42, 0x42, 0x24, 0x18, 0x00, /* … */ }; /* 16 bytes, ROM */ uint8_t Program_frame; /* 1 bytes, WRAM */ ``` Those annotations are the same numbers the diagnostics report at build time: [GBS0203](../reference/diagnostics/memory.md#gbs0203-rom-allocation) for ROM data, [GBS0201](../reference/diagnostics/memory.md#gbs0201-static-allocation) for WRAM. So the cost of a declaration is visible in the editor, in the build output, and in the C, and they cannot disagree. The rule of thumb falls out of the sizes: ROM is measured in banks of 16 KB and WRAM in single kilobytes shared with the stack, so data that never changes should say `readonly` and live in the cartridge. See [Memory and budgets](memory-and-budgets.md) for what the WRAM side costs. ## ROM data must be constant at build time GB# writes static data into the ROM image while the project builds, so every element has to be known then. An initializer that can only be computed at runtime is refused ([GBS0057](../reference/diagnostics/language.md#gbs0057-initializer-is-not-constant)); assign the value in `Main` instead, which moves the array to WRAM where writes are possible. ## Writing to ROM is a compile error A cartridge cannot be written to. SDCC would let the store through and the hardware would ignore it, or, on a real cartridge with a mapper, interpret it as a bank switch. GB# catches it in the frontend instead, where it can point at your C#: ``` Program.cs(9,9): error GBS0056: 'Program.TileData' is read-only data in ROM and cannot be assigned. ``` The fix depends on what you meant ([GBS0056](../reference/diagnostics/language.md#gbs0056-write-to-read-only-data)): copy the value into a mutable array or a local if it has to change while the game runs, or drop `readonly` to move the whole array into WRAM and pay for it there. ## `[Binary]` files For data GB# has no opinion about (level layouts, a table another tool produced, anything already in the form your code wants), `[Binary]` copies a file into ROM unchanged: ```csharp [Binary("level1.dat")] private static BinaryAsset Level1; byte first = Data.Read(Level1, 0); ``` Nothing is converted and nothing is validated beyond the file being there, which is exactly the service being offered. The alternative is a `static readonly byte[]` full of literals, which works and is unreadable past about twenty bytes. `Data.Length(asset)` returns how many bytes the file held, and `Data.Read(asset, index)` reads one byte by index. `Read` is not bounds-checked, since the cartridge is not readable past its end, so the bounds are yours to keep, with `Length` as the ceiling. The bytes are reported like any other ROM cost ([GBS0622](../reference/diagnostics/assets.md#gbs0622-binary-asset-rom-cost)), and a file too large to place is an error ([GBS0615](../reference/diagnostics/assets.md#gbs0615-binary-asset-too-large)). ## ROM data and banking Everything above describes data in bank 0, the 16 KB that is always mapped. `static readonly` data and `[Binary]` files can also be placed in switchable banks with `[Bank]`, at which point reading them directly becomes an error rather than a silently wrong load. [Banking](banking.md) covers the rules. Mutable statics cannot be banked at all ([GBS0306](../reference/diagnostics/banking.md#gbs0306-mutable-data-cannot-be-banked)): they live in WRAM, which is always mapped and is not banked on this hardware. --- # Banking A Game Boy maps 16 KB of cartridge permanently and 16 KB at a time, so a game without banking stops at 32 KB. The permanent half is bank 0: it holds the interrupt vectors, the GBDK runtime, and everything you do not mark otherwise. The other window shows one switchable bank at a time, chosen by writing to the cartridge's mapper. Banking is the discipline of deciding what lives where and paying for the switches, and `[Bank]` is that discipline as one attribute. ## `[Bank(n)]` on a class ```csharp [Bank(2)] public static class ForestLevel { [Asset("forest.png")] private static TileMap Art; public static void Load() => Background.Load(Art); } ``` The code, the tiles, the map, the attributes and the palettes all go to bank 2 together. `Background.Load` is handed the bank alongside the pointers, so it maps the data in before reading and puts the previous bank back afterwards: writing that yourself is what `[Bank]` is instead of. A type's attribute applies to its methods and its `static readonly` fields, and a member's own attribute beats its containing type's, so `[Bank(0)]` on one method of a banked class forces that one member to stay resident. Mutable statics cannot be banked ([GBS0306](../reference/diagnostics/banking.md#gbs0306-mutable-data-cannot-be-banked)): they live in work RAM, which is always mapped and is not banked on this hardware. And `Main` must stay in bank 0 ([GBS0300](../reference/diagnostics/banking.md#gbs0300-the-entry-point-cannot-be-banked)): execution starts there before any bank has been switched in. ## Automatic placement, then pinning Written without a number, `[Bank]` lets the linker choose and then tells you what it chose, so you can pin it once the layout settles: ``` Program.cs(7,24): info GBS0309: 'Credits.Initial()' was placed automatically in bank 1. GB# left this to GBDK's bankpack rather than choosing itself. Write [Bank(n)] on the declaration, with the bank named above, to pin it there instead. ``` The intended workflow is exactly what [GBS0309](../reference/diagnostics/banking.md#gbs0309-automatic-placement) describes: start automatic while the game is growing, read what the build chose, and write the numbers down when you want the layout to stop moving. ## What a banked call costs Reaching banked code is not free, and the build says where you are paying for it: ``` Program.cs(20,9): performance GBS0301: Calling 'ForestLevel.Load()' switches to ROM bank 2. A banked call goes through a trampoline that saves the current bank, switches, calls, and switches back, which costs roughly a hundred cycles more than a local call. ``` The caller's own bank is unmapped for the duration. [GBS0301](../reference/diagnostics/banking.md#gbs0301-banked-call) fires at every banked call site; the call-graph analysis adds two things a single site cannot know: that a call switches banks *on the path that runs sixty times a second* ([GBS0440](../reference/diagnostics/cycle-cost.md#gbs0440-banked-call-every-frame)), and that a callee's callers all sit in one other bank it could share ([GBS0441](../reference/diagnostics/cycle-cost.md#gbs0441-callee-could-share-its-callers-bank)). Neither fires on setup code, and neither will ever suggest moving banked code *into* bank 0: that would be advising you to undo the `[Bank]` you wrote, and bank 0 is the 16 KB banking exists to protect. ## Reading banked data Data outside bank 0 can only be read while its bank is mapped, so reading it directly from elsewhere is an error rather than a silently wrong load: ``` Program.cs(14,22): error GBS0303: 'ForestLevel.Art' is in ROM bank 2 and cannot be read directly. ``` Pass the data to a loader that takes its bank, such as `Background.Load`, or switch explicitly with `Banking.Switch` and take responsibility for restoring the previous bank yourself ([GBS0303](../reference/diagnostics/banking.md#gbs0303-banked-data-read-directly)). Explicit switching is only safe from resident code: a banked function that switches banks unmaps itself. ## What the build shows you The generated C says where it lands on its first line, and every build ends with the layout: ```c #pragma bank 2 #include "game.h" const uint8_t ForestLevel_Art_tiles[144] = { /* … */ }; /* 144 bytes, ROM bank 2 */ BANKREF(ForestLevel_Art_tiles) void ForestLevel_Load(void) BANKED ``` ``` Cartridge MBC 0x1B, 4 banks ROM Banks Bank 0 754 B / 16.0 KB ░░░░░░░░░░░░░░░░░░░░ Bank 1 14 B / 16.0 KB ░░░░░░░░░░░░░░░░░░░░ Bank 2 918 B / 16.0 KB █░░░░░░░░░░░░░░░░░░░ (888 B declared) Placement Bank 2 ForestLevel.Load(), ForestLevel_Art_map, ForestLevel_Art_tiles, … Bank (chosen) Credits.Initial(), Credits_Text ``` Declared and actual are shown separately for the same reason they are for WRAM: the difference is the code in that bank, and reporting one number would be a useful-sounding lie. See [Memory and budgets](memory-and-budgets.md) for the same principle applied to work RAM. ## When bank 0 fills up Bank 0 filling is a note ([GBS0307](../reference/diagnostics/banking.md#gbs0307-bank-0-nearly-full)); bank 0 *overflowing* is an error, because the linker does not treat it as one. Areas are placed in order and past `0x4000` they land where the switchable bank appears, so the ROM builds and then dies as soon as it reaches the part that moved. Neither lcc nor sdld mentions it, and the usage table above cannot show it: the spilled bytes are counted against the bank whose addresses they occupy, leaving bank 0 at a plausible-looking 97%: ``` error GBS0310: Bank 0 overflowed by 4032 bytes: '_CODE' runs from 0x0200 to 0x48D9, past the 0x4000 boundary. ``` GB# reads the linker map itself and reports [GBS0310](../reference/diagnostics/banking.md#gbs0310-bank-0-overflowed) where the toolchain stays silent. The fix is always the same: move code or data out of the resident bank with `[Bank]`. ## Configuring the cartridge Set `"mbc"`, `"romBanks"` and `"ramBanks"` in [gbsharp.json](../reference/gbsharp-json.md) to control the cartridge; left unset, a banked project gets MBC5 with battery-backed RAM, one 8 KB bank of save RAM behind that battery, and a ROM sized to fit. A budget on the bank count itself (for a smaller mapper, or a flash cart with a fixed size) is `[assembly: MaxROMBanks(n)]`, covered in [Memory and budgets](memory-and-budgets.md). `Samples/Banking` is a working 64 KB cartridge using both `[Bank]` forms (explicit and automatic) plus banked assets, and the [Banking a big game](../tutorials/banking-a-big-game.md) tutorial walks through it. --- # Memory and budgets A Game Boy has 8 KB of work RAM, and everything mutable shares it: your static fields, the call stack, the shadow OAM the sprite system copies from, and GBDK's own state. There is no allocator rationing it and no protection between the parts: the stack starts at the top and grows down through the same bytes the statics grow up through. GB# cannot make the 8 KB bigger; what it can do is tell you the truth about it at every build, and fail the build when a number you declared is exceeded. ## Declared versus placed Every static declaration has a knowable cost, and GB# reports it as it is written ([GBS0201](../reference/diagnostics/memory.md#gbs0201-static-allocation) for WRAM, [GBS0203](../reference/diagnostics/memory.md#gbs0203-rom-allocation) for ROM): ``` Program.cs(17,37): resource GBS0201: Program.enemies reserves 33 bytes of WRAM. ``` But the sum of your declarations is not what the machine uses. The real WRAM figure includes the stack, shadow OAM and GBDK's own state, none of which appear in your source, so the build report shows both numbers and never pretends one is the other: ``` WRAM used 63 B / 4.0 KB Static objects (declared) 38 B ``` Reporting only the declared figure would let a game creep past its real footprint and still look fine; reporting only the placed figure would leave you unable to see which declaration is responsible. The difference between them is itself information: it is the overhead everything else contributes. The same two-number honesty applies to ROM banks, where the gap is the code the linker put there. See [Banking](banking.md). The report also bounds the stack the only way that is exact on this toolchain: in calls, not bytes. GB# rejects delegates and has no function pointers, so the call graph is complete and the depth is a fact; a byte figure would require modelling SDCC's frame layout and would be wrong in the optimistic direction, which is the one that lets a ROM ship and then corrupt memory. ``` Call stack Deepest path 3 calls Program.Main() -> Program.Setup() -> FixedList.Add Work RAM free 3.9 KB for stack and locals ``` ## Budgets that fail the build A budget nobody enforces is a comment. GB#'s budgets are assembly attributes, and exceeding one fails the build: ```csharp [assembly: MaxWRAM(6144)] [assembly: MaxROMBanks(8)] ``` ``` Budgets WRAM 18 B / 8 B EXCEEDED error GBS0210: This game uses 18 bytes of work RAM; the declared budget is 8. ``` Three are available: - **`[assembly: MaxWRAM(bytes)]`**: the most work RAM the game may use ([GBS0210](../reference/diagnostics/memory.md#gbs0210-wram-budget-exceeded)). - **`[assembly: MaxROM(bytes)]`**: the largest ROM image the game may produce ([GBS0211](../reference/diagnostics/memory.md#gbs0211-rom-budget-exceeded)). - **`[assembly: MaxROMBanks(banks)]`**: the most 16 KB banks the cartridge may declare, counting bank 0 ([GBS0212](../reference/diagnostics/memory.md#gbs0212-rom-bank-budget-exceeded)). Useful where cartridge size is a cost rather than a limit: a smaller mapper, or a flash cart with a fixed budget. Budgets are checked against what the linker placed, not what the code declared. This is the load-bearing detail: a declared-bytes check would exclude the stack, shadow OAM and GBDK's own state, so a game could creep past its budget and still pass. Checking the linker map means the budget holds against the number the hardware will actually see, which is the whole value of declaring one: it holds while nobody is looking. When a budget fires, the fix is either honest revision (raise the number, because it was optimistic) or an actual saving: move data into ROM by making it `static readonly` ([Data in ROM](rom-data.md)), shrink a `[Capacity]`, or pack banks more tightly. ## Budgets in CI `--report-json` writes the same numbers the report prints, unrounded, plus the exact call depth, with the GB# and GBDK versions that produced them, for a CI script to check: ```bash gbsharp build --report-json ``` The budget attributes already fail the build on their own, so the simplest CI enforcement is to declare them in the source and let `gbsharp build` exit nonzero. The JSON is for the checks a build error cannot express: trend lines, a call-depth ceiling, comparing two branches. Its sections are absent rather than zeroed when there is nothing to say, and additions are nullable, so a script written against schema version 1 reads what it always did. See the [CLI reference](../reference/cli.md) for the schema. ## VRAM has a budget too Video memory is not WRAM, but it is just as fixed: background and window share one 256-tile region, and sprites have their own. GB# sums every asset's unique tiles because it can see them all, and reports the total against the region ([GBS0204](../reference/diagnostics/memory.md#gbs0204-vram-tile-budget)). A total above the region is fine when screens replace each other at runtime: GB# cannot see load order, so it reports the sum rather than failing the build. A *single* asset larger than the region is an error ([GBS0205](../reference/diagnostics/memory.md#gbs0205-vram-tile-budget-exceeded)), because no load order can make it work. --- # Drawing text Text on a Game Boy is tiles. A font is a tileset where each tile happens to be a glyph, and drawing a string is writing those tiles into the background map, the same map write everything else on the layer uses. GB# keeps that model visible instead of wrapping it: there is no cursor, no scrolling, and no `printf`. ## A font is an asset `[Font]` converts a font sheet into background tiles and a character-to-tile lookup at build time: ```csharp [Font("font.png", Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,!?")] private static FontAsset Alphabet; ``` The sheet is one row of 8x8 glyphs, one tile per character in `Characters`, left to right, so the image must be exactly `Characters.Length` tiles wide and one tile tall ([GBS0627](../reference/diagnostics/assets.md#gbs0627-font-sheet-has-the-wrong-shape)), and `Characters` is required because there is no default set to fall back to ([GBS0628](../reference/diagnostics/assets.md#gbs0628-font-characters-required)). Monospaced, single-row fonts are a deliberate v1 simplification, not a missing feature: proportional glyph widths need a per-glyph advance table and a text layout GB# does not have yet, and monospaced text is what most Game Boy games draw anyway. A font carries no colour of its own. A background cell already has whatever palette or attribute is active there, so drawing text never touches either. ## The Text API Three methods, and nothing hidden between them: - **`Text.Load(font, firstTile)`** uploads the font's glyph tiles into the background/window tile region, starting at `firstTile`. Once, typically at startup. - **`Text.Draw(font, firstTile, x, y, length, text)`** draws `length` bytes of `text` as tiles, left to right starting at `(x, y)` on the background. Each byte is a character code, looked up in the font's glyph table and written as a background tile: the same map write `Background.SetTile` makes, just several in a row. - **`Text.DrawWindow(...)`** is the same call onto the window layer instead of the background. A code the font's `Characters` did not declare draws whichever glyph is tile 0, rather than being checked at runtime, because a bounds check on every glyph of every draw would be a cost paid by correct code to catch a mistake the build could not see anyway. ## No cursor, no scrolling: by design GBDK's own `gbdk/font.h` and `console.h` exist for the opposite choice: a cursor and scrolling state the game does not control. That is exactly the kind of hardware-hiding layer GB# does not add, since GB# abstracts boilerplate, not hardware. Where text goes is a decision the game makes with coordinates, every time, and redrawing a region is writing over it, which is what `Samples/Text` does with its counter: ```csharp [Font("font.png", Characters = "0123456789HI")] private static FontAsset Digits; // "HI", as the character codes gbs_font_draw indexes the glyph table with. private static readonly byte[] Greeting = { 72, 73 }; private static byte[] counter = { 48, 48 }; // "00" public static void Main() { Display.Disable(); Text.Load(Digits, 0); Display.Enable(); Display.ShowBackground(); Text.Draw(Digits, 0, 1, 1, 2, Greeting); Text.Draw(Digits, 0, 1, 3, 2, counter); // …update counter's bytes, then Text.Draw the same region again. } ``` ## Strings are byte arrays GB#'s subset has no `System.String` ([GBS0043](../reference/diagnostics/language.md#gbs0043-systemstring-is-unavailable)): it would need heap allocation the target does not have, so there is no string literal to pass to `Draw` yet. Write the bytes directly, as `Samples/Text` does above: each byte is a character code, meaning an index into the font's `Characters`. Making them `static readonly` puts them in ROM rather than work RAM ([Data in ROM](rom-data.md)). Sugar that turns a string literal into that array at compile time is real, separate work (a language-subset change with its own review), and is deliberately not part of this. The [Drawing text](../tutorials/drawing-text.md) tutorial builds the counter above from scratch. --- # Audio The `Audio` class is the sound hardware at register level, and that is deliberate. It writes the APU registers and stops there: a note plays until something stops it, nothing is sequenced, and there is no mixing. A game that wants music wants a tracker driver, and that is a library rather than a framework concern: GB# abstracts boilerplate, not hardware, and a music engine would be a policy layer pretending to be a wrapper. ## The channels The Game Boy has four sound channels, named by the `Channel` enum: - **`Channel.Pulse1`** is a square wave with a frequency sweep. - **`Channel.Pulse2`** is a square wave without a sweep. - **`Channel.Wave`** plays a 32-sample waveform from wave RAM. - **`Channel.Noise`** is pseudo-random noise, for percussion and effects. Square waves also take a `Duty` (how much of the wave's period is spent high): `Eighth`, `Quarter`, `Half` or `ThreeQuarters`. Lower duties sound thinner and reedier; `Half` is the classic square. ## Notes are register values `Note` names pitches as the value the frequency registers actually take. These are periods, not frequencies: the hardware wants `2048 - 131072/Hz`, and that is what the members hold, so higher values are higher pitches and the spacing is not linear. The enum covers C3 through B6. The enum is backed by `ushort` so a note used at a call site folds to a literal during lowering. There is no note table in ROM and no lookup at runtime: writing `Note.A4` costs exactly what writing `1750` would. The name is free. ## What exists ```csharp Audio.Enable(); Audio.PlayTone(Channel.Pulse1, Note.A4, 15, Duty.Half); ``` - **`Enable()`** powers up the APU and enables all channels on both speakers. The sound hardware ignores every other register while it is off, so this has to come first. - **`Disable()`** powers it down, silencing everything and clearing its registers. - **`SetMasterVolume(left, right)`** sets each speaker's volume, 0–7. - **`SetRouting(mask)`** sets which channels reach which speaker: the low nibble routes channels 1–4 to the right speaker, the high nibble to the left, `0xFF` is everything on both. - **`PlayTone(channel, note, volume, duty)`** starts a note on a square-wave channel. Only `Pulse1` and `Pulse2` do anything, and the note plays until `Stop`. The channel is a runtime value, so this costs a branch in the shim on top of the register writes. - **`PlayNoise(volume, period)`** starts noise on channel 4. `period` is the raw polynomial-counter byte: lower values are brighter. Useful for hits and explosions. - **`Stop(channel)`** silences one channel by dropping its envelope to zero. ## What does not exist, and where to go instead There is no music driver, no sequencer, no sound-effect engine, and no per-frame envelope handling beyond what the hardware does itself. A note started with `PlayTone` sounds until stopped; anything resembling a melody is your frame loop's job, one register write at a time. That gap is intentional, and it has a supported exit. Framework members reach the hardware through `[Native]`, and your code can use exactly the same mechanism: there is no privileged path. A hUGEDriver-style music driver is C code supplied under `"libraries"` in `gbsharp.json`, a header under `"includes"`, and a static class of `[Native]` declarations to call it from C#. [The native escape hatch](native-escape-hatch.md) walks through the pattern, and the [gbsharp.json reference](../reference/gbsharp-json.md) documents the two keys. When a driver like that lands, it will be a library you add, not a framework release you wait for, which is the point of the escape hatch existing. --- # Configuring diagnostics GB# reports what your code costs at every build: WRAM, ROM, estimated cycles, bank switches. A diagnostic nobody can silence is a diagnostic that eventually gets ignored wholesale: [GBS0201](../reference/diagnostics/memory.md#gbs0201-static-allocation) fires on every static field a program declares, and a developer who has accepted their WRAM budget needs a way to stop hearing about it without also stopping hearing about the next thing. So anything that only describes a cost can be turned down, in `.editorconfig` or in the project file, one id or a whole category at a time. ## Severities A diagnostic is reported at one of five severities, and configuration can move it to any of them, or to `none` to silence it entirely: | Severity | Meaning | |---|---| | `error` | Compilation cannot continue. | | `warning` | The code is suspect. | | `performance` | The code is correct but costs more than it looks like it does. | | `resource` | The code consumes a constrained resource: WRAM, VRAM, ROM, sprites. | | `info` | Informational only. | `performance` and `resource` are distinct from `warning` because they say something about the hardware rather than about the code's correctness. The [diagnostics reference](../reference/diagnostics/index.md) lists every id, its default severity, and whether it can be configured. ## In the project file The `"diagnostics"` key in [gbsharp.json](../reference/gbsharp-json.md) maps an id or a category to a severity: ```jsonc { "diagnostics": { "GBS0201": "none", "GBSharp.CycleCost": "none" } } ``` A category is there because bands arrive whole: a developer who does not want estimated cycle costs does not want any of them, and naming them one at a time means editing the setting again every time GB# learns to report something new. The categories match the id bands: | Category | Ids | Covers | |---|---|---| | `GBSharp.Language` | GBS0001–0099 | Constructs outside the GB# language subset | | `GBSharp.Performance` | GBS0100–0199 | Operations that are expensive on SM83 | | `GBSharp.Memory` | GBS0200–0299 | WRAM, VRAM and ROM consumption | | `GBSharp.Banking` | GBS0300–0399 | ROM banking | | `GBSharp.CycleCost` | GBS0400–0499 | Estimated cycle costs | | `GBSharp.Toolchain` | GBS0500–0599 | The toolchain and the build itself | | `GBSharp.Assets` | GBS0600–0699 | The asset pipeline | The `GBSharp.` prefix is optional in the project file (`"CycleCost"` means the same thing) because in a file that is entirely GB# settings, the prefix only says what the file already knows. A setting that names something that does not exist is almost always a typo, so it is rejected as an invalid project file rather than silently ignored: silently ignoring it would leave you believing you configured something. ## In .editorconfig The same settings in the standard Roslyn spellings, which also configure the [editor analyzers](ide-analyzers.md): ```ini [*.cs] dotnet_diagnostic.GBS0201.severity = none dotnet_analyzer_diagnostic.category-GBSharp.CycleCost.severity = none ``` GB# reads `.editorconfig` with Roslyn's own parser, so nesting, globs and section precedence behave exactly as they do for `CS` ids, and every `.editorconfig` from the project directory up to the filesystem root is considered, nearest winning. The per-id form takes Roslyn's severity vocabulary (`none`, `silent`, `error`, `warning`, `suggestion`); the category form accepts GB#'s full scale as well, including `performance` and `resource`. ## Precedence An id wins over a category, the same way the project file wins over an `.editorconfig`: the more specific statement. In full, from most to least specific: 1. An id in `gbsharp.json` 2. A category in `gbsharp.json` 3. An id in `.editorconfig` 4. A category in `.editorconfig` 5. The diagnostic's declared default So `{ "GBSharp.CycleCost": "none", "GBS0401": "performance" }` silences the whole cycle-cost band except the frame-loop figure, which is usually the one worth keeping. ## What cannot be suppressed Anything the compiler depends on stopping the build cannot be configured. Downgrading [GBS0042](../reference/diagnostics/language.md#gbs0042-dynamic-collection) would not make `List` work; it would produce C that compiles and does the wrong thing. Asking is answered rather than ignored: ```text warning GBS0508: GBS0043 cannot be suppressed or downgraded, and the setting for it was ignored. ``` That is [GBS0508](../reference/diagnostics/toolchain.md#gbs0508-diagnostic-cannot-be-suppressed), and it only answers a setting that names a non-suppressible diagnostic *by id*. Muting a whole category is never refused: a blanket statement about a band is not a claim about any particular member of it, and refusing it would mean a developer muting a category could be scolded for a descriptor they have never heard of. Which diagnostics are configurable is marked per id in the [diagnostics reference](../reference/diagnostics/index.md): as a rule, costs and resource notes can be configured freely, and errors cannot. --- # Profiling and the cost model A Game Boy gives you 70,224 cycles between frames, at 59.7 frames a second. GB# estimates what your code spends against that at every build, from the IR, with no toolchain and no emulator, and when an estimate is not enough, `gbsharp profile` measures. ## The static estimates ```text Program.cs(69,9): performance GBS0410: This loop runs up to 8 times at an estimated 3,500 cycles each, about 28,000 in total. for (byte i = 0; i < enemies.Count; i++) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` That loop's bound is not in the source. `enemies.Count` is a runtime field, but a `FixedList` refuses to grow past its capacity, so the capacity is a ceiling the compiler can prove. Without that rule the estimate would have had nothing to say about the most ordinary loop in a GB# program. A `break` makes the count an upper bound rather than an exact one, which is what a worst-case estimate wants. See [GBS0410](../reference/diagnostics/cycle-cost.md#gbs0410-loop-cost). **These are estimates, and the wording never pretends otherwise.** GB# emits C and SDCC decides what actually runs, so the model cannot see the register allocator, the peephole pass, or which way a branch goes. That is honest to within about ±30–50% for straight-line 8-bit work, and a factor of two to five once calls and 16-bit arithmetic are involved. Figures are rounded to two significant figures for the same reason. What survives a systematic error is the comparison: is this loop dearer than that one, did this change make things worse, and that is what the numbers are for. ## Frame loops and spin loops The model refuses more than it answers. `while (true)` is the canonical GB# game loop, and there is no code path that can give an unbounded loop a total: ```csharp while (true) { if (Input.Right) x++; Sprites[0].X = x; Game.WaitVBlank(); } ``` A `while (true)` that waits for VBlank is the *frame* loop, so its per-iteration cost gets measured against a frame instead. One that does not wait is a spin loop with nothing to do with a frame, and gets nothing said about it at all. ```text Program.cs(20,9): performance GBS0401: This frame loop costs an estimated 45,000 cycles an iteration, about 64% of a frame. The hardware gives 70,224 cycles between frames, at 59.7 frames a second. Everything else comes out of the same budget: the VBlank handler, any audio driver, and whatever the loaders copy, none of which GB# can see from the source. So 100% is well past too late. ``` [GBS0401](../reference/diagnostics/cycle-cost.md#gbs0401-frame-loop-is-close-to-a-frame) fires early on purpose, and the build report ranks the functions the loop reaches, which is usually where the time has gone. ## Recursion Recursion is not an estimate: it is a graph property, and it was legal and undetected until now: ```text Program.cs(31,24): warning GBS0058: 'Enemies.Cascade()' is part of a recursive call cycle: Enemies.Cascade calls itself. SM83 has no stack limit check. The stack starts at the top of work RAM and grows down through the same 8 KB the static fields grow up through, so a recursion that goes one level too deep overwrites them: the failure looks like a variable changing value on its own rather than like a crash. ``` See [GBS0058](../reference/diagnostics/language.md#gbs0058-recursive-call). ## Banked calls on the hot path The call graph also knows something about [banking](banking.md) that a single call site cannot. [GBS0301](../reference/diagnostics/banking.md#gbs0301-banked-call) already says a call switches banks; [GBS0440](../reference/diagnostics/cycle-cost.md#gbs0440-banked-call-every-frame) says it switches banks *on the path that runs sixty times a second*: ```text Program.cs(22,13): performance GBS0440: 'ForestLevel.Load()' is reached from the frame loop and switches to ROM bank 2, which costs an estimated 100 cycles more than a local call every time. ``` Its sibling [GBS0441](../reference/diagnostics/cycle-cost.md#gbs0441-callee-could-share-its-callers-bank) notices when a callee's callers all sit in one other bank. Neither fires on setup code, and neither will ever suggest moving banked code *into* bank 0, because that would be advising you to undo the `[Bank]` you wrote, and bank 0 is the 16 KB banking exists to protect. ## Call depth, in calls rather than bytes Call depth is reported in calls rather than bytes, deliberately. GB# rejects delegates and has no function pointers, so the call graph is the complete account of what can reach what and the depth is exact. A byte figure would not be, since GB# never sees SDCC's frame layout or its spills, and would be wrong by a factor of two to four in the optimistic direction, which is the one that lets a ROM ship and then corrupt memory. The measured byte figure comes from the linker instead, in the build report's [memory figures](memory-and-budgets.md). The frame budget is printed exactly and every other cycle figure is rounded, because only one of them is a fact. `--report-json` carries the same numbers unrounded, plus the call depth ([GBS0420](../reference/diagnostics/cycle-cost.md#gbs0420-call-depth)), which is the one worth a CI check, since it is exact and a growing stack is a real regression. ## Measuring with `gbsharp profile` ```bash gbsharp profile MyGame ``` This builds the ROM, runs it headlessly on the instrumented flavour of the bundled emulator for 600 frames (ten seconds of emulated time; `--frames` changes it), and attributes real cycles to C# methods. No window opens and no input is played; it measures what the game does on its own, which makes it repeatable enough to compare across changes. The attribution goes through the same symbol chain every build writes: the linker's `.sym` maps a program counter to a C symbol, and `.functions.json` maps that symbol back to the C# method it was lowered from. The estimate says what a method should cost from a walk over the IR; this says what it did cost, and the two disagreeing is information rather than a bug in either. The same run reports **coverage**: which methods never executed at all. The two are complementary: the profile says what the expensive code was, coverage says what code this run never reached and so proved nothing about. Costs land on the code that paid them, not on its callers. Cross-call attribution (folding a callee's cycles into whoever called it) is deliberately absent rather than approximated, because GBDK's banked-call trampolines rewrite return addresses and a wrong attribution is worse than a missing one. `gbsharp profile` needs the instrumented emulator runtime; if it is missing, the command says so and `gbsharp doctor --fix` fetches it (`tools/get-emulator.ps1` in a checkout). See the [CLI reference](../reference/cli.md) for the full option list, and [Configuring diagnostics](diagnostics-configuration.md) for turning the estimates down once you have accepted them. --- # Publishing a game A ROM needs an emulator, and most people do not have one. `gbsharp publish` produces something they can just run: ```bash gbsharp publish win-x64 ``` ```text Published: MyGame/publish/win-x64/MyGame.exe 124.1 KB, opens straight into the game ``` ## What publish produces Six targets. Five are native platforms and one is the browser: | Rid | Output | |---|---| | `win-x64` | `MyGame.exe`, with `SDL2.dll` beside it | | `linux-x64` | An executable ELF | | `linux-arm64` | An executable ELF | | `osx-x64` | An executable Mach-O | | `osx-arm64` | An executable Mach-O | | `web` | One `.html`, or a folder for a static host | Any of the five native platforms can be published for from any of them, since the player stub is fetched from the same checksum-pinned release as the emulator runtime. Output lands in `/publish//` unless `--out` says otherwise. ## How it works The published executable is the prebuilt GB# Player with the ROM and the window settings appended to it. The Player reads them out of its own file at startup, so nothing is unpacked to disk and nothing is relinked, which is why publishing needs no C toolchain and takes about as long as copying a file. The same mechanism works on every platform, because PE, ELF and Mach-O loaders all ignore bytes past the end of the image they describe. ## Player settings How the game presents itself is the game's decision, not the Player's, so it lives in [gbsharp.json](../reference/gbsharp-json.md): ```jsonc { "name": "MyGame", "player": { "title": "My Game", "scale": 4, "fullscreen": false, "resizable": true, "integerScaling": true, "volume": 80 } } ``` `scale` is 1 to 8 times the Game Boy's 160×144 screen and `volume` runs 0 to 100; both are validated when you publish, because the person who can fix a typo in the project file is the one running the command, not the one running the game. The Player has no settings screen, no ROM browser and no menu, because a player that could disagree with the game would be an emulator wearing the game's name. Saves go to the per-user application data directory, so a game installed somewhere read-only still works and two people on one machine keep separate progress. ## Before shipping Two things to know. The executable is unsigned, and signing has to happen **after** publishing, because the appended bytes are part of what a signature covers: sign first and the append invalidates the signature. And on Windows the game ships with `SDL2.dll` beside it, so it is a small folder rather than a single file; making it one file needs a statically linked SDL in the runtime's release build, which is on the roadmap rather than done. ## The browser ```bash gbsharp publish web --single-file ``` ```text Published: MyGame/publish/web/My Game.html 188.5 KB, one file, opens without a server ``` One `.html` with the emulator and the ROM inlined into it. It opens from a file manager with nothing else installed, which makes it the thing to send somebody who asked to try your game. Without `--single-file` you get a folder to upload to any static host. That layout has to be served over http rather than opened from disk, because browsers block module scripts and wasm fetches under `file://`, which is exactly the constraint the single file mode exists to sidestep. The web player is the native one in a different room: canvas instead of a window, WebAudio instead of a sound device, IndexedDB instead of an application data directory, and the same emulator ABI underneath. Both are paced by the Game Boy's own 59.7275 Hz rather than by the display, so a game does not run 2.4 times too fast on a 144 Hz monitor. Keyboard and gamepad both work; saves survive a reload. ## Related - [Running your game](emulators.md): the same Player is what `gbsharp run` launches during development. - [CLI reference](../reference/cli.md): every `publish` option. --- # GB# in the editor GB# diagnostics live in the editor, before any build. Type `List` and the squiggle appears as you type it, with the same id, the same message and the same suggested alternative a build would print: `List`, `string`, delegates, interfaces, LINQ, and what a static field costs are all reported live. Two pieces make that work: an MSBuild SDK that lets an editor understand a game project, and a set of Roslyn analyzers that share their rules with the compiler. ## The project SDK A game project scaffolded by `gbsharp new` gets a `.csproj` that uses `GBSharp.Sdk`. The project exists so an IDE can bind and analyse the code: it is not how a ROM is produced; `gbsharp build` is. The SDK sets up what an editor needs and nothing else: - **`netstandard2.0`**, because game code is never executed by .NET. It only has to bind, and the lowest common target keeps the reference set small enough that nothing in the BCL looks available when it is not. - **`build/` excluded from compilation**, because build output is not source, and MSBuild's defaults do not know that. - **`gbsharp.json` passed to the analyzers** as an `AdditionalFile`, which is the cached, deterministic path and the only one available to code that must not touch disk. That is how your [diagnostic configuration](diagnostics-configuration.md) reaches the editor. - **`CS0649` silenced**, because an `[Asset]` field is written by the build, never in source. The SDK deliberately carries no GB# configuration. The target, name, emulator, banks and diagnostics all live in [gbsharp.json](../reference/gbsharp-json.md), which stays the single source of truth; duplicating any of it in MSBuild properties would create two places to disagree. ## Why `dotnet build` is blocked Running `dotnet build` on a game project fails, by design: ```text error GBS0509: This project exists so an editor can analyse your code. Build the ROM with 'gbsharp build' instead. (Set GBSharpAllowManagedBuild=true to override.) ``` The guard makes "design-time only" a fact rather than a comment. Design-time builds (what IntelliSense and live analysis run) are exempt, so the editor keeps working completely; what the guard stops is `dotnet build` quietly producing a netstandard assembly that is not a ROM and cannot become one. GBS0509 is raised by MSBuild rather than by the compiler, which is why it does not appear in the [diagnostics reference](../reference/diagnostics/index.md): it can only ever fire from a build GB# never runs. For the same reason, `gbsharp build` never reads the `.csproj`: it enumerates the project's source files itself. If the two views of "the files in this project" drift apart, [GBS0507](../reference/diagnostics/toolchain.md#gbs0507-project-file-drift) says so, a warning rather than an error, because a wrong `.csproj` cannot produce a wrong ROM. ## Shared rules, guaranteed parity The analyzers and the compiler share their rules through `GBSharp.Rules`, a project that targets netstandard2.0 and touches no files, so both read one definition. An id means the same thing in both places: the [GBS0042](../reference/diagnostics/language.md#gbs0042-dynamic-collection) in your editor is the GBS0042 a build reports, down to the message. The guarantee runs one direction on purpose: a test asserts the analyzer's ids are a **subset** of what a build reports, never a superset. The editor may miss things only whole-program analysis can see (banking conflicts, cycle totals, the linker's placements) but it will never invent a diagnostic the build would not confirm. An editor that cried wolf would teach you to ignore it. ## `gbsharp analyze`: the CI lint The same checks run from the command line, without building a ROM: ```bash gbsharp analyze MyGame ``` Everything it needs (parsing, validation, lowering, asset conversion) is in managed code, so it runs with no GBDK installed. That is the whole point: a CI lint job should not have to install a C toolchain to find out a project uses `List`, and an artist working on a PNG gets a loop that does not involve a C compiler. It exits non-zero on any error, which is all a CI step needs. See the [language subset guide](language-subset.md) for what the diagnostics enforce, and the [CLI reference](../reference/cli.md) for the command's options. --- # Running your game ```bash gbsharp run MyGame ``` `gbsharp run` builds the ROM and launches it. With nothing configured, it launches the bundled **GB# Player**: the same player [`gbsharp publish`](publishing.md) wraps around a finished game, here running your latest build. It is what a player runs, not a debugger: it opens no settings screen, reads no symbol files, and shows the game exactly as a published copy would. It ships with the toolchain at a known path (fetched by `gbsharp doctor --fix`, or `tools/get-emulator.ps1` in a checkout), so it wins the default on being reliably there, not on being better. When you want a debugger, name one. ## The named emulators GB# knows how to find and launch the emulators a Game Boy developer is likely to have: | Id | Emulator | Looked for as | |---|---|---| | `player` | The bundled GB# Player | Ships with the toolchain | | `sameboy` | SameBoy | `sameboy`, `SameBoy`, `sameboy_sdl` | | `bgb` | BGB | `bgb64`, `bgb` | | `emulicious` | Emulicious | `Emulicious` | | `mgba` | mGBA | `mgba`, `mgba-qt` | The list is short on purpose: it exists so `gbsharp run` works without configuration, not to be exhaustive: any other emulator still works by giving its path. Executables are searched for on `PATH`. The symbol column matters more than it looks. Every build leaves a `.sym` beside the ROM, and SameBoy, BGB and Emulicious pick it up on their own, so source-level debugging needs no setup at all: `gbsharp run` tells you when the emulator it launched will do this: ```text Launched BGB Symbols alongside the ROM will be picked up for source-level debugging. ``` ## Choosing one For a single run, `--emulator` takes a catalog id or a path to any executable: ```bash gbsharp run MyGame --emulator bgb gbsharp run MyGame --emulator C:/tools/some-emulator.exe ``` An unrecognised executable is handed the ROM path and nothing else, which is what every Game Boy emulator accepts. `--emulator player` names the bundled Player explicitly, so a project can get it even on a machine with BGB installed. For a per-project default, set the `"emulator"` key in [gbsharp.json](../reference/gbsharp-json.md), the same spellings, an id or a path: ```jsonc { "emulator": "sameboy" } ``` ## Resolution order GB# resolves the emulator from the most specific statement to the least: 1. `--emulator` on the command line 2. `"emulator"` in the project file 3. The `GBSHARP_EMULATOR` environment variable 4. A per-user setting: the path in `emulator.txt` under the `gbsharp` config directory (`%APPDATA%\gbsharp` on Windows, `$XDG_CONFIG_HOME/gbsharp` elsewhere) 5. The bundled GB# Player 6. The catalog emulators, searched for on `PATH` A machine-specific absolute path belongs in the per-user file rather than the project: a project that only runs on the machine that wrote it is not shareable, and the failure lands on whoever cloned it. If nothing at all can be launched, the build still succeeds: the ROM is the deliverable, and running it is a convenience. You get [GBS0505](../reference/diagnostics/toolchain.md#gbs0505-no-emulator-configured), a list of everywhere GB# looked, and the path to the ROM. --- # The native escape hatch Framework members are mapped to C symbols by attribute, and user code can use the same mechanism to reach anything GBDK exposes that the framework does not wrap: ```csharp public static class Raw { [Native("set_bkg_tile_xy")] public static void SetBackgroundTile(byte x, byte y, byte tile) => throw new System.NotSupportedException(); } ``` `Raw.SetBackgroundTile(3, 4, 7)` emits `set_bkg_tile_xy(3U, 4U, 7U);` and the C# declaration itself is never emitted. The body exists only to satisfy the C# compiler. There is no privileged path: the framework is written exactly this way. `GB.Background.Load` is a `[Native]` method that happens to ship in a box, so anything the framework can do, your code can do too. A `[Native]` method has to be a shape the mapping can honour (static, with parameter and return types in the GB# subset), and one that is not is [GBS0053](../reference/diagnostics/language.md#gbs0053-invalid-native-declaration) rather than a C error later. ## Reaching GBDK For a function GBDK's own headers declare, the declaration above is all there is. The generated C includes the GBDK headers, so SDCC already knows the prototype, and the call compiles like any other. Find the symbol name in GBDK-2020's documentation, write a `[Native]` method with matching parameters, done. ## Bringing your own C The same attribute reaches functions of your own, in C files you supply. Two [gbsharp.json](../reference/gbsharp-json.md) keys carry them: ```json { "libraries": ["native/sram.c"], "includes": ["native/sram.h"] } ``` `"libraries"` names C source, object, or library files to hand to the linker: a prebuilt hUGEDriver, say, since GB# [owns no music engine](audio.md), or a `.c` file of your own. GB# has no opinion on what is in these files; it only links them, the way any C toolchain links a library the developer supplies. `"includes"` is what makes the functions in them callable. The generated C only includes the GBDK and GB# runtime headers, and SDCC rejects a call to an undeclared function, so a `[Native]` symbol GBDK does not declare needs a prototype. Declare it in a header of your own and name that header under `"includes"`. The header is copied beside the generated C and included after the runtime shim, in every generated file, so every `[Native]` call site sees it. ```c // native/sram.h void sram_save(const uint8_t* data, uint8_t length); ``` ```csharp [Native("sram_save")] public static void Save(ref byte data, byte length) => throw new System.NotSupportedException(); ``` Both keys resolve relative to the project directory, the same as any other path in the file. A file that does not exist is an error at validation, before SDCC ever runs: [GBS0512](../reference/diagnostics/toolchain.md#gbs0512-include-not-found) for a missing header, [GBS0511](../reference/diagnostics/toolchain.md#gbs0511-library-not-found) for a missing library. That is because by the time a build reaches the linker, silently proceeding without a file you thought you linked is a worse failure than a clear upfront error. ## Where the line sits The escape hatch is for reaching *functions*: GBDK's, or yours. It does not exempt the C# around the call from the [language subset](language-subset.md): the arguments still have to be types GB# can lower, and the costs still show up in the [build report](memory-and-budgets.md). That is the point of the design: the boundary is a symbol name, and everything on the C# side of it stays analysable. --- # CLI commands The `gbsharp` command line is how a GB# project is created, checked, built, run, measured and published. It installs as a global `dotnet` tool, `dotnet tool install --global gbsharp`, which is how these pages assume you have it. Working from a checkout instead, every command runs as `dotnet run --project GBSharp.CLI -- ` from the repository root, and `gbsharp ` on these pages is shorthand for exactly that. See [installation](../getting-started/installation.md). Most commands take a project directory as their first argument and default it to the current directory, so `gbsharp build` inside a project and `gbsharp build MyGame` from outside it do the same thing. ## gbsharp new ``` gbsharp new [--template