Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GoudEngine

GoudEngine is a Rust game engine with multi-language SDK support. All game logic lives in Rust. SDKs under sdks/ provide thin wrappers over the FFI boundary.

SDK Support

SDKPackageBackend
C#NuGetDllImport (P/Invoke)
PythonPyPIctypes
TypeScriptnpmnapi-rs (Node.js) + wasm-bindgen (Web)
Rustcrates.ioDirect linking (no FFI)
C/C++localC header via cbindgen
Golocalcgo
KotlinlocalJNI
SwiftlocalC interop
LualocalC FFI

See the Getting Started section in the sidebar for per-language guides.

Engine Features

  • Physics — 2D and 3D rigid body simulation via Rapier. Supports dynamic, kinematic, and static bodies, collision shapes (circle, box, capsule, polygon), raycasting, and collision events.
  • Audio — Playback via Rodio with per-channel volume control (Music, SFX, Ambience, UI, Voice), spatial audio with distance attenuation, looping, and pitch control.
  • Text rendering — TrueType and bitmap font loading, glyph atlas caching, text alignment (left, center, right), word-wrapping, and line spacing.
  • Animation — Sprite sheet animation with frame events, state machine controller with parametric transitions, multi-layer blending (override and additive modes), and standalone tweening with easing functions.
  • Scene management — Named scenes with isolated ECS worlds, transitions (instant, fade, custom), and transition progress tracking.
  • UI — Hierarchical node tree with generational IDs, parent/child relationships, and component-based widgets (buttons, panels, text, images).
  • Error diagnostics — Structured error codes by category, thread-local error state for FFI, backtrace capture, recovery hints, and severity levels.

Status

GoudEngine is in alpha. APIs change frequently. Report issues on GitHub.

Getting Started

Pick the language you know best. Every SDK has the same capabilities – the engine runs in Rust, and your SDK calls into it.

LanguageBest forInstall
RustMaximum performance, engine contributionscargo add goud-engine
C#Unity-like workflow, .NET ecosystemdotnet add package GoudEngine
PythonRapid prototyping, scriptingpip install goudengine
TypeScriptWeb games (WASM), desktop via Node.jsnpm install goudengine
CMinimal overhead, embedded systemsHeader-only
C++RAII wrappers, existing C++ projectsCMake / vcpkg / Conan
GoSimple concurrency, Go-native projectsgo get github.com/aram-devdocs/GoudEngine/sdks/go
KotlinJVM ecosystem, Android (future)Gradle: io.github.aram-devdocs:goudengine
SwiftApple platforms, SwiftPM projectsSwift Package Manager
LuaEmbedded scripting, mod supportluarocks install goudengine or embedded runner

What you get

Each guide walks you through the same steps:

  1. Prerequisites – what to install
  2. Install – one command to get the SDK
  3. Hello World – open a window
  4. Draw a Sprite – load and render an image
  5. Handle Input – respond to keyboard and mouse
  6. Run Examples – try the included demo games
  7. Next Steps – where to go from here

How it works

All 10 SDKs are thin wrappers over the same Rust engine. Your game logic calls SDK functions, which call into Rust via FFI. This means:

  • Identical behavior across all languages
  • Bugs fixed once in Rust, fixed everywhere
  • New features available in all SDKs simultaneously via codegen

Getting Started — C# SDK

Alpha — GoudEngine is under active development. APIs change frequently. Report issues

Prerequisites

Installation

Create a new console project and add the NuGet package:

dotnet new console -n MyGame
cd MyGame
dotnet add package GoudEngine

Open MyGame.csproj and add <AllowUnsafeBlocks>true</AllowUnsafeBlocks>. The SDK uses unsafe interop internally.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="GoudEngine" Version="0.0.832" />
  </ItemGroup>
</Project>

First Project

Replace Program.cs with a minimal window that closes on Escape:

using GoudEngine;

using var game = new GoudGame(800, 600, "My Game");

while (!game.ShouldClose())
{
    game.BeginFrame(0.2f, 0.3f, 0.4f, 1.0f);

    float dt = game.DeltaTime;

    if (game.IsKeyPressed(Keys.Escape))
    {
        game.Close();
        continue;
    }

    game.EndFrame();
}

BeginFrame clears the screen to the given color and prepares the frame. EndFrame swaps buffers and polls events. DeltaTime gives seconds since the last frame — use it to keep movement frame-rate independent.

Run it:

dotnet run

For 3D rendering, the same game window supports both 2D and 3D rendering. Load a 3D model and render it within the same frame loop:

var model = game.LoadModel("assets/model.gltf");
game.Draw3D(model, transform);

Debugger Runtime

Enable debugger mode before creating the windowed game or headless context.

using GoudEngine;

using var ctx = new GoudContext(
    new ContextConfig(new DebuggerConfig(true, true, "getting-started-csharp"))
);

ctx.SetDebuggerProfilingEnabled(true);
string snapshotJson = ctx.GetDebuggerSnapshotJson();
string manifestJson = ctx.GetDebuggerManifestJson();

For a ready-made headless route, run ./dev.sh --game feature_lab. The example publishes feature-lab-csharp-headless, confirms manifest and snapshot access, and prints the manual attach steps:

  1. start cargo run -p goudengine-mcp
  2. call goudengine.list_contexts
  3. call goudengine.attach_context

Drawing a Sprite

Load textures once before the loop. Drawing happens inside the loop between BeginFrame and EndFrame.

using GoudEngine;

using var game = new GoudGame(800, 600, "My Game");

ulong textureId = game.LoadTexture("assets/sprite.png");

float x = 100f;
float y = 100f;
float width = 64f;
float height = 64f;

while (!game.ShouldClose())
{
    game.BeginFrame(0.1f, 0.1f, 0.1f, 1.0f);

    if (game.IsKeyPressed(Keys.Escape))
    {
        game.Close();
        continue;
    }

    game.DrawSprite(textureId, x, y, width, height);

    game.EndFrame();
}

To draw a colored quad without a texture:

game.DrawQuad(x, y, width, height, new Color(1.0f, 0.0f, 0.0f, 1.0f));

Put your image files in an assets/ folder next to the project. The path is relative to the working directory when you run dotnet run.

Handling Input

IsKeyPressed returns true every frame the key is held. Use it for movement. For one-shot actions, track state yourself.

float speed = 200f;

while (!game.ShouldClose())
{
    game.BeginFrame(0.1f, 0.1f, 0.1f, 1.0f);

    float dt = game.DeltaTime;

    if (game.IsKeyPressed(Keys.Escape))
    {
        game.Close();
        continue;
    }

    if (game.IsKeyPressed(Keys.Left))  x -= speed * dt;
    if (game.IsKeyPressed(Keys.Right)) x += speed * dt;
    if (game.IsKeyPressed(Keys.Up))    y -= speed * dt;
    if (game.IsKeyPressed(Keys.Down))  y += speed * dt;
    if (game.IsKeyPressed(Keys.Space)) { }

    game.DrawSprite(textureId, x, y, width, height);

    game.EndFrame();
}

Mouse input follows the same pattern:

if (game.IsMouseButtonPressed(MouseButton.Left)) { /* click action */ }

float mouseX = game.MouseX;
float mouseY = game.MouseY;

Running an Example Game

The repository includes several complete C# games. Clone and run them directly:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
./dev.sh --game flappy_goud     # Flappy Bird clone
./dev.sh --game sandbox         # Full feature sandbox
./dev.sh --game goud_jumper     # Platformer
./dev.sh --game 3d_cube         # 3D rendering demo
./dev.sh --game isometric_rpg   # Isometric RPG
./dev.sh --game hello_ecs       # ECS basics
./dev.sh --game feature_lab     # Supplemental smoke coverage

dev.sh builds the engine and runs the example in one step. Source for each example is in examples/csharp/.

To use a locally built version of the engine instead of the published NuGet package:

./build.sh
./package.sh --local
./dev.sh --game flappy_goud --local

Next Steps

Getting Started: Python SDK

Alpha — APIs change frequently. Report issues

This guide covers installing the Python SDK, opening a window, drawing a sprite, and handling input.

See also: C# guide · TypeScript guide · Rust guide · Go guide · Kotlin guide · Lua guide

Prerequisites

  • Python 3.9 or later
  • A supported OS: Windows x64, macOS x64, macOS ARM64, or Linux x64

Installation

pip install goudengine

The package bundles the native Rust library (.so, .dylib, or .dll). No separate build step is needed when installing from PyPI.

First Project

Create main.py:

from goudengine import GoudGame, Key

game = GoudGame(800, 600, "My Game")

while not game.should_close():
    game.begin_frame()

    if game.is_key_just_pressed(Key.ESCAPE):
        game.close()

    game.end_frame()

game.destroy()

Run it:

python main.py

A window opens at 800x600 and closes when you press Escape.

begin_frame() polls events and clears the screen. end_frame() presents the frame. Everything you draw goes between those two calls.

Debugger Runtime

Enable debugger mode before creating the headless context:

from goudengine import (
    GoudContext,
)
from goudengine.generated._types import ContextConfig, DebuggerConfig

ctx = GoudContext(
    ContextConfig(
        debugger=DebuggerConfig(
            enabled=True,
            publish_local_attach=True,
            route_label="getting-started-python",
        )
    )
)

ctx.set_debugger_profiling_enabled(True)
snapshot_json = ctx.get_debugger_snapshot_json()
manifest_json = ctx.get_debugger_manifest_json()
ctx.destroy()

For a ready-made headless route, run python3 examples/python/feature_lab.py. The example publishes feature-lab-python-headless, confirms manifest and snapshot access, and prints the manual attach steps:

  1. start cargo run -p goudengine-mcp
  2. call goudengine.list_contexts
  3. call goudengine.attach_context

Drawing a Sprite

Load textures once before the game loop, then draw each frame.

from goudengine import GoudGame, Key

game = GoudGame(800, 600, "My Game")

player_tex = game.load_texture("assets/player.png")

while not game.should_close():
    game.begin_frame()

    if game.is_key_just_pressed(Key.ESCAPE):
        game.close()

    game.draw_sprite(player_tex, 400, 300, 64, 64)

    game.end_frame()

game.destroy()

draw_sprite takes the center position of the sprite, not the top-left corner.

An optional sixth argument sets rotation in radians:

import math
game.draw_sprite(player_tex, 400, 300, 64, 64, math.pi / 4)

Handling Input

Keyboard

Two modes are available: pressed this frame, or held continuously.

from goudengine import GoudGame, Key

game = GoudGame(800, 600, "My Game")

x = 400.0

while not game.should_close():
    game.begin_frame()
    dt = game.delta_time

    if game.is_key_just_pressed(Key.ESCAPE):
        game.close()

    if game.is_key_pressed(Key.LEFT):
        x -= 200 * dt
    if game.is_key_pressed(Key.RIGHT):
        x += 200 * dt

    game.end_frame()

game.destroy()

delta_time is the elapsed seconds since the last frame. Use it to make movement frame-rate independent.

Common key constants: Key.ESCAPE, Key.SPACE, Key.ENTER, Key.W, Key.A, Key.S, Key.D, Key.LEFT, Key.RIGHT, Key.UP, Key.DOWN.

Mouse

from goudengine import GoudGame, MouseButton

game = GoudGame(800, 600, "My Game")

while not game.should_close():
    game.begin_frame()

    if game.is_mouse_button_just_pressed(MouseButton.LEFT):
        pos = game.get_mouse_position()
        print(f"Click at ({pos.x:.0f}, {pos.y:.0f})")

    game.end_frame()

game.destroy()

Mouse button constants: MouseButton.LEFT, MouseButton.RIGHT, MouseButton.MIDDLE.

Running an Example Game

The repository includes a complete Flappy Bird clone in Python. Clone the repo and run it with dev.sh:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
./dev.sh --sdk python --game python_demo    # Basic demo
./dev.sh --sdk python --game flappy_bird    # Flappy Bird clone
./dev.sh --sdk python --game sandbox        # Full feature sandbox
python3 examples/python/feature_lab.py      # Supplemental smoke coverage

dev.sh builds the native library and launches the example. It requires a Rust toolchain (cargo) to be installed.

Running Examples from Source (Without dev.sh)

If you have the repository checked out and the native library built, add the SDK path manually:

import sys
from pathlib import Path

sdk_path = Path(__file__).parent.parent.parent / "sdks" / "python"
sys.path.insert(0, str(sdk_path))

from goudengine import GoudGame, Key

Build the native library first:

cargo build --release

Available Types

ImportDescription
GoudGameWindow, game loop, rendering, input
KeyKeyboard key constants (GLFW values)
MouseButtonMouse button constants
Vec22D vector with arithmetic methods
ColorRGBA color (Color.red(), Color.from_hex(0xFF0000))
Transform2D2D position, rotation, scale
SpriteSprite rendering component
EntityECS entity handle

Next Steps

Getting Started — Rust SDK

The Rust SDK links directly against the engine with no FFI overhead. It re-exports goud_engine::sdk::* from a single crate, so all engine types are available through use goudengine::*;.

Other SDKs: C# · Python · TypeScript · Go · Kotlin · Lua


Prerequisites

Rust toolchain

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update stable

System dependencies

Linux:

sudo apt-get install libglfw3-dev libgl1-mesa-dev

macOS:

brew install glfw
# OpenGL is provided by the OS — no extra package needed.

Windows: Install GLFW via vcpkg or download the pre-built binaries from glfw.org.


Installation

Create a new project and add the dependency:

cargo new my-game
cd my-game
cargo add goud-engine

Or set the version directly in Cargo.toml:

[package]
name = "my-game"
version = "0.1.0"
edition = "2021"

[dependencies]
goud-engine = "0.0.832"

First Project

This opens a window, clears it to a blue-grey color each frame, and exits when the window is closed.

use goudengine::{GameConfig, GoudGame};

fn main() {
    let config = GameConfig::new("My Game", 800, 600);
    let mut game = GoudGame::with_platform(config).expect("Failed to create game");

    game.enable_blending();

    while !game.should_close() {
        let _dt = game.poll_events().unwrap_or(0.016);
        game.begin_render();
        game.clear(0.2, 0.3, 0.4, 1.0);

        game.end_render();
        game.swap_buffers().expect("swap_buffers failed");
    }
}

GameConfig::new takes window title, width, and height. GoudGame::with_platform creates the window and OpenGL context. poll_events returns the elapsed time in seconds since the last frame, which you use to scale physics and animations.


Debugger Runtime

Rust is the reference path for the shared debugger contract. Enable debugger mode before startup through DebuggerConfig, then expose the route to goudengine-mcp with publish_local_attach.

#![allow(unused)]
fn main() {
use goudengine::{Context, ContextConfig, DebuggerConfig, EngineConfig};

let debugger = DebuggerConfig {
    enabled: true,
    publish_local_attach: true,
    route_label: Some("getting-started".to_string()),
};

let _windowed = EngineConfig::new()
    .with_title("Debugger Demo")
    .with_debugger(debugger.clone());

let headless = Context::create_with_config(ContextConfig { debugger });
assert!(Context::is_valid(headless));
Context::destroy(headless);
}

Attach workflow:

  1. Start the app with debugger mode enabled.
  2. In another terminal, run cargo run -p goudengine-mcp.
  3. Call goudengine.list_contexts, then goudengine.attach_context.
  4. Inspect the route with goudengine.get_snapshot, goudengine.inspect_entity, goudengine.get_metrics_trace, goudengine.capture_frame, and replay tools.

The runtime stays Rust-owned. SDK helpers and MCP clients only forward requests through the shared local contract.

For a ready-made headless route, run cargo run -p feature-lab. The example publishes feature-lab-rust-headless, enables the shared debugger config path, and prints the same three manual attach steps before its smoke results.


Drawing a Sprite

Load a texture once before the loop, then call draw_sprite each frame.

use goudengine::{GameConfig, GoudGame};

fn main() {
    let config = GameConfig::new("Sprite Demo", 800, 600);
    let mut game = GoudGame::with_platform(config).expect("Failed to create game");

    game.enable_blending();

    let texture = game.load("assets/sprite.png");

    while !game.should_close() {
        let _dt = game.poll_events().unwrap_or(0.016);
        game.begin_render();
        game.clear(0.2, 0.3, 0.4, 1.0);

        game.draw_sprite(
            texture,
            400.0, 300.0,
            64.0, 64.0,
            0.0,
            1.0, 1.0,
            1.0, 1.0, 1.0, 1.0,
        );

        game.end_render();
        game.swap_buffers().expect("swap_buffers failed");
    }
}

Positions are in pixels from the top-left corner. The center_x/center_y arguments are the sprite’s center, not its top-left corner.


Handling Input

Query key state inside the game loop with is_key_pressed.

use goudengine::input::Key;
use goudengine::{GameConfig, GoudGame};

fn main() {
    let config = GameConfig::new("Input Demo", 800, 600);
    let mut game = GoudGame::with_platform(config).expect("Failed to create game");

    game.enable_blending();

    let mut x = 400.0_f32;

    while !game.should_close() {
        let dt = game.poll_events().unwrap_or(0.016);
        game.begin_render();
        game.clear(0.2, 0.3, 0.4, 1.0);

        if game.is_key_pressed(Key::Escape) {
            break;
        }
        if game.is_key_pressed(Key::Left) {
            x -= 200.0 * dt;
        }
        if game.is_key_pressed(Key::Right) {
            x += 200.0 * dt;
        }

        game.end_render();
        game.swap_buffers().expect("swap_buffers failed");
    }
}

is_key_pressed returns true as long as the key is held down. Mouse buttons use is_mouse_button_pressed(MouseButton::Button1).


Running the Example Game

The repository includes a complete Flappy Bird clone in examples/rust/flappy_bird/. It demonstrates texture loading, sprite drawing, physics, collision detection, and input handling across multiple modules.

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo run -p flappy-bird
cargo run -p feature-lab

Controls: Space or left click to flap, R to restart, Escape to quit.

The example must be run from the repository root so asset paths resolve correctly. The game reuses the shared asset directory at examples/csharp/flappy_goud/assets/.


Next Steps

Getting Started — TypeScript SDK

Alpha — This SDK is under active development. APIs change frequently. Report issues

The TypeScript SDK ships a single npm package (goudengine) with two backends:

  • Node.js — native addon via napi-rs, uses GLFW + OpenGL, near-native performance
  • Web — WASM module via wasm-bindgen, runs in any browser with WebAssembly support

Both backends expose the same TypeScript API. Game logic written for one target works on the other.

Other getting-started guides: C# · Python · Rust · Go · Kotlin · Lua


Prerequisites

  • Node.js 16 or later
  • npm 7 or later
  • For web: a browser with WebAssembly support (all modern browsers qualify)

Installation

npm install goudengine

The package uses conditional exports. Node.js projects get the napi-rs backend automatically. For the browser, import from the goudengine/web sub-path (see the web section below).


First Project: Desktop (Node.js)

Create game.ts:

import { GoudGame } from 'goudengine';

const game = new GoudGame({ width: 800, height: 600, title: 'My Game' });

while (!game.shouldClose()) {
  game.beginFrame(0.2, 0.3, 0.4, 1.0);

  const dt = game.deltaTime;

  if (game.isKeyPressed(256)) {
    break;
  }

  game.endFrame();
}

game.destroy();

Run it:

npx tsx game.ts

This opens an 800x600 GLFW window. The loop calls beginFrame to clear the screen, runs your logic, then calls endFrame to swap buffers. deltaTime returns seconds elapsed since the last frame.


First Project: Web (WASM)

Create index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Game</title>
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>

<script type="importmap">
{
  "imports": {
    "goudengine/web": "/node_modules/goudengine/dist/web/generated/web/index.g.js"
  }
}
</script>

<script type="module">
import { GoudGame } from 'goudengine/web';

const canvas = document.getElementById('canvas');

const game = await GoudGame.create({
  width: 800,
  height: 600,
  title: 'My Game',
  canvas,
  wasmUrl: '/node_modules/goudengine/wasm/goud_engine_bg.wasm'
});

game.setClearColor(0.2, 0.3, 0.4, 1.0);

game.run((dt) => {
});
</script>
</body>
</html>

Serve the directory (the importmap requires a real HTTP server, not file://):

npx serve .

Key differences from the Node.js version:

Node.jsWeb
Constructornew GoudGame({...})await GoudGame.create({...})
Extra parameterscanvas, wasmUrl
Game loopwhile (!game.shouldClose())game.run((dt) => { ... })
Clear colorbeginFrame(r, g, b, a)setClearColor(r, g, b, a)

Networking note:

  • Desktop supports the full current wrapper path.
  • Web supports browser WebSocket client connections only.
  • On the web target, use NetworkProtocol.WebSocket and wait until peerCount() > 0 before sending your first packet.
  • The debugger runtime is desktop-only in this batch. goudengine/web does not support debugger attach, snapshot, or manifest APIs.

Debugger Runtime (Desktop Only)

For this rollout, the shipped TypeScript desktop reference path is the Feature Lab example:

./dev.sh --sdk typescript --game feature_lab

That example publishes feature-lab-typescript-desktop, confirms manifest and snapshot access, and prints the manual attach steps:

  1. start cargo run -p goudengine-mcp
  2. call goudengine.list_contexts
  3. call goudengine.attach_context

The web entry feature_lab_web stays outside the debugger rollout in this batch.


Drawing a Sprite

loadTexture is asynchronous on both targets — it returns a Promise<number>. The returned number is a texture handle you pass to drawSprite.

const textureId = await game.loadTexture('assets/player.png');

game.drawSprite(textureId, x, y, width, height);

game.drawSprite(textureId, x, y, width, height, Math.PI / 4);

The x and y coordinates are the center of the sprite. All numeric parameters are f64 at the JavaScript boundary; the engine converts to f32 internally.

Node.js example:

import { GoudGame } from 'goudengine';

const game = new GoudGame({ width: 800, height: 600, title: 'Sprite Demo' });

const playerTex = await game.loadTexture('assets/player.png');

while (!game.shouldClose()) {
    game.beginFrame(0.1, 0.1, 0.1, 1.0);
    game.drawSprite(playerTex, 400, 300, 64, 64);
    game.endFrame();
}

game.destroy();

Web example — same logic, different setup:

const game = await GoudGame.create({
    width: 800, height: 600, title: 'Sprite Demo',
    canvas, wasmUrl: '/node_modules/goudengine/wasm/goud_engine_bg.wasm'
});

const playerTex = await game.loadTexture('assets/player.png');

game.setClearColor(0.1, 0.1, 0.1, 1.0);
game.run((_dt) => {
    game.drawSprite(playerTex, 400, 300, 64, 64);
});

Handling Input

Use isKeyPressed with GLFW key codes. Common codes:

KeyCode
Escape256
Space32
R82
Arrow Left263
Arrow Right262
Arrow Up265
Arrow Down264

For mouse input, use isMouseButtonPressed. Button 0 is the left mouse button.

while (!game.shouldClose()) {
  game.beginFrame(0.1, 0.1, 0.1, 1.0);

  if (game.isKeyPressed(256)) {
    break;
  }
  if (game.isKeyPressed(32) || game.isMouseButtonPressed(0)) {
  }
  if (game.isKeyPressed(263)) {
  }
  if (game.isKeyPressed(262)) {
  }

  game.endFrame();
}

On the web target, isKeyPressed and isMouseButtonPressed work the same way. The WASM backend handles browser keyboard and mouse events internally.


Running an Example Game

Clone the repository and run the Flappy Bird example:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine

# Desktop (Node.js)
./dev.sh --sdk typescript --game flappy_bird

# Web (browser, serves on localhost)
./dev.sh --sdk typescript --game flappy_bird_web

# Sandbox parity app (desktop + web)
./dev.sh --sdk typescript --game sandbox
./dev.sh --sdk typescript --game sandbox_web

# Supplemental smoke coverage
./dev.sh --sdk typescript --game feature_lab
./dev.sh --sdk typescript --game feature_lab_web

dev.sh handles building the SDK and running the example in one step.

To run the example manually:

# Build the native addon first
cd sdks/typescript && npm run build:native && cd ../..

# Desktop
cd examples/typescript/flappy_bird
npm install
npm run desktop

# Web
npm run build:web   # Compile TS to dist/
npm run web         # Start HTTP server on port 8765
# Open http://localhost:8765/examples/typescript/flappy_bird/web/index.html

Controls: Space or left-click to flap, R to restart, Escape to quit (desktop only).


Next Steps

C/C++ SDK

Overview

The C/C++ SDK provides a header-only layer over the GoudEngine native library. The C header (goud/goud.h) exposes type aliases and inline wrapper functions that return integer status codes. The C++ header (goud/goud.hpp) adds RAII wrappers with move semantics and noexcept destructors.

Both headers depend on the generated FFI header (goud_engine.h) and the pre-built native library for your platform.

Installation

vcpkg

vcpkg install goud-engine

Then in your CMakeLists.txt:

find_package(GoudEngine CONFIG REQUIRED)
target_link_libraries(my_game PRIVATE GoudEngine::GoudEngine)

Conan

conan install --requires=goud-engine/<VERSION>

Then in your CMakeLists.txt:

find_package(GoudEngine REQUIRED)
target_link_libraries(my_game PRIVATE GoudEngine::GoudEngine)

Manual (tarball)

Download the tarball for your platform from the GitHub Releases page. Each tarball contains lib/, include/, and cmake/ directories.

Console partners should use the separate goud-engine-console-v<version>-<rid>.tar.gz archives described in the Console Porting Guide. Those archives contain only the static library and goud_engine.h.

Extract and point CMake at the tarball root:

tar -xzf goud-engine-v0.0.832-linux-x64.tar.gz
cmake_minimum_required(VERSION 3.15)
project(MyGame LANGUAGES CXX)

set(GOUD_ENGINE_ROOT "/path/to/goud-engine-v0.0.832-linux-x64")
list(APPEND CMAKE_PREFIX_PATH "${GOUD_ENGINE_ROOT}/cmake")

find_package(GoudEngine CONFIG REQUIRED)

add_executable(my_game main.cpp)
target_link_libraries(my_game PRIVATE GoudEngine::GoudEngine)

CMake Setup

Regardless of the installation method, the CMake usage is the same:

find_package(GoudEngine CONFIG REQUIRED)
target_link_libraries(my_game PRIVATE GoudEngine::GoudEngine)

This provides the GoudEngine::GoudEngine imported target with all necessary include paths and link libraries.

C Hello World

#include <goud/goud.h>
#include <stdio.h>

int main(void) {
    goud_engine_config config = NULL;
    goud_context ctx;
    int status;

    status = goud_engine_config_init(&config);
    if (status != SUCCESS) {
        fprintf(stderr, "config init failed: %d\n", status);
        return 1;
    }

    goud_engine_config_set_title_utf8(config, "Hello C");
    goud_engine_config_set_window_size(config, 800, 600);

    status = goud_engine_create_checked(&config, &ctx);
    if (status != SUCCESS) {
        fprintf(stderr, "engine create failed: %d\n", status);
        return 1;
    }

    goud_color clear = {0.2f, 0.3f, 0.4f, 1.0f};

    while (!goud_window_should_close_checked(ctx)) {
        goud_window_poll_events_checked(ctx);
        goud_renderer_begin_frame(ctx);
        goud_renderer_clear_color(ctx, clear);
        goud_renderer_end_frame(ctx);
        goud_window_swap_buffers_checked(ctx);
    }

    goud_context_dispose(&ctx);
    return 0;
}

C++ Hello World

#include <goud/goud.hpp>

int main() {
    auto config = goud::EngineConfig::create();
    config.setTitle("Hello C++");
    config.setSize(800, 600);

    auto engine = goud::Engine::create(std::move(config));
    engine.enableBlending();

    goud_color clear{0.2f, 0.3f, 0.4f, 1.0f};

    while (!engine.shouldClose()) {
        engine.pollEvents();
        engine.context().beginFrame();
        engine.context().clear(clear);
        engine.context().endFrame();
        engine.swapBuffers();
    }

    return 0;
}

Error Handling

C

Use goud_get_last_error to retrieve structured error information:

goud_error_info err;
int code = goud_get_last_error(&err);
if (code != SUCCESS) {
    fprintf(stderr, "[%s] %s: %s\n", err.subsystem, err.operation, err.message);
}

C++

Use goud::Error::last():

auto err = goud::Error::last();
if (err) {
    std::fprintf(stderr, "[%s] %s: %s\n",
        err.subsystem().c_str(),
        err.operation().c_str(),
        err.message().c_str());
}

Next Steps

Getting Started – Go SDK

Alpha – GoudEngine is under active development. APIs change frequently. Report issues

This guide covers installing the Go SDK, opening a window, drawing a sprite, and handling input.

See also: C# guidePython guideTypeScript guideRust guideSwift guideKotlin guide

Prerequisites

  • Go 1.21 or later
  • A C compiler (GCC or Clang) – required for cgo
  • Rust toolchain – needed to build the native engine library

Building the Native Library

The Go SDK wraps the native Rust engine through its C FFI. Build the engine first:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release

This produces target/release/libgoud_engine.dylib (macOS), libgoud_engine.so (Linux), or goud_engine.dll (Windows).

Installation

The Go SDK uses cgo to call into the native library. Add the module to your project:

go get github.com/aram-devdocs/GoudEngine/sdks/go

For local development from the repository, the SDK is at sdks/go/. Set CGO flags to find the native library:

export CGO_CFLAGS="-I$(pwd)/sdks/go/include"
export CGO_LDFLAGS="-L$(pwd)/target/release"

First Project

Create main.go:

package main

import "github.com/aram-devdocs/GoudEngine/sdks/go/goud"

func main() {
    game := goud.NewGame(800, 600, "My First Game")
    defer game.Destroy()

    for !game.ShouldClose() {
        game.BeginFrame(0.2, 0.2, 0.2, 1.0)

        if game.IsKeyJustPressed(goud.KeyEscape) {
            game.Close()
        }

        game.EndFrame()
    }
}

Run it:

CGO_ENABLED=1 go run main.go

A window opens at 800x600 and closes when you press Escape.

BeginFrame clears the screen to the given RGBA color and prepares the frame. EndFrame swaps buffers and polls events. Everything you draw goes between those two calls.

Drawing a Sprite

Load textures once before the game loop, then draw each frame.

package main

import "github.com/aram-devdocs/GoudEngine/sdks/go/goud"

func main() {
    game := goud.NewGame(800, 600, "Sprite Demo")
    defer game.Destroy()

    tex := game.LoadTexture("assets/player.png")

    for !game.ShouldClose() {
        game.BeginFrame(0.1, 0.1, 0.1, 1.0)

        game.DrawSprite(tex, 400, 300, 64, 64, 0, goud.ColorWhite())

        if game.IsKeyJustPressed(goud.KeyEscape) {
            game.Close()
        }

        game.EndFrame()
    }
}

DrawSprite takes the center position of the sprite, width, height, rotation in radians, and a color tint.

Put your image files in an assets/ folder next to your project. The path is relative to the working directory.

Handling Input

Keyboard

Two modes are available: just pressed this frame, or held continuously.

// One-shot: true only on the frame the key goes down
if game.IsKeyJustPressed(goud.KeySpace) {
    // jump
}

// Held: true every frame the key is down
if game.IsKeyPressed(goud.KeyW) {
    y -= speed * game.DeltaTime()
}
if game.IsKeyPressed(goud.KeyS) {
    y += speed * game.DeltaTime()
}

DeltaTime() is the elapsed seconds since the last frame. Use it to make movement frame-rate independent.

Common key constants: goud.KeyEscape, goud.KeySpace, goud.KeyEnter, goud.KeyW, goud.KeyA, goud.KeyS, goud.KeyD, goud.KeyLeft, goud.KeyRight, goud.KeyUp, goud.KeyDown.

Mouse

if game.IsMouseButtonPressed(goud.MouseButtonLeft) {
    // click action
}

mouseX := game.MouseX()
mouseY := game.MouseY()

Mouse button constants: goud.MouseButtonLeft, goud.MouseButtonRight, goud.MouseButtonMiddle.

Available Types

ImportDescription
goud.NewGameCreate a windowed game instance
goud.ColorRGBA color (goud.ColorWhite(), goud.ColorRGB(r, g, b))
goud.Vec22D vector with arithmetic methods
goud.Vec33D vector
goud.RectRectangle (x, y, width, height)
goud.Transform2D2D position, rotation, scale
goud.EntityIDECS entity handle

Running an Example Game

The repository includes a complete Flappy Bird clone in Go:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release
./dev.sh --sdk go --game flappy_bird

dev.sh builds the native library and launches the example. Source is in examples/go/flappy_bird/.

Code Generation

All files in sdks/go/goud/ and sdks/go/internal/ffi/helpers.go are auto-generated. Do not hand-edit them. Regenerate with:

python3 codegen/gen_go_sdk.py    # Wrapper package
python3 codegen/gen_go.py        # Internal cgo bindings

Next Steps

Getting Started – Kotlin SDK

Alpha – GoudEngine is under active development. APIs change frequently. Report issues

This guide covers installing the Kotlin SDK, opening a window, drawing a sprite, and handling input.

See also: C# guidePython guideTypeScript guideRust guideSwift guideGo guide

Prerequisites

  • JDK 17 or later (Temurin recommended)
  • Gradle 8.5+ (wrapper included in the SDK)
  • Rust toolchain – needed to build the native engine library

Building the Native Library

The Kotlin SDK wraps the native Rust engine through JNI. Build the engine first:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release -p goud-engine-core

This produces the native library that the Kotlin JNI layer loads at runtime.

Installation

Gradle (from source)

Add the SDK as a local dependency. In your build.gradle.kts:

dependencies {
    implementation(files("path/to/sdks/kotlin/build/libs/goudengine-0.0.832.jar"))
}

Maven Central (when published)

dependencies {
    implementation("io.github.aram-devdocs:goudengine:0.0.833")
}

Build the SDK jar

cd sdks/kotlin
./gradlew build --no-daemon

The Gradle build task automatically compiles the native Rust library and bundles it into the jar under native/.

First Project

Create a Kotlin file with a minimal window that closes on Escape:

import com.goudengine.core.EngineConfig
import com.goudengine.core.GoudEngine
import com.goudengine.core.GoudGame

fun main() {
    GoudEngine.ensureLoaded()

    val game = EngineConfig.create()
        .title("My First Game")
        .width(800)
        .height(600)
        .build()

    while (!game.shouldClose()) {
        game.beginFrame()
        // draw here
        game.endFrame()
    }

    game.destroy()
}

Build and run:

./gradlew run

A window opens at 800x600 and closes when you press Escape.

beginFrame clears the screen and prepares the frame. endFrame swaps buffers and polls events. Everything you draw goes between those two calls.

Drawing a Sprite

Load textures once before the loop. Drawing happens between beginFrame and endFrame.

import com.goudengine.core.EngineConfig
import com.goudengine.core.GoudEngine
import com.goudengine.types.Color

fun main() {
    GoudEngine.ensureLoaded()

    val game = EngineConfig.create()
        .title("Sprite Demo")
        .width(800)
        .height(600)
        .build()

    val texture = game.loadTexture("assets/player.png")

    while (!game.shouldClose()) {
        game.beginFrame()

        game.drawSprite(texture, 400f, 300f, 64f, 64f, 0f, Color.white())

        game.endFrame()
    }

    game.destroy()
}

Put your image files in an assets/ folder next to your project. The path is relative to the working directory.

Handling Input

isKeyJustPressed returns true only on the frame the key goes down. Use isKeyPressed for held-down detection.

import com.goudengine.input.Key
import com.goudengine.input.MouseButton

// One-shot: true only on the frame the key goes down
if (game.isKeyJustPressed(Key.Space.value)) {
    // jump
}

// Held: true every frame the key is down
if (game.isKeyPressed(Key.W.value)) {
    y -= speed * game.deltaTime
}
if (game.isKeyPressed(Key.S.value)) {
    y += speed * game.deltaTime
}

// Mouse
if (game.isMouseButtonPressed(MouseButton.Left.value)) {
    val pos = game.getMousePosition()
    // use pos.x, pos.y
}

deltaTime is the elapsed seconds since the last frame. Use it to make movement frame-rate independent.

Value Types

The SDK provides Kotlin data classes for common types:

import com.goudengine.types.Vec2
import com.goudengine.types.Vec3
import com.goudengine.types.Color
import com.goudengine.types.Rect

val position = Vec2(100f, 200f)
val direction = position.normalize()
val distance = position.distance(Vec2.zero())

val color = Color.rgb(1f, 0f, 0f) // red
val transparent = color.withAlpha(0.5f)

val bounds = Rect(0f, 0f, 100f, 50f)
val inside = bounds.contains(Vec2(50f, 25f)) // true

Running an Example Game

The repository includes a complete Flappy Bird clone in Kotlin:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release
./dev.sh --sdk kotlin --game flappy_bird

Source is in examples/kotlin/flappy_bird/.

API Documentation

Generate HTML docs with Dokka:

cd sdks/kotlin
./gradlew dokkaHtml --no-daemon
# output in build/dokka/html/

Code Generation

All Java/Kotlin source files under src/main/java/com/goudengine/internal/ and the Kotlin wrapper classes are auto-generated by codegen/gen_kotlin.py. Do not hand-edit generated files. Regenerate with:

python3 codegen/gen_kotlin.py

Next Steps

Getting Started — Swift SDK

Alpha — GoudEngine is under active development. APIs change frequently. Report issues

Prerequisites

  • Swift 5.9+ (ships with Xcode 15+, or install via swift.org)
  • macOS 13+ (Ventura or later)
  • Rust toolchain — needed to build the native engine library

Building the Native Library

The Swift SDK wraps the native Rust engine through its C FFI. Build the engine first:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release

This produces target/release/libgoud_engine.dylib (macOS) which the Swift package links against.

Installation via SPM

Add GoudEngine as a local Swift Package Manager dependency. In your project’s Package.swift:

// swift-tools-version: 5.9

import Foundation
import PackageDescription

// Point to the built native library.
// Override with GOUD_ENGINE_LIB_DIR for custom layouts.
let libSearchPath: String = ProcessInfo.processInfo.environment["GOUD_ENGINE_LIB_DIR"]
    ?? "../../target/release"

let package = Package(
    name: "MyGame",
    platforms: [
        .macOS(.v13),
    ],
    dependencies: [
        .package(path: "../../sdks/swift"),  // adjust relative path to GoudEngine repo
    ],
    targets: [
        .executableTarget(
            name: "MyGame",
            dependencies: [
                .product(name: "GoudEngine", package: "swift"),
            ],
            path: "Sources/MyGame",
            linkerSettings: [
                .unsafeFlags(["-L", libSearchPath]),
            ]
        ),
    ]
)

The linkerSettings tell the Swift compiler where to find libgoud_engine.dylib. Adjust the path to match your project layout relative to the GoudEngine repository root.

First Project

Create Sources/MyGame/main.swift with a minimal window that closes on Escape:

import GoudEngine

let config = EngineConfig()
config
    .setSize(width: 800, height: 600)
    .setTitle(title: "My First Game")

let game = config.build()

while !game.shouldClose() {
    game.beginFrame(r: 0.2, g: 0.2, b: 0.2, a: 1.0)

    if game.isKeyPressed(key: .ESCAPE) {
        game.requestClose()
    }

    game.endFrame()
}

beginFrame clears the screen to the given color and prepares the frame. endFrame swaps buffers and polls events. game.deltaTime gives seconds since the last frame – use it to keep movement frame-rate independent.

Build and run:

swift build && swift run

Drawing a Sprite

Load textures once before the loop. Drawing happens between beginFrame and endFrame.

import GoudEngine

let config = EngineConfig()
config.setSize(width: 800, height: 600).setTitle(title: "Sprite Demo")
let game = config.build()

let texture = game.loadTexture(path: "assets/player.png")

while !game.shouldClose() {
    game.beginFrame(r: 0.1, g: 0.1, b: 0.1, a: 1.0)

    game.drawSprite(
        textureId: texture,
        x: 100, y: 100,
        width: 64, height: 64,
        rotation: 0,
        color: Color.white()
    )

    if game.isKeyPressed(key: .ESCAPE) {
        game.requestClose()
    }

    game.endFrame()
}

Put your image files in an assets/ folder next to your project. The path is relative to the working directory when you run the executable.

Handling Input

isKeyPressed returns true every frame the key is held. Use it for movement. For one-shot actions, track state yourself.

var x: Float = 400
var y: Float = 300
let speed: Float = 200

while !game.shouldClose() {
    game.beginFrame()
    let dt = game.deltaTime

    if game.isKeyPressed(key: .W) { y -= speed * dt }
    if game.isKeyPressed(key: .S) { y += speed * dt }
    if game.isKeyPressed(key: .A) { x -= speed * dt }
    if game.isKeyPressed(key: .D) { x += speed * dt }

    if game.isMouseButtonPressed(button: .LEFT) {
        // click action
    }

    let mouseX = game.mouseX()
    let mouseY = game.mouseY()

    game.endFrame()
}

Running the Flappy Bird Example

The repository includes a complete Flappy Bird clone in Swift that mirrors the C# flappy_goud example for parity testing:

cd GoudEngine
cargo build --release
./dev.sh --sdk swift --game flappy_bird

Source is in examples/swift/flappy_bird/.

Next Steps

Getting Started – Lua SDK

Alpha – GoudEngine is under active development. APIs change frequently. Report issues

This guide covers installing the Lua SDK, running scripts through the embedded runner, and using the core engine APIs from Lua.

See also: C# guidePython guideTypeScript guideRust guideSwift guideGo guideKotlin guide

Prerequisites

  • Lua 5.4 or later
  • LuaRocks 3.x (for standalone module installation)
  • Rust toolchain – needed to build the native engine library

How It Works

The Lua SDK uses mlua to embed Lua 5.4 inside the Rust engine. There are two ways to use it:

  1. Embedded runner (recommended for games) – The lua-runner binary loads your Lua scripts and registers all engine bindings as globals automatically.
  2. Standalone module (via LuaRocks) – A require-able module for standalone Lua interpreters that provides key constants and version info.

Building the Native Library

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release

Installation

The embedded runner is built as part of the engine. No separate installation is needed:

cargo build --release
# The runner is at examples/lua/runner/

LuaRocks Module

For standalone Lua scripts that need key constants:

cd sdks/lua
luarocks make goudengine-scm-1.rockspec

Or build manually:

cd sdks/lua/luarocks
make          # builds the native library via cargo
make install  # installs Lua modules and the native library

First Project

Create main.lua:

-- When running through the embedded runner, engine globals are
-- registered automatically. No require() needed.

local SCREEN_W = 800
local SCREEN_H = 600

-- The game loop is driven by the embedded runtime.
-- on_update is called every frame with delta time.

function on_init()
    -- Called once when the script loads.
    -- Load textures, set up initial state here.
end

function on_update(dt)
    -- Called every frame.
    -- dt is the elapsed seconds since the last frame.

    if is_key_just_pressed(Key.escape) then
        request_close()
    end
end

function on_draw()
    -- Called every frame after on_update.
    -- Draw sprites and shapes here.
end

Run it through the embedded runner:

./dev.sh --sdk lua --game flappy_bird

Drawing a Sprite

Load textures in on_init, then draw each frame in on_draw.

local player_tex

function on_init()
    player_tex = load_texture("assets/player.png")
end

function on_draw()
    draw_sprite(player_tex, 400, 300, 64, 64, 0)
end

draw_sprite takes the texture handle, center x, center y, width, height, and rotation in radians.

Handling Input

Keyboard

function on_update(dt)
    -- One-shot: true only on the frame the key goes down
    if is_key_just_pressed(Key.space) then
        -- jump
    end

    -- Held: true every frame the key is down
    if is_key_pressed(Key.w) then
        y = y - speed * dt
    end
    if is_key_pressed(Key.s) then
        y = y + speed * dt
    end
    if is_key_pressed(Key.a) then
        x = x - speed * dt
    end
    if is_key_pressed(Key.d) then
        x = x + speed * dt
    end
end

Mouse

function on_update(dt)
    if is_mouse_button_pressed(MouseButton.left) then
        local mx = mouse_x()
        local my = mouse_y()
        -- click action at (mx, my)
    end
end

Using the LuaRocks Module

For standalone scripts that only need constants (not the full engine runtime):

local goud = require("goudengine")
print(goud.VERSION)  -- "0.0.832"

-- Access key constants
local Key = goud.constants.key
print(Key.space)  -- 32

Running the Example Game

The repository includes a complete Flappy Bird clone in Lua:

git clone https://github.com/aram-devdocs/GoudEngine.git
cd GoudEngine
cargo build --release
./dev.sh --sdk lua --game flappy_bird

Source is in examples/lua/flappy_bird/.

Platform Support

PlatformLibraryStatus
macOSlibgoud_engine.dylibSupported
Linuxlibgoud_engine.soSupported
Windowsgoud_engine.dllExperimental

Code Generation

The Lua SDK constants module is auto-generated by codegen/gen_lua.py. Do not hand-edit the generated files. Regenerate with:

python3 codegen/gen_lua.py

Next Steps

Dev Environment Setup

GoudEngine builds on Rust with SDK bindings for C#, Python, and TypeScript. The sections below cover system dependencies, toolchain installation, and verification.

Prerequisites

ToolVersionNotes
Ruststable (edition 2021)Installed via rustup
.NET SDK8.0For C# SDK and examples
Python3.9+ (3.11 recommended)For codegen scripts and Python SDK
Node.js16+ (20 recommended)For TypeScript SDK
cbindgen0.29cargo install cbindgen
wasm-packlatestOnly needed for TypeScript Web/WASM builds
cargo-denylatestcargo install cargo-deny

Python and TypeScript SDK support is optional. Only Rust and the C# SDK are required for core development.

System Dependencies

macOS

Xcode Command Line Tools provide everything needed:

xcode-select --install

GLFW and OpenGL headers ship with macOS. Homebrew is not required for core development.

Linux (Ubuntu/Debian)

sudo apt-get update
sudo apt-get install -y \
    build-essential cmake pkg-config \
    libgl1-mesa-dev libglu1-mesa-dev \
    libxrandr-dev libxinerama-dev libxcursor-dev \
    libxi-dev libxxf86vm-dev \
    libasound2-dev

Required to build GLFW and the audio subsystem.

Note: libglu1-mesa-dev and libxxf86vm-dev are required by CI but are not installed by install.sh. Include them when setting up a fresh machine.

Linux (Fedora)

sudo dnf install -y gcc gcc-c++ cmake pkgconfig \
    alsa-lib-devel libXrandr-devel libXinerama-devel \
    libXcursor-devel libXi-devel mesa-libGL-devel

Linux (Arch)

sudo pacman -S --needed \
    base-devel cmake pkgconf alsa-lib \
    libxrandr libxinerama libxcursor libxi mesa

Using install.sh

The repo includes install.sh to automate system dependency installation:

./install.sh

What it does:

  1. Detects the OS (Linux distro or macOS)
  2. Installs system libraries (OpenGL, X11, ALSA) via the native package manager
  3. Installs .NET SDK 8.0 on Ubuntu/Debian if not already present
  4. Installs Rust via rustup if cargo is not found
  5. Installs cbindgen via cargo install

The script takes no flags. It runs unconditionally based on OS detection.

Limitations:

  • Does not install Node.js, Python, wasm-pack, or cargo-deny
  • Does not install libglu1-mesa-dev or libxxf86vm-dev (needed by CI)
  • Does not cover .NET SDK installation on Fedora
  • On macOS, relies on Xcode Command Line Tools being installed (prompts if missing)

Run the manual system dependency commands above even after running install.sh to match CI parity exactly.

Step-by-Step Setup

  1. Clone the repo

    git clone https://github.com/aram-devdocs/GoudEngine.git
    cd GoudEngine
    
  2. Install system dependencies — run ./install.sh or use the manual commands in the System Dependencies section above.

  3. Install Rust (skip if install.sh already installed it — run cargo --version to check)

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
    source "$HOME/.cargo/env"
    
  4. Install Rust tools

    cargo install cbindgen
    cargo install cargo-deny
    
  5. Verify the build

    cargo check
    cargo build
    cargo test
    
  6. Install .NET SDK 8.0 (required for C# SDK work) — download from dotnet.microsoft.com/download/dotnet/8.0.

  7. Install Python 3.11+ (for codegen and Python SDK) — verify with python3 --version.

  8. Install Node.js 20 (for TypeScript SDK) — use nvm or the official installer.

  9. (Optional) Install wasm-pack (for TypeScript Web/WASM builds)

    curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
    
  10. Run a smoke test

    cargo check
    cargo fmt --all -- --check
    cargo clippy -- -D warnings
    cargo test
    

Verify Your Setup

All five commands must pass before pushing code:

cargo check                        # Type checks
cargo fmt --all -- --check         # Format check
cargo clippy -- -D warnings        # Lint check
cargo test                         # Unit tests
cargo deny check                   # Dependency audit

Common Issues

glfw build fails on Linux

Missing X11 or OpenGL dev libraries. Install the full set from the Linux (Ubuntu/Debian) section above.

libasound2-dev not found (Ubuntu 24.04+)

Ubuntu 24.04 ships a transitional package. If libasound2-dev fails, try libasound-dev instead.

dotnet command not found after install.sh

The .NET SDK install may need a new shell session to take effect. Close and reopen your terminal, or run source ~/.bashrc.

cargo test fails with “failed to initialize any backend” or GL errors

Tests that need an OpenGL context fail without a display server. There are two approaches:

GoudEngine includes a headless NullBackend renderer that requires no GPU or display. Run tests that use it with:

cargo test --features headless --lib -- null
cargo test --test ffi_safety
cargo test --test integration

Use goud_engine::test_helpers::init_test_context() in your own tests to get a NullBackend instance.

Option 2: Xvfb (for tests that require a real GL context)

On headless Linux (CI, WSL without WSLg, SSH sessions), use Xvfb:

sudo apt-get install -y xvfb
xvfb-run -a cargo test

Permission denied on install.sh

chmod +x install.sh

cbindgen version mismatch

The project uses cbindgen 0.29. If you have an older version installed:

cargo install cbindgen --force

Node.js native addon build fails (TypeScript SDK)

node-gyp requires a C++ compiler and Python 3:

# Ubuntu/Debian
sudo apt-get install -y g++ python3
# macOS — Xcode Command Line Tools covers this

Optional Tools

ToolPurposeInstall
mdbookBuild and preview docs locallycargo install mdbook
cargo-tarpaulinCode coverage reportscargo install cargo-tarpaulin
cargo-auditSecurity vulnerability scanningcargo install cargo-audit
GraphVizModule dependency graph (./graph.sh)sudo apt install graphviz / brew install graphviz
cargo-modulesModule dependency graph (./graph.sh)cargo install cargo-modules

Next Steps

Building GoudEngine

Alpha — GoudEngine is under active development. APIs change frequently. Report issues · Contact

Core Build Commands

cargo build                 # Debug build
cargo build --release       # Release build
cargo test                  # Run all tests
cargo test -- --nocapture   # Tests with output

Release Build

build.sh compiles the engine in release mode and copies the native library into the SDK output directories:

./build.sh
./build.sh --release        # Explicit release flag

After running, the compiled .dylib / .so / .dll is placed in the SDK staging directories. The build also refreshes codegen/generated/goud_engine.h, copies that header into the C# and Python package staging paths, and regenerates the C# NativeMethods.g.cs surface.

Packaging

package.sh creates a NuGet package from the built artifacts:

./package.sh                # Package to nuget_package_output/
./package.sh --local        # Package and push to local NuGet feed

The local NuGet feed is at $HOME/nuget-local. To consume it in an example project:

./dev.sh --game <game> --local
# or manually:
dotnet add package GoudEngine --version <version> --source $HOME/nuget-local

SDK Tests

# C# SDK tests
dotnet test sdks/csharp.tests/

# Python SDK tests
python3 sdks/python/test_bindings.py

# TypeScript SDK tests
cd sdks/typescript && npm test

Module Dependency Graph

Generate a visual graph of module dependencies:

./graph.sh

This creates docs/diagrams/module_graph.png and .pdf using cargo modules and GraphViz.

Development Guide

Alpha — GoudEngine is under active development. APIs change frequently. Report issues · Contact

Quick Start

Use dev.sh to build and run examples in one step:

# C# SDK (default)
./dev.sh --game flappy_goud         # 2D game example
./dev.sh --game 3d_cube             # 3D game example
./dev.sh --game goud_jumper         # Platform game example
./dev.sh --game flappy_goud --local # Use local NuGet feed

# Python SDK
./dev.sh --sdk python --game python_demo  # SDK demo
./dev.sh --sdk python --game flappy_bird  # Flappy Bird

# TypeScript SDK
./dev.sh --sdk typescript --game flappy_bird      # Desktop (Node.js)
./dev.sh --sdk typescript --game flappy_bird_web  # Web (WASM)

# Rust SDK
./dev.sh --sdk rust  # Run Rust SDK tests

Local Development Cycle

./build.sh                       # 1. Build engine and SDKs
./package.sh --local             # 2. Deploy to local NuGet feed
./dev.sh --game <game> --local   # 3. Test with example

Git Hooks

This project uses husky-rs for Git hook management.

Pre-commit (fast): format, clippy, basic tests, Python SDK checks.

Pre-push (thorough): full test suite, doctests, security audit.

After editing .husky/hooks/pre-commit or .husky/hooks/pre-push, run:

cargo clean && cargo test

This is required for husky-rs to reload the hooks via build.rs.

Version Management

Versioning is automated through release-please via conventional commits.

  1. Use conventional commit prefixes (feat:, fix:, chore:) in PR titles.
  2. On merge to main, release-please creates or updates a Release PR.
  3. When the Release PR merges, it creates a tag and GitHub release.
  4. The tag triggers the publish pipeline (npm, NuGet, PyPI, crates.io).

For local testing, ./increment_version.sh updates versions manually:

./increment_version.sh           # Patch version (0.0.X)
./increment_version.sh --minor   # Minor version (0.X.0)
./increment_version.sh --major   # Major version (X.0.0)

The script updates goud_engine/Cargo.toml (source of truth), sdks/csharp/GoudEngine.csproj, and all .csproj files under examples/.

Pre-commit Checks

Run these before pushing to confirm the build is clean:

cargo check
cargo fmt --all -- --check
cargo clippy -- -D warnings
cargo deny check
cargo test

AI Agent Setup

This repository includes configuration for AI coding assistants (Claude Code, Codex, Cursor, Gemini) with shared infrastructure across tools.

Directory Structure

.agents/              # Shared cross-tool configuration (source of truth)
├── rules/            # Coding/domain rules (dependency hierarchy, FFI, TDD, etc.)
└── skills/           # Cross-tool skills (shared between Claude, Codex, Cursor, Gemini)
    ├── subagent-driven-development/
    ├── review-changes/
    ├── code-review/
    ├── gh-issue/
    ├── hardening-checklist/
    ├── tdd-workflow/
    ├── sdk-parity-check/
    └── ...

.claude/              # Claude Code configuration
├── agents/           # Subagent definitions (implementer, debugger, reviewers, etc.)
├── rules/            # -> symlinks to .agents/rules/
├── hooks/            # Lifecycle hooks (quality checks, secret scanning, session state)
├── skills/           # -> symlinks to .agents/skills/
├── memory/           # Session state (gitignored)
├── specs/            # Feature specs for multi-session work
└── settings.local.json

.codex/               # OpenAI Codex configuration
└── config.toml       # Agent roles pointing to shared .agents/rules/

.cursor/              # Cursor IDE configuration
├── rules/            # Cursor-specific contextual rules (.mdc files)
└── skills/           # -> symlink to .agents/skills/

Key Files

FilePurpose
AGENTS.mdRoot agent instructions (commands, architecture, anti-patterns)
CLAUDE.mdSymlink to AGENTS.md (Claude Code compatibility)
GEMINI.mdSymlink to AGENTS.md (Gemini compatibility)
.cursorignoreExcludes build artifacts from Cursor indexing

Local Debugger Attach

The shipped local attach flow is debugger-runtime-first. Enable debugger mode in the app process, then let your assistant attach through goudengine-mcp.

The fastest repo-owned validation paths are the Feature Lab examples:

  • C# headless: ./dev.sh --game feature_lab
  • Python headless: python3 examples/python/feature_lab.py
  • Rust headless: cargo run -p feature-lab
  • TypeScript desktop: ./dev.sh --sdk typescript --game feature_lab

Those examples now enable debugger mode with stable route labels and print the manual attach steps. The SDKs that already expose raw manifest/snapshot helpers also exercise those paths directly:

  1. start cargo run -p goudengine-mcp
  2. call goudengine.list_contexts
  3. call goudengine.attach_context

TypeScript web remains out of scope for debugger attach in this batch.

Distributed AGENTS.md

Each subdirectory with non-trivial logic has its own AGENTS.md providing module-specific context to agents working in that area. A CLAUDE.md symlink exists alongside each for Claude Code compatibility. Key locations:

  • goud_engine/AGENTS.md – engine core patterns
  • goud_engine/src/ffi/AGENTS.md – FFI boundary rules
  • sdks/AGENTS.md – SDK development rules
  • codegen/AGENTS.md – codegen pipeline details
  • examples/AGENTS.md – example game conventions

Adding New Skills

Skills live at .agents/skills/<skill-name>/SKILL.md. They are available to both Claude Code and Cursor through symlinks at .claude/skills/ and .cursor/skills/.

To add a skill:

  1. Create .agents/skills/<skill-name>/SKILL.md
  2. The symlinks pick it up automatically – no further configuration needed.

SDK-First Architecture

GoudEngine is a Rust game engine with multi-language SDK support. All game logic lives in Rust. SDKs are thin wrappers: they marshal data and call FFI functions, never implementing logic of their own.

Layer Architecture

Dependencies flow DOWN only. A higher-numbered layer may import from lower layers; the reverse is a violation. The canonical layer definition lives in tools/lint_layers.rs.

Layer 1 (Foundation) : goud_engine/src/core/   — error types, math, handles, provider traits
Layer 2 (Libs)       : goud_engine/src/libs/   — graphics backend, platform, native providers
Layer 3 (Services)   : goud_engine/src/ecs/, assets/  — ECS, asset loading
Layer 4 (Engine)     : goud_engine/src/sdk/, rendering/, context_registry/  — game API, render orchestration
Layer 5 (FFI)        : goud_engine/src/ffi/, wasm/    — C-ABI exports consumed by external SDKs

Beyond the engine crate, two additional layers complete the picture:

  • SDKs (sdks/) — language-specific wrappers over FFI for all SDK languages
  • Apps (examples/) — example games that use SDK APIs

Each layer knows nothing about the layers above it. ffi/ may import from core/, sdk/, ecs/, and assets/. It must never import from sdks/.

Layer Enforcement

A lint-layers binary scans use crate:: imports across all source files and fails the build if any upward dependency is found. It runs in two places:

  • codegen.sh step 2 (cargo run -p lint-layers)
  • Pre-commit hook (via .husky/)

Key Files by Layer

Core (goud_engine/src/core/)

FilePurpose
types.rsShared FFI-compatible types (FfiVec2, GoudContextId, GoudResult)
context_registry.rsThread-safe registry mapping GoudContextId → engine instances
component_ops.rsGeneric component CRUD used by FFI component handlers
math.rsVec2, Vec3, Color, Rect, Mat3x3 with #[repr(C)] for FFI
error.rsGoudError, GoudErrorCode, GoudResult

FFI (goud_engine/src/ffi/)

Each domain has its own file. All public functions are #[no_mangle] pub extern "C".

FileDomain
context.rsEngine context create/destroy
entity.rsEntity spawn/despawn
component.rsGeneric component add/remove/query
component_transform2d.rsTransform2D component operations
component_sprite.rsSprite component operations
window.rsWindow management, frame lifecycle
renderer.rs2D rendering
renderer3d.rs3D rendering
input.rsInput state queries
collision.rsCollision detection
types.rsRe-exports core/types.rs types at the FFI boundary

SDK (sdks/)

All SDK languages live under sdks/. Each has a generated wrapper surface and a thin hand-written API layer. See the sdks/ directory for the full list of supported languages.

Codegen Pipeline

All SDK source files under sdks/*/generated/ and sdks/typescript/src/generated/ are auto-generated. Do not edit them by hand.

goud_sdk.schema.json   — universal type/method definitions
ffi_mapping.json       — maps schema methods -> C ABI function names + signatures
ffi_manifest.json      — auto-extracted from Rust source by build.rs (each cargo build)
goud_engine.h          — auto-generated C header at codegen/generated/ (each native cargo build)
                                           |
                                           v
                              codegen/gen_*.py generators
                                           |
                                           v
                              sdks/*/generated/ output directories

Run the full pipeline:

./codegen.sh

The script runs scaffolding, build, validation, and generation steps in order. See codegen.sh for the current step count and sequence. Validation gates (header checks, lint-layers, coverage, schema consistency) abort the pipeline on failure.

Schema Files

codegen/goud_sdk.schema.json — the single source of truth. Defines:

  • types — value types (e.g., Color, Vec2, Transform2D) with fields and factory methods
  • enums — enumeration types (e.g., Key, MouseButton) with values and optional platform maps
  • tools — high-level objects like GoudGame with constructor, destructor, lifecycle hooks, and methods

codegen/ffi_mapping.json — implementation details. Defines:

  • ffi_types — how each schema type maps to a C struct name and its fields
  • ffi_handles — opaque handle types (GoudContextId, GoudTextureHandle)
  • ctypes_mappings — Python ctypes annotations for pointer types
  • tools — per-method mapping from schema method name to FFI function name and parameters

codegen/ffi_manifest.json — auto-generated by build.rs. Contains the full list of #[no_mangle] extern "C" functions extracted from goud_engine/src/ffi/. Used by validate_coverage.py to detect unmapped functions.

Shared Utilities (codegen/sdk_common.py)

All generators import sdk_common.py for:

  • load_schema() / load_ffi_mapping() — JSON loading
  • Name converters: to_pascal(), to_snake(), to_camel(), to_screaming_snake()
  • Type maps: CSHARP_TYPES, PYTHON_TYPES, TYPESCRIPT_TYPES, CTYPES_MAP, CSHARP_FFI_TYPES
  • write_generated(path, content) — writes output files, creating parent directories

Key Design Decisions

Rust-first. Logic is never duplicated in SDKs. If a SDK method does anything beyond marshaling and calling an FFI function, that logic belongs in Rust.

Single schema, multiple generators. Adding a type to goud_sdk.schema.json causes it to appear in every SDK on the next codegen run. Each generator (codegen/gen_*.py) applies language-appropriate naming conventions. Run ./codegen.sh to regenerate all SDKs.

C# bindings are doubly generated. NativeMethods.g.cs is produced by csbindgen on every cargo build. The higher-level C# wrapper classes in sdks/csharp/generated/ are produced by gen_csharp.py. The two files work together: csbindgen handles the raw [DllImport] declarations, and the Python generator handles the public wrapper API.

Context handles, not pointers. All FFI calls take a GoudContextId (an opaque u64) rather than a raw pointer. The context registry resolves handles to engine instances under a mutex. This prevents use-after-free and type confusion across the FFI boundary.

Error propagation. FFI functions return GoudResult (an i32) — 0 for success, negative for error. Detailed error messages are stored in thread-local storage and retrieved via goud_get_last_error_message().

What Goes Where

You want to…Where to put it
Add a new game mechanicgoud_engine/src/ (core, ecs, or assets)
Expose a mechanic to SDKsAdd #[no_mangle] extern "C" function in goud_engine/src/ffi/
Add a new type to all SDKsAdd to codegen/goud_sdk.schema.json, run ./codegen.sh
Change method naming in one SDKEdit the relevant generator in codegen/gen_<lang>.py
Add a new SDK languageSee Adding a New Language

Architecture Deep-Dive

This guide is for contributors and advanced users who need to understand how GoudEngine works internally. For a shorter overview of the SDK-first design and codegen pipeline, read SDK-First Architecture first.


Layer Hierarchy

GoudEngine enforces a strict dependency hierarchy. Dependencies flow down only. No layer may import from a layer above it.

Within goud_engine/src/, the lint-layers tool enforces 5 internal layers:

Layer 5 — FFI        ffi/, wasm/
               |
Layer 4 — Engine     sdk/, rendering/, component_ops/, context_registry/
               |
Layer 3 — Services   ecs/, assets/
               |
Layer 2 — Libs       libs/  (graphics, platform, providers)
               |
Layer 1 — Foundation core/  (error, math, handle, providers traits)
LayerModules (under goud_engine/src/)Responsibility
1 — Foundationcore/Error types, math primitives, Handle<T>, provider trait definitions
2 — Libslibs/Graphics backend, platform windowing, native provider impls
3 — Servicesecs/, assets/ECS (World, Entity, Component, Systems), asset loading
4 — Enginesdk/, rendering/, component_ops/, context_registry/Game API, render orchestration, context registry
5 — FFIffi/, wasm/C-ABI exports consumed by external SDKs

Beyond the engine crate, two additional layers complete the picture:

LayerPathResponsibility
SDKssdks/Language-specific wrappers over FFI (all SDK languages under sdks/)
Appsexamples/Example games that use SDK APIs

Enforcement

The lint-layers tool (tools/lint_layers.rs) scans all use statements across source files and fails if any upward import is found. It runs in two places:

  • Step 2 of ./codegen.sh (cargo run -p lint-layers)
  • The pre-commit hook

A use goud_engine:: statement inside libs/ or a use crate::ffi:: inside libs/graphics/ are examples of violations the tool will catch.

To generate a visual dependency graph:

./graph.sh   # outputs docs/diagrams/module_graph.png and docs/diagrams/module_graph.pdf

Rust-First Design

All game logic lives in Rust. SDKs are thin wrappers: they marshal data and call FFI functions. They never implement game mechanics, physics, math, or rendering logic.

If you find a method in an SDK that does computation instead of calling an FFI function, that logic belongs in Rust. Move it to goud_engine/src/, expose it via a new FFI function, then have the SDK call that.

DRY validation: when adding features, search for duplicate logic between Rust core and SDK code. If the same calculation appears in both places, one of them is wrong.

Math-in-SDK exception (TypeScript)

Simple value-type operations (Vec2.add, Color.fromHex) are computed locally in the TypeScript SDK to avoid FFI round-trips for trivial math. These are generated by codegen from goud_sdk.schema.json, not hand-written, so they remain consistent across SDK updates. The napi layer accepts f64 at the JS boundary (JavaScript’s native number type) and casts to f32 internally where the Rust engine expects it.

This exception applies only to pure value-type math. Any operation that touches world state, entities, or components must go through FFI.


Dependency Diagram

All paths below are relative to goud_engine/src/.

core/ (Layer 1 — Foundation)
  ├── error.rs              — GoudError, GoudErrorCode, GoudResult
  ├── math.rs               — Vec2, Vec3, Color, Rect, Mat3x3 (#[repr(C)])
  ├── handle.rs             — generational Handle<T>
  └── providers/            — provider trait definitions, null impls, registry, builder

libs/ (Layer 2 — Libs)
  ├── graphics/
  │     ├── backend/opengl/        — all raw gl:: calls live here
  │     ├── renderer2d/           — sprite batching, Camera2D
  │     └── renderer3d/           — 3D primitives, Camera3D, lighting
  ├── platform/
  │     ├── glfw_platform.rs       — desktop window + input
  │     └── winit_platform.rs     — alternative platform backend
  └── providers/
        └── impls/                — native provider impls (OpenGL, Rodio, GLFW)

ecs/ (Layer 3 — Services)
  ├── world.rs              — World, EntityAllocator
  ├── sparse_set.rs         — O(1) component storage
  ├── archetype.rs          — ArchetypeGraph for cache-friendly layout
  ├── query.rs              — typed Query, QueryIter
  ├── systems/transform.rs  — TransformPropagationSystem
  └── components/           — Transform2D, GlobalTransform2D, Sprite, etc.

assets/ (Layer 3 — Services)
  ├── asset_server.rs       — central loader + hot-reload coordinator
  └── loaders/              — texture (PNG/JPG), shader (GLSL), audio (WAV/OGG)

sdk/ (Layer 4 — Engine)
  ├── goud_game.rs          — GoudGame, native Rust API (zero FFI overhead)
  └── entity_builder.rs     — EntityBuilder
rendering/                  — render orchestration
component_ops/              — component operation helpers
context_registry/           — GoudContextId → GoudContext (Mutex-guarded)

ffi/ (Layer 5 — FFI)
  ├── context/                    — goud_context_create / goud_context_destroy
  ├── entity/                     — goud_entity_spawn_empty / goud_entity_despawn
  ├── component/                  — generic component add/remove/query
  ├── component_transform2d/      — Transform2D FFI operations
  ├── component_sprite/           — Sprite FFI operations
  ├── renderer/                   — 2D rendering lifecycle
  ├── renderer3d/                 — 3D rendering lifecycle
  ├── input/                      — keyboard, mouse state queries
  ├── window/                     — frame lifecycle (begin/end frame, swap buffers)
  ├── collision.rs                — collision detection results
  └── types.rs                    — #[repr(C)] shared types (FfiVec2, GoudResult, etc.)
wasm/                             — WASM bindings

--- external to goud_engine crate ---

sdks/                   — all SDK language wrappers (see sdks/ for full list)

examples/
  ├── csharp/         — flappy_goud, 3d_cube, goud_jumper, isometric_rpg, hello_ecs
  ├── python/         — main.py, flappy_bird.py
  └── typescript/     — flappy_bird desktop + web

FFI Boundary

Context-based design

Every FFI call takes a GoudContextId (an opaque u64) as its first argument. The context registry maps that ID to an isolated engine instance under a Mutex. Using a stale ID after context destruction returns an error — it never accesses freed memory.

#![allow(unused)]
fn main() {
// From goud_engine/src/ffi/context/lifecycle.rs
#[no_mangle]
pub extern "C" fn goud_context_create() -> GoudContextId {
    let mut registry = match get_context_registry().lock() {
        Ok(r) => r,
        Err(_) => {
            set_last_error(GoudError::InternalError(
                "Failed to lock context registry".to_string(),
            ));
            return GOUD_INVALID_CONTEXT_ID;
        }
    };
    match registry.create() {
        Ok(id) => id,
        Err(err) => {
            set_last_error(err);
            GOUD_INVALID_CONTEXT_ID
        }
    }
}
}

Function conventions

Every public FFI function must follow these rules:

  • Marked #[no_mangle] pub extern "C"
  • Returns i32 (0 = success, negative = error code) or a sentinel value for handle returns
  • Null-checks every pointer parameter before dereferencing
  • Every unsafe block has a // SAFETY: comment explaining why it is sound

Type requirements

Structs shared across the FFI boundary must use #[repr(C)]. Signatures accept only C-compatible types: primitive integers, floats, *const T, *mut T, and bool. No String, Vec, Option, or other Rust-only types appear at the boundary.

Error propagation

FFI functions return GoudResult (an i32). Detailed error messages are stored in thread-local storage and retrieved by callers via goud_get_last_error_message(). This avoids passing string pointers across the boundary for the common case.

Memory ownership

The default rule is: caller allocates, caller frees. Any deviation must be documented on the function. Box-allocated values returned to callers require a corresponding _free function.

Call flow

C# / Python / TypeScript SDK
        |
        |  goud_*(ctx_id, ...)
        v
FFI function (goud_engine/src/ffi/)
        |
        |  get_context_registry().lock() -> GoudContextHandle
        v
context_registry resolves GoudContextId -> GoudContext
        |
        |  context.world_mut().{spawn_empty, insert, query, ...}
        v
ECS World operation
        |
        v
Graphics / Physics / Asset backend

ECS Architecture

Core concepts

World owns all entities and components. It is the central ECS container. Access it through the context: context.world_mut().

Entity is a generational ID: an index packed with a generation counter. The generation counter detects stale references — if you store an entity ID and the slot is reused after despawn, the generation will not match. Never store raw u64 bits and compare them as if they were stable identifiers; always use the entity API.

Component is a plain data struct with no side-effecting methods. Derive Debug and Clone at minimum. The Component trait is a marker — implementing it registers the type with the ECS.

System is a function that takes a &mut World reference and operates on components via typed queries. Systems run in a declared order within each frame.

Query provides typed, cache-friendly iteration over components. Filter to the minimal set of components the system needs. Never downcast or use Any to access component data.

Storage

The ECS uses two complementary storage strategies:

  • SparseSet — O(1) insert, remove, and lookup. Iteration is cache-friendly because the dense array holds components contiguously.
  • ArchetypeGraph — groups entities with identical component sets into archetypes. This improves iteration performance when systems query large numbers of entities with the same component set.

Built-in components

ComponentPurpose
Transform2DLocal 2D spatial transform: position, rotation, scale
GlobalTransform2DWorld-space 2D transform; written by the propagation system
SpriteTexture handle, color tint, flip flags
RigidBodyPhysics body: type (dynamic/kinematic/static), velocity, mass, gravity scale, damping
ColliderCollision shape (circle, box, capsule, polygon), friction, restitution, sensor flag, layer/mask filtering
AudioSourceAudio clip handle, playback state, volume, pitch, channel (Music/SFX/Ambience/UI/Voice), spatial positioning
TextText content, font handle, font size, color, alignment, word-wrapping
SpriteAnimatorSprite sheet animation clip, frame timing, playback mode (loop/one-shot), frame events
AnimationControllerState machine with parametric transitions, blend duration, bool/float parameters
AnimationLayerStackMulti-layer animation blending with per-layer weight and blend mode (override/additive)
ParentReference to this entity’s parent
ChildrenList of child entity IDs

Transform Propagation

Local vs. world-space

Transform2D holds the local transform relative to the entity’s parent. If an entity has no parent, local and world-space are identical. GlobalTransform2D holds the computed world-space transform. The rendering system reads GlobalTransform2D — never Transform2D directly.

Always mutate Transform2D. Read GlobalTransform2D for world-space positions. The propagation system keeps them in sync.

Parent/Children hierarchy

Hierarchy relationships use two components:

  • Parent — attached to a child, stores the parent entity ID
  • Children — attached to a parent, stores a list of child entity IDs

The system traverses the tree top-down: it processes root entities first, then recurses into children. This ensures each child’s GlobalTransform2D is computed using an already-updated parent GlobalTransform2D.

TransformPropagationSystem

TransformPropagationSystem (in goud_engine/src/ecs/systems/transform.rs) runs both 2D and 3D propagation in a single run() call:

#![allow(unused)]
fn main() {
fn run(&mut self, world: &mut World) {
    propagate_transforms_2d(world);
    propagate_transforms(world);   // 3D
}
}

It declares reads on Transform2D, Parent, and Children, and writes on GlobalTransform2D. The access declaration is used by the scheduler to detect conflicts with other systems.

When it runs

In the game loop, TransformPropagationSystem runs after the user update callback and before the rendering system. The sequence per frame:

poll_events()
    |
    v
InputManager updated
    |
    v
User update callback
  - query World, read/write Transform2D
  - spawn / despawn entities
    |
    v
TransformPropagationSystem::run()
  - Transform2D + Parent/Children hierarchy -> GlobalTransform2D
    |
    v
Rendering system
  - queries (GlobalTransform2D, Sprite) -> screen-space quads
    |
    v
SpriteBatch::flush()
  - group by texture, sort by z-order -> OpenGL draw calls
    |
    v
swap_buffers()

Concrete example

A parent entity at position (100, 50) with a child at local position (20, 10). After propagation:

parent.Transform2D.position  = (100, 50)
parent.GlobalTransform2D.translation = (100, 50)   -- root: local == world

child.Transform2D.position   = (20, 10)             -- local, relative to parent
child.GlobalTransform2D.translation  = (120, 60)    -- world = parent_world + child_local

The test test_2d_propagation_parent_child in goud_engine/src/ecs/systems/transform.rs verifies this (abbreviated):

#![allow(unused)]
fn main() {
// parent at (100, 50)
let parent = spawn_2d(&mut world, Vec2::new(100.0, 50.0));
// child at local (20, 10) -> global (120, 60)
let child = spawn_2d(&mut world, Vec2::new(20.0, 10.0));
set_parent(&mut world, parent, child);

system.run(&mut world);

let child_global = world.get::<GlobalTransform2D>(child).unwrap();
assert!((child_global.translation().x - 120.0).abs() < 0.001);
assert!((child_global.translation().y - 60.0).abs() < 0.001);
}

Provider System

The provider pattern (goud_engine/src/core/providers/) lets subsystems swap implementations at engine initialization time. All provider traits require Send + Sync + 'static so they can be stored in ProviderRegistry and accessed from worker threads during asset streaming. The exception is WindowProvider, which is !Send + !Sync because GLFW requires main-thread access.

Provider lifecycle has five phases: create, init, per-frame update, shutdown, and drop. Null implementations in providers/impls/ enable headless testing without a real window, GPU, or audio device.

See docs/src/architecture/providers.md for the full provider API reference.


Phase 2 Subsystems

Physics

The engine supports 2D physics via Rapier2D and 3D physics via Rapier3D. Both backends run through a PhysicsWorld resource attached to the context. The simulation advances on a fixed timestep accumulator defaulting to 1/60 s; the remainder carries forward to the next frame.

Key behaviors:

  • Bodies that have not moved for several frames enter a sleep state and stop consuming simulation time. They wake automatically when a force is applied or a collision occurs.
  • Gravity scale is set per RigidBody — a value of 0.0 makes a body float freely without disabling collision response.
  • Layer-based collision filtering uses a bitmask on Collider: a body’s layer is compared against the other body’s mask. If the bitwise AND is zero, the pair is skipped entirely before narrow-phase.

FFI exports for physics are grouped under ffi/physics/.

Audio

RodioAudioProvider wraps the rodio crate directly and exposes five named channels: Music, SFX, Ambience, UI, and Voice. Each channel has an independent volume multiplier combined with a master volume at the provider level.

Spatial audio uses two attenuation modes:

  • Inverse-distance — gain falls off as 1/distance from the listener.
  • Linear-distance — gain decreases linearly between a reference distance and a maximum distance cutoff.

Volume is controllable at three granularities: per-instance on AudioSource, per-channel via the provider API, and globally via the master volume setting.

Animation

Three systems handle different levels of animation complexity:

SpriteAnimator drives frame-by-frame sprite sheet animation. It reads an animation clip (start frame, end frame, frame duration) from the component and advances the Sprite texture region each tick. Frame events fire user callbacks at specified frame indices.

AnimationController implements a state machine on top of SpriteAnimator. States are named clips; transitions are edges with conditions evaluated against bool or float parameters set at runtime. Blend duration controls how long a cross-fade lasts when entering a new state.

AnimationLayerStack composes multiple AnimationController outputs by weight. Each layer specifies a blend mode:

  • Override — the layer replaces lower layers for the bones it controls.
  • Additive — the layer’s pose is added on top of the lower result, scaled by weight.

Standalone tweens (not attached to an animator) apply easing functions to arbitrary scalar or Vec2 values: Linear, EaseIn, EaseOut, EaseInOut, EaseInBack, EaseOutBounce.

Text Rendering

TrueType fonts load as FontAsset via the standard asset server pipeline. Bitmap fonts use BitmapFontAsset with a pre-built glyph atlas texture and metrics file. Both types produce glyphs that are cached into a shared atlas; repeated text with the same font and size costs one texture lookup instead of re-rasterizing.

Text component options:

  • Alignment: left, center, right
  • Word-wrapping: enabled by setting max_width; disabled when max_width is zero
  • Line spacing: multiplier applied to the font’s natural line height

Scene Management

SceneManager is a per-context service that owns a collection of named scenes. Each scene contains an isolated ECS World; entities and components in one scene are not visible to queries in another.

Scene transitions use one of three modes:

  • Instant — the old scene is deactivated and the new one activates on the same frame.
  • Fade — the engine renders a fade-out over the outgoing scene, then a fade-in over the incoming scene.
  • Custom — a user-supplied callback controls the transition render.

Scene ID 0 is the default scene. It is created automatically with the context and cannot be destroyed.

UI System

UiManager maintains a hierarchical node tree. Each node is identified by a UiNodeId — a generational handle that detects use-after-free without unsafe code. Widgets are components attached to nodes; the tree structure determines layout propagation and event routing.

Cycle detection runs on every set_parent call. Attempting to create a circular hierarchy returns an error immediately; it does not silently corrupt the tree.

Error Diagnostics

Error codes are grouped into numeric ranges by category:

RangeCategory
0–99Context lifecycle
100–199Resource management
200–299Rendering
300–399Physics
400–499Audio
500–599Animation
600–699UI
700–799Scene management

Errors carry structured metadata: subsystem name, operation name, and a recovery hint string. Thread-local error state stores the most recent error so FFI callers can retrieve the full message after inspecting the GoudResult return code.

In diagnostic mode (enabled via a compile feature or runtime flag), errors capture a backtrace at the point they are created. ErrorSeverity classifies each error:

  • Fatal — engine cannot continue; context should be destroyed.
  • Recoverable — the operation failed but the engine remains in a valid state.
  • Warning — the operation succeeded but something unexpected occurred.

Cross-References

  • SDK-First Architecture — codegen pipeline, thin wrapper rule, schema files
  • Adding a New Language — step-by-step guide for new SDK targets
  • ARCHITECTURE.md in the repo root — high-level overview with mermaid diagrams

Provider System

Introduction

Providers are swappable backend implementations for engine subsystems. Each subsystem — rendering, physics, audio, input, and windowing — is represented by a trait. At engine initialization, you supply concrete implementations. The rest of the engine uses those implementations through the trait interface without knowing which backend is active.

When to use the provider system:

  • Adding a new backend. Implement the relevant provider trait and pass it to the builder. No engine internals need to change.
  • Headless testing. Null providers ship for every subsystem. Tests that do not need real audio or a real window use null providers and avoid any platform dependency.
  • Platform-specific implementations. NDA-bound or platform-restricted backends can be kept out of the public repository and injected via the Rust SDK builder.

The engine selects providers at startup and does not swap them at runtime in v1 (see Design Decisions).


Provider Trait Hierarchy

Every provider implements two supertraits: Provider and ProviderLifecycle. Subsystem traits extend both.

Provider + ProviderLifecycle
         |
         +-- RenderProvider
         +-- PhysicsProvider
         +-- AudioProvider
         +-- InputProvider

WindowProvider  (standalone — no Provider supertrait, see below)

Provider

#![allow(unused)]
fn main() {
pub trait Provider: Send + Sync + 'static {
    fn name(&self) -> &str;
    fn version(&self) -> &str;
    fn capabilities(&self) -> Box<dyn std::any::Any>;
}
}

name and version identify the implementation (e.g., "opengl", "1.0.0"). capabilities returns a type-erased value; prefer the typed accessor on the subsystem trait (e.g., RenderProvider::render_capabilities()) to avoid downcasting.

Send + Sync + 'static is required because ProviderRegistry may be accessed from worker threads during asset streaming.

ProviderLifecycle

#![allow(unused)]
fn main() {
pub trait ProviderLifecycle {
    fn init(&mut self) -> GoudResult<()>;
    fn update(&mut self, delta: f32) -> GoudResult<()>;
    fn shutdown(&mut self);
}
}

The five lifecycle phases:

Create -> Init -> Update (per frame) -> Shutdown -> Drop
  • init is called once during GoudGame::new(). Failure is fatal unless a fallback is configured.
  • update is called once per frame. Providers that do not need per-frame work implement it as a no-op.
  • shutdown is called during GoudGame::drop(). Must not fail; all GPU and OS resources must be released before Drop.

Subsystem Traits

Each subsystem trait extends Provider + ProviderLifecycle and adds domain methods:

#![allow(unused)]
fn main() {
pub trait AudioProvider: Provider + ProviderLifecycle { ... }
pub trait PhysicsProvider: Provider + ProviderLifecycle { ... }
pub trait RenderProvider: Provider + ProviderLifecycle { ... }
pub trait InputProvider: Provider { ... }
}

InputProvider extends only Provider (not ProviderLifecycle) because input polling is handled through update_input(), a method on the trait itself, rather than through the generic lifecycle.

WindowProvider Exception

WindowProvider does not extend Provider and has no Send + Sync bounds:

#![allow(unused)]
fn main() {
pub trait WindowProvider: 'static {
    fn name(&self) -> &str;
    fn init(&mut self) -> GoudResult<()>;
    fn shutdown(&mut self);
    fn should_close(&self) -> bool;
    fn set_should_close(&mut self, value: bool);
    fn poll_events(&mut self);
    fn swap_buffers(&mut self);
    fn get_size(&self) -> (u32, u32);
    fn get_framebuffer_size(&self) -> (u32, u32);
}
}

GLFW requires all window calls on the main thread. Making WindowProvider !Send + !Sync enforces this at the type level. As a consequence, WindowProvider is stored directly in GoudGame rather than in ProviderRegistry. See Thread Safety.


Built-in Provider Reference

RenderProvider

ImplementationFeatureNotes
OpenGLRenderProvidernativeWraps OpenGLBackend
NullRenderProvideralwaysNo-op, returns zero handles

Key methods:

  • begin_frame() -> GoudResult<FrameContext> — starts a frame, returns an opaque token
  • end_frame(frame: FrameContext) -> GoudResult<()> — finalizes and presents; consumes the token
  • create_texture(desc) / destroy_texture(handle) — GPU texture management
  • create_buffer / create_shader / create_pipeline / create_render_target — resource creation with paired destroy methods
  • draw(cmd) / draw_batch(cmds) — 2D sprite draws
  • draw_mesh / draw_text / draw_particles — specialized draw paths
  • set_viewport / set_camera / set_render_target / clear — render state

The FrameContext token returned by begin_frame must be passed to end_frame. This enforces correct frame pairing at compile time — you cannot present without first beginning, and you cannot call begin_frame twice without calling end_frame between.

Console partners should read Console Render Backend Contract before writing a proprietary renderer. That guide maps each method to the engine’s frame loop, resource lifetime rules, and the public integration points for windowing and command submission.

AudioProvider

ImplementationFeatureNotes
RodioAudioProvideraudioUses the rodio crate directly
NullAudioProvideralwaysSilent no-op

Key methods:

  • play(handle, config) -> GoudResult<PlaybackId> — starts playback, returns a handle for the active instance
  • stop / pause / resume(id) — control a specific instance
  • is_playing(id) -> bool
  • set_volume(id, volume) / set_master_volume(volume) / set_channel_volume(channel, volume)
  • set_listener_position(pos) / set_source_position(id, pos) — spatial audio (3D positions as [f32; 3])
  • audio_update() — per-frame stream refill and listener sync

RodioAudioProvider wraps rodio directly (a Layer 1 dependency) rather than going through the AudioManager asset system. Layer 2 code bridges between the asset system and this provider as needed.

InputProvider

ImplementationFeatureNotes
GlfwInputProvidernativeState synced from InputManager each frame
NullInputProvideralwaysAll buttons unpressed, all axes zero

Key methods:

  • update_input() — process queued events and update state
  • key_pressed / key_just_pressed / key_just_released(key: KeyCode) -> bool
  • mouse_position() -> [f32; 2] — window coordinates
  • mouse_delta() -> [f32; 2] — movement since last frame
  • mouse_button_pressed(button: MouseButton) -> bool
  • scroll_delta() -> [f32; 2]
  • gamepad_connected(id) / gamepad_axis(id, axis) / gamepad_button_pressed(id, button)

GlfwInputProvider does not read GLFW state directly. It exposes a sync_from_input_manager method that Layer 2 code calls each frame to copy state from InputManager. This avoids a Layer 1 import of a Layer 2 type.

PhysicsProvider

ImplementationFeatureNotes
Rapier2DPhysicsProviderphysicsWraps rapier2d for 2D rigid body simulation
Rapier3DPhysicsProviderphysicsWraps rapier3d for 3D rigid body simulation
NullPhysicsProvideralwaysNo simulation; all queries return defaults

Key methods:

  • step(delta) — advance the simulation
  • set_gravity / gravity — global gravity as [f32; 2]
  • create_body(desc) / destroy_body(handle)
  • body_position / set_body_position / body_velocity / set_body_velocity
  • apply_force / apply_impulse
  • create_collider / destroy_collider
  • raycast(origin, dir, max_dist) -> Option<RaycastHit>
  • overlap_circle(center, radius) -> Vec<BodyHandle>
  • drain_collision_events() -> Vec<CollisionEvent> — returns owned Vec to avoid lifetime coupling with the provider borrow
  • create_joint / destroy_joint
  • debug_shapes() -> Vec<DebugShape>

All Vec2-like parameters use [f32; 2] arrays to avoid depending on external math types in the trait definition.

WindowProvider

ImplementationFeatureNotes
GlfwWindowProvidernativeWraps GlfwPlatform
NullWindowProvideralwaysNo-op, should_close always false

Key methods:

  • init() / shutdown()
  • should_close() -> bool / set_should_close(value)
  • poll_events() — pump the OS event queue
  • swap_buffers() — present the frame
  • get_size() -> (u32, u32) — screen coordinates
  • get_framebuffer_size() -> (u32, u32) — pixel coordinates (differs from get_size on high-DPI)

GlfwWindowProvider::poll_events() only calls GLFW event polling without dispatching to an input manager. Layer 2 code (GoudGame) calls PlatformBackend::poll_events() with an InputManager for full input dispatch.


Implementing a Custom Provider

This example implements a custom AudioProvider. The same pattern applies to all other subsystem traits.

#![allow(unused)]
fn main() {
use goud_engine::core::error::GoudResult;
use goud_engine::core::providers::audio::AudioProvider;
use goud_engine::core::providers::types::{
    AudioCapabilities, AudioChannel, PlayConfig, PlaybackId, SoundHandle,
};
use goud_engine::core::providers::{Provider, ProviderLifecycle};

pub struct MyAudioProvider {
    capabilities: AudioCapabilities,
    master_volume: f32,
}

impl MyAudioProvider {
    pub fn new() -> Self {
        Self {
            capabilities: AudioCapabilities {
                supports_spatial: true,
                max_channels: 32,
            },
            master_volume: 1.0,
        }
    }
}

// Step 1: Implement the base Provider supertrait.
impl Provider for MyAudioProvider {
    fn name(&self) -> &str { "my-audio" }
    fn version(&self) -> &str { "1.0.0" }
    fn capabilities(&self) -> Box<dyn std::any::Any> {
        Box::new(self.capabilities.clone())
    }
}

// Step 2: Implement ProviderLifecycle.
impl ProviderLifecycle for MyAudioProvider {
    fn init(&mut self) -> GoudResult<()> {
        // Open audio device, allocate buffers, etc.
        Ok(())
    }

    fn update(&mut self, _delta: f32) -> GoudResult<()> {
        // Refill stream buffers, sync listener position, etc.
        Ok(())
    }

    fn shutdown(&mut self) {
        // Close device, free buffers. Must not fail.
    }
}

// Step 3: Implement the subsystem trait.
impl AudioProvider for MyAudioProvider {
    fn audio_capabilities(&self) -> &AudioCapabilities {
        &self.capabilities
    }

    fn audio_update(&mut self) -> GoudResult<()> {
        // Audio-specific per-frame work (distinct from the generic lifecycle update).
        Ok(())
    }

    fn play(&mut self, _handle: SoundHandle, _config: &PlayConfig) -> GoudResult<PlaybackId> {
        // Start playback. Return an ID for the active instance.
        Ok(PlaybackId(1))
    }

    fn stop(&mut self, _id: PlaybackId) -> GoudResult<()> { Ok(()) }
    fn pause(&mut self, _id: PlaybackId) -> GoudResult<()> { Ok(()) }
    fn resume(&mut self, _id: PlaybackId) -> GoudResult<()> { Ok(()) }
    fn is_playing(&self, _id: PlaybackId) -> bool { false }

    fn set_volume(&mut self, _id: PlaybackId, _volume: f32) -> GoudResult<()> { Ok(()) }
    fn set_master_volume(&mut self, volume: f32) { self.master_volume = volume; }
    fn set_channel_volume(&mut self, _channel: AudioChannel, _volume: f32) {}

    fn set_listener_position(&mut self, _pos: [f32; 3]) {}
    fn set_source_position(&mut self, _id: PlaybackId, _pos: [f32; 3]) -> GoudResult<()> { Ok(()) }
}
}

All three trait impls are required. There is no default implementation for any subsystem method — this is intentional so that each backend is explicit about what it supports.


Registration and Swapping

ProviderRegistry holds one boxed trait object per subsystem. Build it with ProviderRegistryBuilder:

#![allow(unused)]
fn main() {
let registry = ProviderRegistryBuilder::new()
    .with_renderer(OpenGLRenderProvider::new(backend))
    .with_audio(RodioAudioProvider::new())
    .build();
// physics and input default to null providers
}

Any slot left unconfigured defaults to its null implementation. This lets you enable subsystems incrementally — a game with no physics needs no physics configuration.

ProviderRegistry fields are public and hold the concrete Box<dyn XxxProvider>:

#![allow(unused)]
fn main() {
pub struct ProviderRegistry {
    pub render:  Box<dyn RenderProvider>,
    pub physics: Box<dyn PhysicsProvider>,
    pub audio:   Box<dyn AudioProvider>,
    pub input:   Box<dyn InputProvider>,
}
}

WindowProvider is not in ProviderRegistry. It is stored directly in GoudGame because it is !Send + !Sync.

FFI SDK users select providers by enum at engine initialization rather than through the builder. Custom provider implementations require the Rust SDK. The capability query API in ffi/providers.rs handles provider selection for SDK consumers.


Thread Safety

All provider traits except WindowProvider require Send + Sync + 'static. This allows ProviderRegistry to be accessed from worker threads — for example, during asset streaming where textures or sound data may be loaded on a background thread.

WindowProvider is !Send + !Sync. GLFW requires all window operations on the main thread, and there is no safe way to enforce this at runtime on arbitrary backends. The type system enforces it instead: WindowProvider lacks the Provider supertrait (which requires Send + Sync), and it is stored outside ProviderRegistry in GoudGame directly. This makes GoudGame itself !Send when a native window is present, matching the constraint of the underlying platform layer.

If an async executor is added to the engine in the future, WindowProvider calls must be scheduled on the main thread through a MainThreadScheduler.


Layer Placement

All paths below are relative to goud_engine/src/.

Layer 1 — Foundation (core/)
  core/providers/            -- canonical trait definitions
  core/providers/registry.rs -- ProviderRegistry
  core/providers/builder.rs  -- ProviderRegistryBuilder
  core/providers/impls/      -- null implementations
                                (NullRenderProvider, NullAudioProvider,
                                 NullPhysicsProvider, NullInputProvider,
                                 NullWindowProvider)

Layer 2 — Libs (libs/)
  libs/providers/            -- re-exports core traits
  libs/providers/impls/      -- native implementations
                                (OpenGLRenderProvider, RodioAudioProvider,
                                 GlfwWindowProvider, GlfwInputProvider)

Layer 5 — FFI (ffi/)
  ffi/providers.rs           -- enum selection for SDK initialization

External — SDKs (sdks/)
  generated from goud_sdk.schema.json  -- C#, Python, TypeScript wrappers

Provider trait definitions are canonical at Layer 1 (core/providers/). Layer 2 (libs/providers/) re-exports those traits and provides native implementations. This allows native implementations to depend on the trait definitions without upward imports — the libs/providers/mod.rs shim explicitly states that crate::core::providers is the canonical source. ProviderRegistry and the builder also live at Layer 1 because they are foundational types alongside GoudError and GoudResult.


Design Decisions

The provider system was designed in RFC-0001 and extended for networking in RFC-0002. Key decisions:

Object-safe dynamic dispatch. All traits are object-safe (no associated types, no generic methods) and stored as Box<dyn XxxProvider>. Dynamic dispatch is acceptable because provider calls are coarse-grained — per-frame or per-batch, not per-vertex. The internal RenderBackend trait, which is not object-safe, remains an implementation detail inside concrete render providers.

Explicit null providers. Each subsystem has an explicit Null*Provider struct rather than relying on Option wrappers or default method implementations. Null providers are visible in the registry, debuggable, and testable. The NullRenderProvider::name() returns "null", which makes it easy to detect misconfigured tests or games.

Provider-owned resources. Providers own their GPU and OS resources. There is no shared resource pool across providers. Handles from one provider are invalid with another.

No hot-swap in release builds. Providers cannot be swapped at runtime in a shipping build. In dev mode, hot-swap is supported using generational handle invalidation. The constraint: the replacement must pass init() before the old provider is dropped, and all existing handles must produce errors (not UB) after the swap.

Vec2 as [f32; 2]. Physics and audio traits use [f32; 2] and [f32; 3] instead of a named vector type. This avoids a dependency on any particular math library in the trait definitions. Concrete implementations convert to their internal types at the boundary.

See RFC-0001 for the full rationale and alternatives considered. See RFC-0002 for the planned NetworkProvider extension.

Console Render Backend Contract

This guide is for NDA partners who need to plug a proprietary renderer into the public engine. It documents the live RenderProvider trait in goud_engine/src/core/providers/render.rs and keeps the public side of the integration clear. Platform SDK calls, swap-chain setup details, and command buffer code stay in your private repo.

What the Engine Expects

The engine owns gameplay, scene traversal, asset descriptors, and the frame loop. Your backend owns GPU device setup, surface management, command submission, and presentation.

The public contract is:

  • RenderProvider defines rendering operations.
  • Provider identifies the backend and exposes capabilities.
  • ProviderLifecycle covers startup, per-frame maintenance, and shutdown.

The engine talks only through these traits. It does not call console graphics APIs directly.

Integration Points

ConcernPublic hookBackend responsibility
Window or surface bootstrapprovider constructor plus init()Bind the renderer to the partner window, surface, or swap-chain object
Back-buffer acquire and presentbegin_frame() / end_frame()Acquire the current target, record commands, submit, and present
Resize or mode changeresize(width, height)Rebuild the swap chain, cached render targets, and dependent state
GPU work submissiondraw and resource methodsTranslate engine descriptors into private API calls
Diagnosticsrender_diagnostics()Report counts and timings the engine can surface in tools

If the console runtime requires extra state that is not part of the public API, store it inside your provider struct and wire it through your private crate.

Base Trait Rules

Provider

Implement name(), version(), and capabilities() with stable values.

  • name() should identify the backend, not the platform holder. Examples: "console-vulkan", "partner-gpu".
  • version() should track the backend package or integration version.
  • capabilities() should return the same data as render_capabilities() in a type-erased form.

ProviderLifecycle

The lifecycle is create -> init -> update -> shutdown -> drop.

  • init() should allocate device objects that must exist before the first frame.
  • update(delta) is available for per-frame maintenance. Use it for fence cleanup, deferred destruction, or stats rollover if your backend needs it.
  • shutdown() must release GPU and OS resources without panicking.

Keep shutdown() idempotent if your private integration layer may call it more than once during teardown.

RenderProvider Method Contracts

Capabilities

render_capabilities(&self) -> &RenderCapabilities

  • Fill in the real limits for texture size, batching, render targets, MSAA, and instancing.
  • Set backend_name to a string that tells the game or tooling which backend is live.
  • Treat these values as stable for the life of the provider.

Frame lifecycle

begin_frame() starts work for one frame and returns a FrameContext.

  • Acquire the back buffer or current render target here.
  • Reset transient allocators and frame-local command state here.
  • Return an error if the frame cannot start cleanly.

end_frame(frame) completes the same frame.

  • Consume the FrameContext you returned from begin_frame().
  • Flush pending work, submit commands, and present.
  • Release frame-local resources that are tied to the acquired image.

resize(width, height) handles any display-size change.

  • Rebuild swap-chain or render-target state that depends on framebuffer size.
  • Preserve persistent resources when possible.
  • Return an error if the new size cannot be supported.

Resource management

The engine creates opaque handles and expects the backend to keep the real GPU objects behind them.

  • create_texture, create_buffer, create_shader, create_pipeline, and create_render_target should allocate backend resources and return stable handles.
  • The paired destroy_* methods must release the matching resource.
  • Keep ownership local to the provider. Do not leak raw backend pointers across the public boundary.
  • If your backend defers destruction until the GPU is idle, queue the work internally and retire it from update() or end_frame().

Drawing

The engine sends already-resolved draw descriptors. Your job is to map them to private API calls.

  • draw(&DrawCommand) handles a single 2D draw.
  • draw_batch(&[DrawCommand]) should preserve the batch order supplied by the engine.
  • draw_mesh(&MeshDrawCommand) covers 3D geometry.
  • draw_text(&TextDrawCommand) covers glyph submission after layout.
  • draw_particles(&ParticleDrawCommand) covers particle draws.

Treat these methods as command recording or direct submission hooks. Do not present from them; presentation belongs in end_frame().

Render state

  • set_viewport(x, y, width, height) updates viewport state.
  • set_camera(camera) updates the active view and projection data.
  • set_render_target(handle) switches between the default framebuffer and an off-screen target.
  • render_target_texture(handle) should return the texture handle bound to an off-screen target when your backend supports that mapping.
  • clear(color) clears the current target.

You can cache this state internally if your backend batches state changes.

Diagnostics

render_diagnostics() should return a cheap snapshot.

Useful fields include:

  • draw-call counts
  • buffer uploads
  • frame time buckets
  • resource counts

These numbers do not need profiler precision. They do need to match what the backend actually submitted.

Skeleton Implementation

#![allow(unused)]
fn main() {
use goud_engine::core::error::GoudResult;
use goud_engine::core::providers::diagnostics::RenderDiagnosticsV1;
use goud_engine::core::providers::render::RenderProvider;
use goud_engine::core::providers::types::{
    BufferDesc, BufferHandle, CameraData, DrawCommand, FrameContext,
    MeshDrawCommand, ParticleDrawCommand, PipelineDesc, PipelineHandle,
    RenderCapabilities, RenderTargetDesc, RenderTargetHandle, ShaderDesc,
    ShaderHandle, TextDrawCommand, TextureDesc, TextureHandle,
};
use goud_engine::core::providers::{Provider, ProviderLifecycle};

pub struct ConsoleRenderProvider {
    caps: RenderCapabilities,
    backend: PrivateConsoleBackend,
}

impl ConsoleRenderProvider {
    pub fn new(backend: PrivateConsoleBackend, caps: RenderCapabilities) -> Self {
        Self { backend, caps }
    }
}

impl Provider for ConsoleRenderProvider {
    fn name(&self) -> &str { "partner-console" }
    fn version(&self) -> &str { "1.0.0" }
    fn capabilities(&self) -> Box<dyn std::any::Any> {
        Box::new(self.caps.clone())
    }
}

impl ProviderLifecycle for ConsoleRenderProvider {
    fn init(&mut self) -> GoudResult<()> {
        self.backend.init_device()
    }

    fn update(&mut self, delta: f32) -> GoudResult<()> {
        self.backend.retire_completed_work(delta)
    }

    fn shutdown(&mut self) {
        self.backend.shutdown();
    }
}

impl RenderProvider for ConsoleRenderProvider {
    fn render_capabilities(&self) -> &RenderCapabilities { &self.caps }

    fn begin_frame(&mut self) -> GoudResult<FrameContext> {
        self.backend.begin_frame()
    }

    fn end_frame(&mut self, frame: FrameContext) -> GoudResult<()> {
        self.backend.end_frame(frame)
    }

    fn resize(&mut self, width: u32, height: u32) -> GoudResult<()> {
        self.backend.resize(width, height)
    }

    fn create_texture(&mut self, desc: &TextureDesc) -> GoudResult<TextureHandle> {
        self.backend.create_texture(desc)
    }

    fn destroy_texture(&mut self, handle: TextureHandle) {
        self.backend.destroy_texture(handle);
    }

    fn create_buffer(&mut self, desc: &BufferDesc) -> GoudResult<BufferHandle> {
        self.backend.create_buffer(desc)
    }

    fn destroy_buffer(&mut self, handle: BufferHandle) {
        self.backend.destroy_buffer(handle);
    }

    fn create_shader(&mut self, desc: &ShaderDesc) -> GoudResult<ShaderHandle> {
        self.backend.create_shader(desc)
    }

    fn destroy_shader(&mut self, handle: ShaderHandle) {
        self.backend.destroy_shader(handle);
    }

    fn create_pipeline(&mut self, desc: &PipelineDesc) -> GoudResult<PipelineHandle> {
        self.backend.create_pipeline(desc)
    }

    fn destroy_pipeline(&mut self, handle: PipelineHandle) {
        self.backend.destroy_pipeline(handle);
    }

    fn create_render_target(
        &mut self,
        desc: &RenderTargetDesc,
    ) -> GoudResult<RenderTargetHandle> {
        self.backend.create_render_target(desc)
    }

    fn destroy_render_target(&mut self, handle: RenderTargetHandle) {
        self.backend.destroy_render_target(handle);
    }

    fn draw(&mut self, cmd: &DrawCommand) -> GoudResult<()> {
        self.backend.draw(cmd)
    }

    fn draw_batch(&mut self, cmds: &[DrawCommand]) -> GoudResult<()> {
        self.backend.draw_batch(cmds)
    }

    fn draw_mesh(&mut self, cmd: &MeshDrawCommand) -> GoudResult<()> {
        self.backend.draw_mesh(cmd)
    }

    fn draw_text(&mut self, cmd: &TextDrawCommand) -> GoudResult<()> {
        self.backend.draw_text(cmd)
    }

    fn draw_particles(&mut self, cmd: &ParticleDrawCommand) -> GoudResult<()> {
        self.backend.draw_particles(cmd)
    }

    fn set_viewport(&mut self, x: i32, y: i32, width: u32, height: u32) {
        self.backend.set_viewport(x, y, width, height);
    }

    fn set_camera(&mut self, camera: &CameraData) {
        self.backend.set_camera(camera);
    }

    fn set_render_target(&mut self, handle: Option<RenderTargetHandle>) {
        self.backend.set_render_target(handle);
    }

    fn clear(&mut self, color: [f32; 4]) {
        self.backend.clear(color);
    }

    fn render_diagnostics(&self) -> RenderDiagnosticsV1 {
        self.backend.render_diagnostics()
    }
}
}

The PrivateConsoleBackend type is yours. Keep it outside the public repo if it contains NDA APIs, private handles, or platform-specific build glue.

Common Failure Modes

  • Presenting from draw_* instead of end_frame()
  • Rebuilding swap-chain state in begin_frame() on every frame instead of only on resize or recoverable acquire failure
  • Returning guessed capabilities instead of measured limits
  • Letting resource handles escape into partner-owned game code
  • Reporting diagnostics from queued work rather than submitted work

Adding a New Language Target

This guide walks through adding a new language binding (e.g., Lua, Go, Java) to GoudEngine. The existing generators are the best reference: codegen/gen_csharp.py for a complete, mature implementation and codegen/gen_ts_web.py for a simpler one.

Before starting, read SDK-First Architecture to understand the pipeline.


Step 1: Understand the Schema

Read the two source-of-truth files:

  • codegen/goud_sdk.schema.json — all types, enums, and tool methods that every SDK must expose
  • codegen/ffi_mapping.json — the C ABI function names and signatures behind each schema method

The schema’s types section defines value types (Color, Vec2, Transform2D) with fields and factory constructors. The enums section defines Key, MouseButton, and similar. The tools section defines GoudGame with a constructor, destructor, lifecycle methods (beginFrame, endFrame), and all game methods.

Your generator reads both files and emits code for each of these sections.


Step 2: Add Type Mappings

Open codegen/sdk_common.py and add a type mapping table for your language. The existing tables show the pattern:

# Example for a hypothetical language "Lua" (Lua has no static types,
# so this would map to annotation strings or documentation stubs)
LUA_TYPES = {
    "f32": "number", "f64": "number",
    "u8": "integer", "u16": "integer", "u32": "integer", "u64": "integer",
    "i8": "integer", "i16": "integer", "i32": "integer", "i64": "integer",
    "bool": "boolean", "string": "string", "void": "nil",
}

The schema uses type names like f32, bool, string, and composite names like Color or Transform2D. Your mapping table handles the primitives; the generator handles the composite types by looking them up in the schema.

Also add a ctypes-equivalent mapping if your language calls the native library through a C interop layer that requires explicit type annotations.


Step 3: Create the Generator

Create codegen/gen_<lang>.py. The generator must produce at minimum:

  1. FFI declarations — how to call the C functions from your language (e.g., DllImport in C#, ctypes.CDLL in Python, napi-rs bindings in TypeScript/Node)
  2. Value type wrappers — classes or structs for Color, Vec2, Vec3, Rect, Transform2D, Sprite
  3. Enum definitionsKey and MouseButton with their numeric values
  4. Tool wrappers — the GoudGame class with all methods from schema["tools"]["GoudGame"]

Generator structure

#!/usr/bin/env python3
"""Generates the <Lang> SDK from the universal schema."""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from sdk_common import (
    HEADER_COMMENT, SDKS_DIR, load_schema, load_ffi_mapping,
    to_pascal, to_snake, write_generated,
)

OUT = SDKS_DIR / "<lang>"
schema = load_schema()
mapping = load_ffi_mapping()


def gen_types():
    # Emit value types from schema["types"]
    ...


def gen_enums():
    # Emit enums from schema["enums"]
    ...


def gen_game():
    # Emit GoudGame wrapper from schema["tools"]["GoudGame"]
    # and mapping["tools"]["GoudGame"]
    ...


if __name__ == "__main__":
    print("Generating <Lang> SDK...")
    gen_types()
    gen_enums()
    gen_game()
    print("<Lang> SDK generation complete.")

All output files MUST start with the HEADER_COMMENT constant so readers know not to edit them:

lines = [f"// {HEADER_COMMENT}", ""]

Use write_generated(path, content) to write output files — it creates parent directories automatically and prints a status line.

Mapping methods to FFI calls

For each method in schema["tools"]["GoudGame"]["methods"], look up the corresponding entry in mapping["tools"]["GoudGame"]["methods"] to get the FFI function name:

for method_name, method_def in schema["tools"]["GoudGame"]["methods"].items():
    ffi_fn = mapping["tools"]["GoudGame"]["methods"][method_name]["ffi"]
    # ffi_fn is a string like "goud_renderer_draw_sprite"

The ffi_mapping.json lifecycle section covers beginFrame and endFrame, which call multiple FFI functions in sequence. Check mapping["tools"]["GoudGame"]["lifecycle"] for those.


Step 4: Add to codegen.sh

Add a step to codegen.sh between the existing generator steps and the final validation:

echo "║ [N/8] Generating <Lang> SDK..."
python3 codegen/gen_<lang>.py

Update the step count in the surrounding echo messages.


Step 5: Create the SDK Directory

Create sdks/<lang>/ with the package manifest for your language’s package manager:

sdks/<lang>/
├── <package manifest>   # e.g., go.mod, build.gradle, rockspec
├── README.md
└── generated/           # files emitted by gen_<lang>.py

The SDK directory must contain at minimum one working example of how a game calls GoudGame. See sdks/csharp/ and sdks/python/ for the expected structure.


Step 6: Add Tests

Add a test file that verifies the generated bindings load and the FFI calls round-trip correctly. At minimum, test:

  • Library loads without error
  • GoudGame constructor (or equivalent) initializes
  • A type factory (e.g., Color.red()) returns the expected values

For Python the equivalent is sdks/python/test_bindings.py. Run it with:

python3 sdks/python/test_bindings.py

Add a corresponding command to the project AGENTS.md Essential Commands section.


Step 7: Add an Example

Port an existing example to your new language. The simplest starting point is examples/csharp/hello_ecs/, which demonstrates ECS basics without physics or complex rendering.

For a fuller parity test, port examples/csharp/flappy_goud/ — the Python SDK already has a matching examples/python/flappy_bird.py, so you can compare those two implementations side by side.

Place the example under examples/<lang>/.


Checklist

Before merging a new language target:

  • codegen/sdk_common.py has a type map for the new language
  • codegen/gen_<lang>.py exists and runs without error
  • All generated files begin with HEADER_COMMENT
  • codegen.sh includes the generator step
  • sdks/<lang>/ has a package manifest
  • ./codegen.sh runs end-to-end cleanly
  • Test file exists and passes
  • At least one example game exists under examples/<lang>/
  • AGENTS.md Essential Commands lists how to run the new SDK

Reference: Existing Generators

FileTargetNotes
codegen/gen_csharp.py.NET 8Most complete; handles struct marshaling, builder pattern, DllImport
codegen/gen_python.pyPython 3Uses ctypes; reference for dynamic-type languages
codegen/gen_ts_node.pyTypeScript (Node)Uses napi-rs; generates both Rust glue and TypeScript wrapper
codegen/gen_ts_web.pyTypeScript (Web)Smallest generator; wraps a WASM module, no FFI declarations needed

gen_ts_web.py is the simplest because it targets WASM — there are no ctypes or DllImport declarations. It wraps a pre-built WASM module handle directly. If your target also has a managed runtime that handles memory, this may be the closest analogue.

gen_csharp.py is the most detailed example of FFI struct mapping, null checking, and builder construction. Read it before writing a generator for a statically typed, natively-compiled language.

Physics

GoudEngine provides 2D and 3D rigid body physics through the Rapier physics library.

Providers

ProviderBackendDimensions
Rapier2DPhysicsProviderrapier2d2D
Rapier3DPhysicsProviderrapier3d3D
NullPhysicsProvidernonefallback

PhysicsWorld

The PhysicsWorld resource controls simulation parameters:

  • Timestep: fixed at 1/60s by default
  • Iterations: 8 velocity, 3 position (configurable)
  • Gravity: Vec2 for 2D, Vec3 for 3D
  • Time scale: 0.0–10.0 range for slow-motion or fast-forward
  • Sleeping: idle bodies stop simulating to save CPU

The simulation uses a fixed-timestep accumulator pattern for deterministic behavior regardless of frame rate.

Components

RigidBody

Attached to entities that participate in physics simulation.

FieldTypeDescription
body_typeRigidBodyTypeDynamic, Kinematic, or Static
velocityVec2Linear velocity
massf32Body mass
gravity_scalef32Per-body gravity multiplier
angular_velocityf32Rotational speed
linear_dampingf32Velocity decay per frame
angular_dampingf32Angular velocity decay
can_sleepboolWhether the body can enter sleep state

Collider

Defines collision geometry attached to a body.

FieldTypeDescription
shapeColliderShapeCircle, Box (AABB), Capsule, or Polygon
frictionf32Surface friction coefficient
restitutionf32Bounciness (0 = no bounce, 1 = full)
is_sensorboolTrigger-only (no physical response)
layeru32Collision layer bitmask
masku32Which layers this collider interacts with

Collision Shapes

  • ColliderShape::Circle(radius) — circle with given radius
  • ColliderShape::Aabb(half_extents) — axis-aligned box
  • ColliderShape::Capsule(radius, height) — capsule (rounded box)
  • ColliderShape::Polygon(vertices) — convex polygon from vertex list

Layer Filtering

Layer-based collision filtering uses bitmasks on Collider. A body’s layer is compared against the other body’s mask. If the bitwise AND is zero, the pair is skipped before narrow-phase collision detection.

FFI

Physics FFI functions are in goud_engine/src/ffi/physics/. Key functions:

  • goud_physics_create() / goud_physics_destroy()
  • goud_physics_set_gravity()
  • goud_physics_add_rigid_body() / goud_physics_remove_body()

Physics providers are registered globally per context ID.

Audio

Audio playback uses the Rodio library through the AudioProvider trait.

Providers

ProviderBackendNotes
RodioAudioProviderrodioFull playback, spatial audio
NullAudioProvidernoneSilent fallback for headless testing

AudioSource Component

Attach AudioSource to an entity for audio playback.

FieldTypeDefaultDescription
volumef321.0Playback volume (0.0–1.0)
pitchf321.0Speed multiplier
loopingboolfalseLoop playback
channelAudioChannelSFXMixing channel
auto_playboolfalseStart playing on spawn
spatialboolfalseEnable spatial positioning
max_distancef32100.0Maximum audible distance
attenuationAttenuationModelInverseDistanceDistance falloff model

Channels

Audio is routed through named channels with independent volume control:

ChannelUse case
MusicBackground music
SFXSound effects
AmbienceEnvironmental audio
UIInterface sounds
VoiceDialogue and narration

Volume Control

Three levels of volume control, applied multiplicatively:

  1. Master volume — global scaling for all audio
  2. Channel volume — per-channel scaling (Music, SFX, etc.)
  3. Instance volume — per-source scaling on individual playback instances

Spatial Audio

When spatial is enabled on an AudioSource:

  • Set listener position with set_listener_position([x, y, z]) (or 2D helper variants)
  • Source position is read from the entity’s transform
  • Two attenuation models: InverseDistance and LinearDistance
  • max_distance controls the cutoff range

Engine-level controls include explicit listener/source placement:

  • goud_audio_set_listener_position_3d()
  • goud_audio_set_source_position_3d()
  • 2D convenience variants: goud_audio_set_listener_position() and goud_audio_set_source_position()

Crossfade And Mixing

Batch 2.5 added timed and immediate blend controls:

  • goud_audio_crossfade(from, to, mix) for immediate two-player blend
  • goud_audio_crossfade_to(from, asset, duration, channel) for timed transition to new content
  • goud_audio_mix_with(primary, asset, secondary_volume, secondary_channel) for layered playback
  • goud_audio_update_crossfades(delta_sec) to advance active timed transitions
  • goud_audio_active_crossfade_count() to inspect active transitions

FFI

Audio FFI functions are in goud_engine/src/ffi/audio/. Key functions:

  • goud_audio_play() / goud_audio_stop()
  • goud_audio_pause() / goud_audio_resume()
  • goud_audio_set_global_volume() / goud_audio_set_channel_volume()
  • goud_audio_set_listener_position_3d() / goud_audio_set_source_position_3d()
  • goud_audio_crossfade() / goud_audio_crossfade_to() / goud_audio_mix_with()
  • goud_audio_update_crossfades() / goud_audio_active_crossfade_count()

Text Rendering

GoudEngine renders text using TrueType fonts with glyph atlas caching for performance.

Text Component

Attach Text to an entity to render text.

FieldTypeDefaultDescription
contentString""Text to display
font_handleAssetHandle<FontAsset>TrueType font asset
bitmap_font_handleOption<AssetHandle<BitmapFontAsset>>NoneBitmap font (.fnt)
font_sizef3216.0Font size in pixels
colorColorwhiteText color (RGBA)
alignmentTextAlignmentLeftHorizontal alignment
max_widthOption<f32>NoneWord-wrap width
line_spacingf321.0Line height multiplier

Font Types

Two font formats are supported:

  • TrueType (TTF/OTF) — vector fonts loaded via FontAsset, rasterized into a glyph atlas at the requested size
  • Bitmap (.fnt) — pre-rendered sprite fonts loaded via BitmapFontAsset

Text Alignment

TextAlignment controls horizontal positioning:

  • Left — left edge aligned to entity position
  • Center — centered on entity position
  • Right — right edge aligned to entity position

Word Wrapping

Set max_width to enable word-wrapping. Text breaks at word boundaries when a line exceeds the specified width. The line_spacing multiplier controls vertical distance between lines.

Glyph Atlas

TrueType fonts are rasterized into a glyph atlas texture on first use. The atlas caches rendered glyphs to avoid per-frame rasterization. Different font sizes produce separate atlas entries.

Animation

GoudEngine provides three animation systems: sprite animation, state machine controllers, and standalone tweening.

Sprite Animation

AnimationClip

Defines a sequence of frames from a sprite sheet.

FieldTypeDescription
framesVec<Rect>Source rectangles in the sprite sheet
frame_durationf32Seconds per frame
modePlaybackModeLoop or OneShot
eventsVec<AnimationEvent>Events triggered at specific frames

SpriteAnimator

Attach SpriteAnimator to an entity to drive frame-by-frame animation. It updates the entity’s Sprite source rectangle each frame based on the active clip.

FieldTypeDescription
clipAnimationClipActive animation clip
current_frameusizeCurrent frame index
elapsedf32Time since last frame change
playingboolWhether animation is advancing
finishedboolTrue when a OneShot clip reaches its last frame

Animation Events

AnimationEvent fires when the animator reaches a specific frame. Events carry an EventPayload for passing data to event handlers.

Animation Controller

A state machine that manages transitions between animation states.

States and Transitions

Each state holds an AnimationClip. Transitions between states are triggered by parameter conditions:

  • BoolEquals { param, value } — transition when a bool parameter matches
  • FloatGreaterThan { param, threshold } — transition when a float exceeds a threshold
  • FloatLessThan { param, threshold } — transition when a float is below a threshold

Parameters are set externally (from game logic) and the controller evaluates transitions each frame.

Blend Duration

Transitions can specify a blend_duration for smooth crossfading between states. During blending, both the outgoing and incoming clips contribute to the final frame.

Animation Layers

AnimationLayerStack supports multiple animation layers with independent clips and blend weights.

FieldTypeDescription
nameStringLayer identifier
weightf32Blend weight (0.0–1.0)
blend_modeBlendModeOverride or Additive
  • Override: higher layers replace lower layers, scaled by weight
  • Additive: layer output is added to layers below

Tweening

Standalone tween functions interpolate values over time with easing:

EasingDescription
LinearConstant speed
EaseInStarts slow, accelerates
EaseOutStarts fast, decelerates
EaseInOutSlow start and end
EaseInBackOvershoots before settling
EaseOutBounceBounces at the end

FFI

Animation FFI is in goud_engine/src/ffi/animation/:

  • goud_animation_controller_*() — state machine operations
  • goud_animation_layer_*() — layer stack management
  • goud_tween_create() / goud_tween_update() — tween lifecycle

Scene Management

Scenes provide isolated ECS worlds that can be switched at runtime with optional transitions.

Scene Lifecycle

Each context has a SceneManager that owns named scenes. A default scene (ID 0) is created automatically and cannot be destroyed.

Creating Scenes

Scenes are created per context with a name. Each scene owns an independent ECS World, so entities in one scene are fully isolated from entities in another.

Switching Scenes

Transition to a scene by ID with a specified transition type:

TransitionBehavior
InstantImmediate switch, no animation
FadeFade out current scene, fade in next
CustomSDK-managed transition with manual progress control

Transition Progress

During a transition, goud_scene_transition_progress() returns a value from 0.0 to 1.0. Use this to drive custom visual effects. Call goud_scene_transition_tick() each frame to advance the transition.

Scene Isolation

Entities and components in one scene are not visible to queries in another. Switching scenes activates the target scene’s World and deactivates the current one. There is no shared entity space across scenes.

FFI

Scene FFI functions are in goud_engine/src/ffi/scene.rs, scene_loading.rs, and scene_transition.rs:

  • goud_scene_create() / goud_scene_destroy()
  • goud_scene_get_by_name()
  • goud_scene_load() / goud_scene_unload()
  • goud_scene_set_active()
  • goud_scene_transition_to()
  • goud_scene_transition_progress() / goud_scene_transition_is_active()
  • goud_scene_transition_tick()

SDK Surface

These scene-loading APIs are exposed directly in all primary SDKs:

  • C#: LoadScene(), UnloadScene(), SetActiveScene()
  • Python: load_scene(), unload_scene(), set_active_scene()
  • TypeScript: loadScene(), unloadScene(), setActiveScene()

UI System

GoudEngine provides a hierarchical UI node tree for building game interfaces.

UiManager

Each context has a UiManager that owns UI nodes in a tree structure. The tree is separate from the ECS world — UI nodes are not entities.

Node Tree

UiNodeId

Nodes are identified by generational IDs (UiNodeId). Like entity IDs, the generation counter detects stale references — using a node ID after the node is removed returns an error.

Creating Nodes

Create a node and optionally set its parent. Nodes without a parent are root nodes.

Parent/Child Relationships

  • Each node has at most one parent
  • Each node can have multiple children
  • Setting a parent is validated: cycle detection prevents circular hierarchies
  • Removing a parent detaches the node (becomes a root)
  • Removing a node removes its entire subtree

UiComponent

A UiComponent can be attached to a node to define its visual role:

  • Button
  • Panel
  • Text
  • Image

Components are data-only — rendering and layout are handled by the UI rendering system.

Layout

The layout system supports both anchor-based and flex-style placement:

  • Anchors: top-left, center, bottom-right, and stretch behaviors
  • Edge spacing: margin and padding fields on nodes
  • Flex containers: row/column direction, alignment, and spacing
  • Deterministic recompute when the UI tree changes or the window size changes

The implementation is in goud_engine/src/ui/layout.rs and goud_engine/src/ui/manager/layout.rs.

Input Semantics

UI input is processed before game-level input polling.

  • Click dispatch targets the topmost interactive node under the cursor
  • Hover state emits enter/leave transitions as the pointer moves
  • Focus traversal supports Tab and Shift+Tab
  • Enter/Space activates focused buttons
  • Consumed UI input is masked so gameplay input queries do not re-handle the same event

The input flow is implemented in goud_engine/src/ui/manager/input.rs with per-frame integration in the game loop.

Cycle Detection

set_parent validates the relationship before applying it. Attempting to create a circular hierarchy returns an error immediately; it does not silently corrupt the tree.

FFI

UI FFI functions are in goud_engine/src/ffi/ui/:

  • Node creation, destruction, and reparenting
  • Component attachment and modification
  • Tree traversal queries

Networking

The networking SDK API is wrapper-based and sits on top of generated low-level bindings.

  • NetworkManager creates endpoints from a game or context:
    • C#: new NetworkManager(gameOrContext)
    • Python: NetworkManager(game_or_context)
    • TypeScript: new NetworkManager(gameOrContext)
  • NetworkEndpoint is returned by host() / Host() and connect() / Connect(). It exposes receive, send, send_to / sendTo, poll, disconnect, stats, peer count, simulation, and overlay helpers.

connect() stores the provider-assigned peer ID on the endpoint. Client code can call send(...) without passing a peer ID each time. Host endpoints do not have a default peer, so they reply with send_to(...) / SendTo(...).

CSharp

using System.Text;
using GoudEngine;

using var hostContext = new GoudContext();
using var clientContext = new GoudContext();

var host = new NetworkManager(hostContext).Host(NetworkProtocol.Tcp, 9000);
var client = new NetworkManager(clientContext).Connect(NetworkProtocol.Tcp, "127.0.0.1", 9000);

client.Send(Encoding.UTF8.GetBytes("ping"));

while (true)
{
    host.Poll();
    client.Poll();

    var packet = host.Receive();
    if (packet is null)
    {
        continue;
    }

    host.SendTo(packet.Value.PeerId, Encoding.UTF8.GetBytes("pong"));
    break;
}

Python

from goudengine import GoudContext, NetworkManager, NetworkProtocol

host_context = GoudContext()
client_context = GoudContext()

host = NetworkManager(host_context).host(NetworkProtocol.TCP, 9000)
client = NetworkManager(client_context).connect(NetworkProtocol.TCP, "127.0.0.1", 9000)

client.send(b"ping")

while True:
    host.poll()
    client.poll()

    packet = host.receive()
    if packet is None:
        continue

    host.send_to(packet.peer_id, b"pong")
    break

TypeScript Desktop

import { GoudContext, NetworkManager, NetworkProtocol } from "goudengine/node";

const hostContext = new GoudContext();
const clientContext = new GoudContext();

const host = new NetworkManager(hostContext).host(NetworkProtocol.Tcp, 9000);
const client = new NetworkManager(clientContext).connect(
  NetworkProtocol.Tcp,
  "127.0.0.1",
  9000,
);

client.send(Buffer.from("ping"));

while (true) {
  host.poll();
  client.poll();

  const packet = host.receive();
  if (!packet) {
    continue;
  }

  host.sendTo(packet.peerId, Buffer.from("pong"));
  break;
}

TypeScript Web

Browser networking is available through goudengine/web as a WebSocket client path.

  • Use NetworkProtocol.WebSocket on the web target.
  • Browser hosting is not supported; host() returns a negative handle and the shared wrapper throws.
  • connect() returns immediately, but the browser socket still opens asynchronously. Poll until peerCount() > 0 before treating the connection as live.
import { GoudGame, NetworkManager, NetworkProtocol } from "goudengine/web";

const game = await GoudGame.create({ width: 800, height: 600, title: "Web Net" });
const endpoint = new NetworkManager(game).connect(
  NetworkProtocol.WebSocket,
  "ws://127.0.0.1:9001",
  9001,
);

game.run(() => {
  endpoint.poll();

  if (endpoint.peerCount() > 0) {
    endpoint.send(new TextEncoder().encode("ping"));
  }

  const packet = endpoint.receive();
  if (packet) {
    console.log(new TextDecoder().decode(packet.data));
  }
});

Current limitation:

  • Browser networking is client-only.
  • Web target networking currently uses the generated browser backend, not the native loopback path used by goudengine/node.

Error Handling

GoudEngine uses structured error codes with contextual diagnostics for debugging.

Error Codes

Error codes are organized by category ranges:

RangeCategoryExamples
0SuccessSUCCESS
1–99ContextERR_NOT_INITIALIZED, ERR_ALREADY_INITIALIZED
100–199ResourceERR_RESOURCE_NOT_FOUND, ERR_HANDLE_EXPIRED
200–299GraphicsERR_SHADER_COMPILATION_FAILED
300–399Entity/ECSERR_ENTITY_NOT_FOUND, ERR_COMPONENT_NOT_FOUND
400–499InputInput-related errors
500–599SystemPlatform and window errors
600–699ProviderProvider initialization and operation errors
900–999InternalUnexpected failures

GoudError

The GoudError enum covers all error conditions. Key variants:

  • NotInitialized / AlreadyInitialized — context lifecycle errors
  • ResourceNotFound(String) / ResourceLoadFailed(String) — asset system errors
  • EntityNotFound / ComponentNotFound — ECS errors
  • PhysicsInitFailed(String) / AudioInitFailed(String) — provider errors
  • InternalError(String) — unexpected internal failures

All errors convert to i32 codes for FFI. Detailed messages are stored in thread-local storage.

FFI Error Retrieval

SDK code retrieves error details after a failed FFI call:

  • goud_get_last_error() — returns the i32 error code
  • goud_get_last_error_message() — returns a human-readable message
  • goud_last_error_subsystem() — which subsystem produced the error
  • goud_last_error_operation() — which operation failed
  • goud_last_error_backtrace() — stack trace (when diagnostic mode is enabled)
  • goud_clear_last_error() — reset error state

Error Context

Each error records:

  • Subsystem: which engine module produced the error (e.g., "physics", "audio")
  • Operation: what was being attempted (e.g., "create_body", "load_texture")
  • Severity: Fatal, Recoverable, or Warning

Recovery

Errors include a RecoveryClass:

  • Fatal — unrecoverable; the engine must shut down
  • Recoverable — caller can handle and continue
  • Retry — transient failure; retrying may succeed

Diagnostic Mode

When enabled, errors capture backtraces at the point of creation. This is useful for debugging but adds overhead. Diagnostic mode is disabled by default in release builds.

Source

Error system implementation is in goud_engine/src/core/error/:

  • types.rsGoudError enum
  • codes.rs — error code constants
  • diagnostic.rs — backtrace capture
  • recovery.rs — recovery strategies
  • ffi_bridge.rs — thread-local FFI error state

Object Pooling and Frame Arenas

GoudEngine provides two allocation primitives for performance-sensitive paths: EntityPool for reusable entity slots, and FrameArena for per-frame temporary allocations.

EntityPool

A pre-allocated, free-list pool of entity slot indices with O(1) acquire and release.

How It Works

  1. Create a pool with a fixed capacity. All slots are pre-allocated upfront.
  2. Acquire a slot – the pool returns a (slot_index, entity_id) pair from its LIFO free-list.
  3. Release a slot – the index returns to the free-list for reuse.

No heap allocation occurs on the hot path. The pool does not interact with the ECS World directly – slot-to-entity mapping is set by the integration layer via set_slot_entity.

FFI Surface

FunctionDescription
goud_entity_pool_create(capacity)Create a pool, returns a handle
goud_entity_pool_destroy(handle)Destroy a pool
goud_entity_pool_acquire(handle)Acquire one entity, returns entity ID
goud_entity_pool_release(handle, entity_id)Release an entity back to the pool
goud_entity_pool_stats(handle, out)Query diagnostic counters

Stats

PoolStats tracks:

  • capacity – total slots
  • active – slots currently in use
  • available – slots ready for acquisition
  • high_water_mark – peak simultaneous active slots
  • total_acquires / total_releases – cumulative operation counts

When to Use

  • Bullet pools, particle systems, or any pattern where entities are created and destroyed frequently
  • Scenarios where allocation jitter matters (e.g., frame-budget-sensitive gameplay)

FrameArena

A bump allocator for per-frame temporary data. All allocations are freed in bulk with a single reset() call.

How It Works

  1. Allocate values with alloc(val) – returns a mutable reference. Each allocation is an O(1) pointer bump.
  2. Call reset() once per frame (typically at the start of the update loop). This frees every allocation at once with no per-object teardown.

A single global arena is shared per process (thread-safe via mutex).

FFI Surface

FunctionDescription
goud_frame_arena_reset()Free all allocations in one call
goud_frame_arena_stats(out)Query diagnostic counters

Stats

ArenaStats tracks:

  • bytes_allocated – currently in use
  • bytes_capacity – total backing storage
  • reset_count – number of resets since creation

When to Use

  • Per-frame scratch data (collision pairs, render commands, temporary buffers)
  • Any allocation pattern where all data has the same short lifetime

Performance Characteristics

OperationEntityPoolFrameArena
AllocateO(1) free-list popO(1) pointer bump
FreeO(1) free-list pushO(1) bulk reset (all at once)
MemoryPre-allocated at creationGrows as needed, reuses on reset
Thread safetyPer-pool (no global lock)Global mutex

Example: Bullet Pool (C#)

// Create a pool of 200 bullet slots at startup
var pool = game.CreateEntityPool(200);

// In the game loop -- acquire when firing
ulong bullet = game.EntityPoolAcquire(pool);

// Release when the bullet goes off-screen
game.EntityPoolRelease(pool, bullet);

// Query stats for debugging
var stats = game.GetEntityPoolStats(pool);
Console.WriteLine($"Active: {stats.Active}/{stats.Capacity}");

Example: Frame Arena Reset

The frame arena resets automatically each frame when using GoudGame. For manual control via GoudContext:

// At the start of each frame
game.ResetFrameArena();

// Query arena usage
var stats = game.GetFrameArenaStats();
Console.WriteLine($"Arena: {stats.BytesAllocated}/{stats.BytesCapacity}");

Spatial Grid

Cell-based spatial partitioning for fast neighbor queries. The SpatialGrid divides 2D space into uniform cells so that proximity queries run in O(1) relative to total entity count.

When to Use

Use a spatial grid when you need to find nearby entities efficiently:

  • Collision broad-phase (reduce pair checks from O(n^2) to O(n))
  • Area-of-effect abilities (find targets within a radius)
  • AI perception (detect entities in a sensory range)
  • Proximity triggers (activate when a player approaches)

Creating a Grid

Create a grid by specifying a cell size in world units. Entities that fall within the same cell are grouped together, so choose a cell size close to your typical query radius.

FFI FunctionDescription
goud_spatial_grid_create(cell_size)Create a grid with the given cell size
goud_spatial_grid_create_with_capacity(cell_size, capacity)Create a grid with pre-allocated storage
goud_spatial_grid_destroy(handle)Destroy a grid and free resources

The cell_size must be positive and finite. A good starting value is the diameter of your most common query radius.

create_with_capacity pre-allocates internal storage for the expected number of entities, reducing allocations during gameplay.

Inserting and Removing Entities

FFI FunctionDescription
goud_spatial_grid_insert(handle, entity_id, x, y)Insert or move an entity to a position
goud_spatial_grid_remove(handle, entity_id)Remove an entity from the grid
goud_spatial_grid_update(handle, entity_id, x, y)Update an entity’s position
goud_spatial_grid_clear(handle)Remove all entities without destroying the grid

Insert is idempotent: if the entity already exists in the grid, it is moved to the new position. Remove is also idempotent: removing a nonexistent entity is a no-op.

Update returns an error if the entity was not previously inserted. Use insert for entities that may or may not already be tracked.

Querying by Radius

goud_spatial_grid_query_radius(handle, center_x, center_y, radius, out_entities, capacity) -> i32

Writes matching entity IDs into the caller-provided buffer. The return value is:

  • Non-negative: total number of entities found (may exceed capacity)
  • Negative: error code

If the buffer is too small, only capacity entities are written, but the full count is still returned. This allows a two-pass pattern: call once with capacity = 0 to get the count, allocate, then call again.

Counting Entities

goud_spatial_grid_entity_count(handle) -> i32

Returns the number of entities currently in the grid, or a negative error code.

Choosing Cell Size

ScenarioSuggested Cell Size
Small query radius (< 50 units)1x to 2x the query radius
Large open worldLarger cells (100-500 units) to reduce memory
Dense clustersSmaller cells for finer granularity

A cell size that is too small wastes memory on empty cells. A cell size that is too large puts too many entities in each cell, reducing the benefit of spatial partitioning.

Error Handling

All functions return error codes on failure. Common error conditions:

  • Invalid grid handle (destroyed or never created)
  • Cell size not positive or not finite
  • Handle space exhausted (too many concurrent grids)
  • Entity not found (for update only)

Call goud_last_error_message() after any negative return value for a human-readable error string.

FFI

Spatial grid FFI functions are in goud_engine/src/ffi/spatial_grid/. The module is split into:

  • lifecycle.rs – create, destroy, clear
  • operations.rs – insert, remove, update
  • queries.rs – query_radius, entity_count

Texture Atlas

Runtime bin-packing of multiple textures into a single GPU texture. Combining many small textures into one atlas reduces texture bind calls during rendering, which is one of the most common GPU bottlenecks.

When to Use

  • Sprite sheets assembled at runtime from individual images
  • UI elements packed into a single texture for efficient rendering
  • Tile sets loaded from separate files but rendered as a batch
  • Any scenario where many small textures cause excessive GPU state changes

Lifecycle

1. Create an Atlas

goud_atlas_create(context_id, category, max_width, max_height) -> GoudAtlasHandle
ParameterDescription
context_idEngine context handle
categoryNull-terminated C string naming the atlas (e.g., “sprites”, “ui”)
max_widthMaximum atlas width in pixels (0 = default 2048, max 8192)
max_heightMaximum atlas height in pixels (0 = default 2048, max 8192)

Returns a valid atlas handle, or GOUD_INVALID_ATLAS on failure.

2. Add Textures

Two methods for adding image data:

FFI FunctionDescription
goud_atlas_add_from_file(ctx, atlas, key, path)Load an image file and pack it
goud_atlas_add_pixels(ctx, atlas, key, pixels, width, height)Pack raw RGBA8 pixel data

Each entry is identified by a string key used for later lookup. The atlas performs bin-packing to place each image in an available region.

goud_atlas_add_texture is reserved and currently returns an error (GPU pixel readback is not supported).

3. Finalize (Upload to GPU)

goud_atlas_finalize(context_id, atlas) -> GoudTextureHandle

Uploads the packed atlas to the GPU and returns a texture handle. After finalization, the atlas can be used for rendering. You must finalize before drawing.

4. Query Entries

goud_atlas_get_entry(context_id, atlas, key, out_entry) -> bool

Writes the UV coordinates and pixel placement for a packed texture into an FfiAtlasEntry:

FieldTypeDescription
u_min, v_minf32Top-left UV coordinates
u_max, v_maxf32Bottom-right UV coordinates
pixel_x, pixel_yu32Pixel offset within the atlas
pixel_w, pixel_hu32Pixel dimensions of the entry

Use the UV coordinates when rendering sprites from the atlas texture.

5. Query Stats

goud_atlas_get_stats(context_id, atlas, out_stats) -> bool

Returns packing statistics via FfiAtlasStats:

FieldTypeDescription
texture_countu32Number of packed textures
width, heightu32Atlas dimensions
used_pixelsu32Pixels occupied by packed textures
total_pixelsu32Total atlas area
efficiencyf32Packing efficiency (0.0 to 1.0)
wasted_pixelsu32Unused pixels

6. Get the GPU Texture

goud_atlas_get_texture(context_id, atlas) -> GoudTextureHandle

Returns the GPU texture handle after finalization. Returns GOUD_INVALID_TEXTURE if the atlas has not been finalized.

7. Destroy

goud_atlas_destroy(context_id, atlas) -> bool

Frees both CPU and GPU resources for the atlas.

Error Handling

All functions set the last error on failure. Common conditions:

  • Invalid context or atlas handle
  • Null pointer for string parameters
  • Atlas dimensions exceeding 8192
  • Entry key not found
  • Atlas not finalized when querying GPU texture

Call goud_last_error_message() for details.

FFI

Atlas FFI functions are in goud_engine/src/ffi/renderer/atlas/. The #[repr(C)] structs FfiAtlasEntry and FfiAtlasStats are defined in the atlas module.

Batching

SpriteBatch and TextBatch reduce GPU overhead by combining many draw calls into a single batched pass. Instead of issuing one draw call per sprite or text label, the batch renderer groups commands by texture and draws all sprites sharing a texture in one GPU call.

Why Batching Matters

Each individual draw call has CPU overhead: binding textures, uploading vertex data, and issuing GPU commands. In a typical 2D game with hundreds of sprites, unbatched rendering can become CPU-bound from draw call overhead alone.

Batching solves this by:

  1. Sorting sprites by z-layer then by texture
  2. Building a single vertex buffer for all sprites
  3. Issuing one draw call per texture group

A scene with 500 sprites using 10 textures drops from 500 draw calls to 10.

SpriteBatch

FfiSpriteCmd

Each sprite in the batch is described by an FfiSpriteCmd struct (#[repr(C)], 72 bytes):

FieldTypeDescription
textureu64Texture handle from goud_texture_load
x, yf32Position in screen-space pixels
width, heightf32Sprite dimensions on screen
rotationf32Rotation in radians
src_x, src_yf32Source rectangle offset in pixel coordinates
src_w, src_hf32Source rectangle size (0,0 = full texture)
r, g, b, af32Color tint and opacity
z_layeri32Depth sorting (lower values drawn first)

Drawing

goud_renderer_draw_sprite_batch(context_id, cmds, count) -> u32
ParameterDescription
context_idEngine context handle
cmdsPointer to an array of FfiSpriteCmd
countNumber of commands in the array

Returns the number of sprites drawn (0 on error).

The renderer:

  1. Sorts commands by z_layer, then by texture
  2. Builds rotated quad vertices with UV mapping
  3. Groups consecutive sprites with the same texture into GPU batches
  4. Draws each batch with a single indexed draw call

Source-rect fields (src_x, src_y, src_w, src_h) are in pixel coordinates. The renderer converts them to UV coordinates automatically. When src_w and src_h are both 0, the full texture is used.

TextBatch

FfiTextCmd

Each text label is described by an FfiTextCmd struct (#[repr(C)], 56 bytes):

FieldTypeDescription
font_handleu64Font handle from goud_font_load
text*const c_charNull-terminated UTF-8 string
x, yf32Position in screen-space pixels
font_sizef32Font size in pixels
alignmentu80=Left, 1=Center, 2=Right
directionu80=Auto, 1=LTR, 2=RTL
max_widthf32Maximum line width (0 = no wrap)
line_spacingf32Line spacing multiplier (default 1.0)
r, g, b, af32Text color and opacity

Drawing

goud_renderer_draw_text_batch(context_id, cmds, count) -> u32

Returns the number of text labels drawn (0 on error).

Each command reuses the glyph atlas cached by (font_handle, font_size), so repeated labels with the same font and size avoid redundant atlas rebuilds. Commands with null text pointers, empty strings, or invalid font sizes are silently skipped.

Performance Expectations

ScenarioWithout BatchingWith Batching
100 sprites, 5 textures100 draw calls5 draw calls
500 sprites, 10 textures500 draw calls10 draw calls
1000 sprites, 1 texture1000 draw calls1 draw call

GPU buffer management is handled automatically:

  • Vertex and index buffers are created lazily on first use
  • Buffers grow dynamically when the sprite count exceeds current capacity
  • Old buffers are destroyed only after the replacement is allocated

Integration with Debugger

Both batch renderers report statistics to the runtime debugger:

  • Sprites/text labels drawn
  • Triangle count
  • Number of GPU batches
  • Draw calls issued

These metrics appear in the debugger overlay when the runtime debugger is active.

FFI

Sprite batch FFI is in goud_engine/src/ffi/renderer/draw/batch.rs. Text batch FFI is in goud_engine/src/ffi/renderer/text/batch.rs.

Build Your First Game

This beginner guide walks from zero to a playable Flappy-style loop.

No prior GoudEngine knowledge is assumed. Use one of these tracks:

  • C# track: examples/csharp/flappy_goud/
  • Python track: examples/python/flappy_bird.py

What you will build

By the end of this guide, your game can:

  1. Open a window and run a frame loop.
  2. Draw a player sprite.
  3. Handle jump input.
  4. Move one obstacle lane.
  5. Detect collision and reset.

Step 0: Run the reference project first

Run the final reference before writing code.

./dev.sh --game flappy_goud
./dev.sh --sdk python --game flappy_bird

If this fails, fix setup first using:

Step 1: Create a minimal frame loop

Goal: open a window and render empty frames.

Verification:

  • Window opens.
  • Escape closes (desktop).
  • Frame timing (dt) updates each frame.

Reference files:

  • C#: examples/csharp/flappy_goud/Program.cs
  • Python: examples/python/flappy_bird.py

Step 2: Add a player sprite and gravity

Track per-frame state:

  • x, y
  • vertical velocity vy
  • gravity constant
  • jump impulse

Update loop:

  • vy += gravity * dt
  • y += vy * dt

Verification:

  • Player falls without input.
  • Jump input applies one upward impulse.
  • Player stays in visible bounds (clamp or reset).

Step 3: Add input

Map one-shot jump input:

  • C#: key press from input API
  • Python: same behavior through Python wrapper

Verification:

  • Pressing jump changes vy once per intent.
  • Holding jump does not spam if your design expects one-shot input.

Step 4: Add one pipe lane

Represent one lane with:

  • lane x
  • gap_y
  • gap_height

Per frame:

  • move lane left
  • when off-screen, reset to the right and randomize gap_y

Verification:

  • Pipe lane scrolls smoothly.
  • Reset logic reuses the lane without crashes.

Step 5: Add collision + restart

Check AABB overlap between:

  • player and top pipe
  • player and bottom pipe
  • player and floor/ceiling

On collision:

  • reset player state
  • reset pipe state
  • reset score

Verification:

  • Collision triggers a full reset.
  • New run starts in a clean state.

C# beginner variant

Use this order:

  1. Getting Started — C#
  2. Build the five steps above.
  3. Compare to examples/csharp/flappy_goud/ only when stuck.

Run command:

./dev.sh --game flappy_goud

Python beginner variant

Use this order:

  1. Getting Started — Python
  2. Build the five steps above.
  3. Compare to examples/python/flappy_bird.py only when stuck.

Run command:

./dev.sh --sdk python --game flappy_bird

Downloadable final project handoff

The hosted docs now ship generated bundles for the final reference projects:

Canonical source locations in this repository:

  • examples/csharp/flappy_goud/
  • examples/python/flappy_bird.py
  • examples/typescript/flappy_bird/
  • examples/rust/flappy_bird/

To refresh the downloadable bundles locally:

PATH="$HOME/.cargo/bin:$HOME/.dotnet/tools:$PATH" bash scripts/clean-room-regenerate.sh --docs

Next step

After this tutorial, run the dedicated Sandbox Guide and the generated Example Showcase to cover more of the SDK surface. Keep Feature Lab as the supplemental smoke path.

Debugger Runtime

The shared debugger runtime is for local desktop and headless development flows.

What is in scope:

  • Native Rust, C#, Python, and TypeScript Node targets
  • Route-scoped attach over local IPC
  • Pause, resume, frame stepping, tick stepping, time scale, debug-draw toggle, and input injection
  • Frame capture, replay recording, replay playback, and metrics trace export
  • The out-of-process goudengine-mcp bridge

What is out of scope in this batch:

  • Remote attach
  • Browser or WASM debugger runtime support
  • Engine-wide determinism claims

goudengine/web exposes explicit unsupported errors for the new debugger runtime methods.

Enable It Before Startup

Enable debugger mode in config before creating the game or context. The runtime publishes one local manifest per process when publish_local_attach is enabled and at least one route is attachable.

Shipped config surfaces:

  • C# headless: new GoudContext(new ContextConfig(new DebuggerConfig(true, true, "...")))
  • Python headless: GoudContext(ContextConfig(debugger=DebuggerConfig(...)))
  • Rust headless: Context::create_with_config(ContextConfig { debugger: ... })
  • TypeScript desktop: use ./dev.sh --sdk typescript --game feature_lab as the shipped reference route in this batch

The runtime is Rust-owned. SDKs only forward control calls and return raw JSON or byte envelopes.

Control and Debug Draw

The public control plane uses the same route-scoped contract across FFI, SDKs, and MCP:

  • pause or resume
  • step by frame or tick
  • set time scale
  • toggle runtime-owned debug draw
  • inject keyboard, mouse button, mouse position, and scroll events

Debug draw is also runtime-owned. The enabled path supports 2D and 3D primitives, color, layering, and optional lifetime. When debugger mode is off, the runtime keeps the disabled path close to zero cost.

Capture Artifacts

captureDebuggerFrame() returns a DebuggerCapture envelope:

  • imagePng
  • metadataJson
  • snapshotJson
  • metricsTraceJson

Capture is on-demand. The renderer readback stays inside the graphics backend, and unsupported routes report a clean failure instead of a partial artifact.

Replay Artifacts and Determinism Limits

stopDebuggerRecording() returns a DebuggerReplayArtifact envelope:

  • manifestJson
  • data

Replay records normalized input, timing, and available runtime facts, then feeds playback through the same debugger control path.

Replay does not promise full engine determinism. Expect differences when behavior depends on:

  • wall-clock time
  • external network traffic or services
  • platform or driver behavior
  • providers that do not expose every source of nondeterminism

Treat replay as a debugger aid for desktop and headless development, not as a strict simulation proof.

Metrics and Trace Export

getDebuggerMetricsTraceJson() exports versioned JSON from a bounded runtime buffer. The trace includes:

  • frame timing
  • per-system timing
  • render counters
  • memory summaries
  • network and service-health state
  • debugger events

The runtime keeps 300 frames of history per route so export cost stays predictable.

MCP Bridge Workflow

goudengine-mcp is a separate local process. It reads manifests, attaches to one route over local IPC, and exposes MCP tools, prompt bundles, SDK knowledge resources, and artifact resources.

Start it from the workspace root:

cargo run -p goudengine-mcp

Feature Lab Reference Routes

The owned Feature Lab examples are the reference rollout for local attach smoke coverage. Each one enables debugger mode and prints the same attach workflow locally. The C#, Python, and TypeScript desktop paths also exercise raw manifest/snapshot access through the SDK helpers they already expose.

SDKTargetRunStable route label
C#Headless./dev.sh --game feature_labfeature-lab-csharp-headless
PythonHeadlesspython3 examples/python/feature_lab.pyfeature-lab-python-headless
RustHeadlesscargo run -p feature-labfeature-lab-rust-headless
TypeScriptDesktop./dev.sh --sdk typescript --game feature_labfeature-lab-typescript-desktop

feature_lab_web stays in the repo for browser/WASM smoke coverage only. It does not publish a debugger route.

Typical workflow:

  1. Call goudengine.list_contexts.
  2. Call goudengine.attach_context.
  3. Use snapshot, control, capture, replay, and metrics tools against that route.
  4. Use prompt bundles such as:
    • goudengine.safe_attach
    • goudengine.inspect_runtime
    • goudengine.troubleshoot_attach
  5. Read knowledge resources such as:
    • goudengine://knowledge/sdk-contract
    • goudengine://knowledge/mcp-workflow
    • goudengine://knowledge/sdk-rust
    • goudengine://knowledge/sdk-csharp
    • goudengine://knowledge/sdk-python
    • goudengine://knowledge/sdk-typescript-desktop
  6. Read stored artifacts through:
    • goudengine://capture/{id}
    • goudengine://metrics/{id}
    • goudengine://recording/{id}

The bridge does not run inside the game process and does not switch routes globally. Each attach session is bound to one route.

Sandbox Guide

Sandbox is the shared parity target for Alpha-001 on this branch.

  • Flappy Bird stays the onboarding baseline.
  • Sandbox is the cross-language feature tour under active recovery.
  • Feature Lab stays in the repo as supplemental smoke coverage.

Run it

./dev.sh --game sandbox
./dev.sh --sdk python --game sandbox
./dev.sh --sdk typescript --game sandbox
./dev.sh --sdk typescript --game sandbox_web
cargo run -p sandbox

Branch status

This branch keeps the recovery ledger in .claude/specs/alpha-001-sandbox-execution.csv. Use that file for live branch truth while the remaining runtime rows are being closed.

Current state:

  • C#, Python, and TypeScript load the shared asset pack and the three-panel HUD.
  • Rust uses the same asset pack and now calls GoudGame::draw_text(...) for HUD copy.
  • The remaining branch recovery work is tracked in the execution CSV row set, not in per-runtime handwritten notes here.

What to expect

Every runtime uses the shared asset pack and the same three-panel layout:

  • Overview panel in the top-left
  • Live status panel in the top-right
  • Try-this-next panel along the bottom

Rust status in this branch:

  • Rust uses the shared manifest, panel layout, and GoudGame::draw_text(...) path.
  • Rust desktop scene visibility and HUD text are now back on the same public SDK path used by the example.

The desktop targets also share the same native packet contract for peer sync:

sandbox|v1|<role>|<mode>|<x>|<y>|<label>

Recovered runs should let you:

  • Press 1, 2, or 3 to switch between 2D, 3D, and Hybrid
  • Move the local bird with WASD or the arrow keys
  • See the mouse marker update live
  • Press SPACE to activate audio and play the shared tone
  • Open a second native sandbox instance and watch a peer bird appear

Use the execution CSV for any runtime-specific exceptions that are still open on this branch. As of the current recovery pass, the shared HUD/text path is back in Rust, while some native 3D scene-flow verification rows remain open.

Panel meanings

  • Overview: shared intent, controls, and what the example is proving
  • Live status: current scene, runtime capabilities, input state, and networking detail
  • Try this next: next checks to perform, audio status, and peer-sync hints

Networking check

For native runtimes, open two local instances on the same machine. One should report host and waiting until the second joins. After the second joins, both should show peer discovery and render a remote bird label.

Useful env vars for smoke and CI:

GOUD_SANDBOX_NETWORK_PORT=38491
GOUD_SANDBOX_NETWORK_ROLE=auto   # or host / client
GOUD_SANDBOX_EXIT_ON_PEER=1
GOUD_SANDBOX_EXPECT_PEER=1

Web limitations

The web target keeps the same HUD structure and shared copy, but it does not fake unsupported desktop behavior.

  • Networking stays visibly capability-gated in browser mode.
  • Renderer fallback is called out explicitly when the runtime does not expose a usable WebGPU adapter.

Use Web Platform Gotchas for browser-specific troubleshooting and Example Showcase for the generated run matrix.

Cross-Platform Deployment

This guide covers shipping GoudEngine projects for macOS, Linux, Windows, and Web.

Supported deployment targets

  • macOS (osx-x64, osx-arm64)
  • Linux (linux-x64)
  • Windows (win-x64)
  • Web (TypeScript + WASM)

Toolchain requirements by platform

macOS

  • Rust toolchain
  • .NET SDK 8+
  • Python 3.11+
  • Node.js 20+

Gotcha:

  • On Apple Silicon, verify both osx-x64 and osx-arm64 runtime payloads.
file sdks/csharp/runtimes/osx-x64/native/libgoud_engine.dylib
file sdks/csharp/runtimes/osx-arm64/native/libgoud_engine.dylib

Linux

  • Rust toolchain
  • Python 3.11+
  • Node.js 20+
  • GLFW/OpenGL runtime packages

Gotcha:

  • Native runtime failures are usually missing system GL dependencies.

Windows

  • Rust toolchain
  • .NET SDK 8+
  • Python 3.11+
  • Node.js 20+

Gotcha:

  • Confirm native DLL placement in RID output folders before packaging.

Web (TypeScript WASM)

  • Node.js 20+
  • Browser serving over HTTP

Gotchas:

  • Do not run from file://.
  • Publish loader JS and .wasm together.

Local release build flow

./build.sh --release
./package.sh --local
./dev.sh --game flappy_goud --local

This validates a local release package install path before CI publishing.

CI/CD deployment example (GitHub Actions)

This is a concrete workflow shape for build + artifact publish.

name: deploy-matrix

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: cargo check
      - run: cargo test --workspace --quiet
      - run: python3 sdks/python/test_bindings.py
      - run: dotnet test sdks/csharp.tests/GoudEngine.Tests.csproj -v minimal
      - run: cd sdks/typescript && npm ci && npm test
      - run: bash scripts/check-generated-artifacts.sh
      - run: PATH="$HOME/.cargo/bin:$HOME/.dotnet/tools:$PATH" bash scripts/clean-room-regenerate.sh --docs

  package-desktop:
    needs: verify
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            artifact: linux-x64
          - os: macos-latest
            artifact: macos-universal
          - os: windows-latest
            artifact: win-x64
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: ./build.sh --release
      - uses: actions/upload-artifact@v4
        with:
          name: goudengine-${{ matrix.artifact }}
          path: |
            target/release
            sdks/csharp/runtimes
            sdks/nuget_package_output

  package-web:
    needs: verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: cd sdks/typescript && npm ci && npm run build:web
      - uses: actions/upload-artifact@v4
        with:
          name: goudengine-web
          path: |
            sdks/typescript/dist/web
            examples/typescript/flappy_bird/web
            examples/typescript/sandbox/web

For this repository, canonical release automation remains:

  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .github/workflows/docs.yml

Use the example workflow above as the portable template, and treat the checked-in workflow files as the source of truth for this repository’s actual release pipeline.

WASM deployment checklist

  • Host goud_engine_bg.wasm and generated JS in the same release payload.
  • Serve all assets over HTTP(S).
  • Keep import-map paths stable across local and deployed environments.
  • Ship the public Sandbox web directory alongside Flappy Bird if you want a full feature tour in browser-safe mode.
  • Use Sandbox Guide as the source of truth for the web HUD, capability-gating copy, and parity checks.

Pre-release checklist

  • cargo check
  • cargo test --workspace --quiet
  • python3 sdks/python/test_bindings.py
  • dotnet test sdks/csharp.tests/GoudEngine.Tests.csproj -v minimal
  • (cd sdks/typescript && npm test)
  • bash scripts/check-generated-artifacts.sh
  • PATH="$HOME/.cargo/bin:$HOME/.dotnet/tools:$PATH" bash scripts/clean-room-regenerate.sh --docs

Do not publish if any item fails.

Console Porting Guide

This guide covers the public side of a console port. It assumes you are an NDA partner building the platform layer in a private repo. The public repository provides the engine core, the generated C header, and the trait contracts you need to plug in your own renderer.

Scope

This document covers:

  • downloading the static partner artifact
  • linking the static library into a console build
  • wiring platform hooks into the engine
  • keeping the public and NDA-only boundaries clean

This document does not cover console SDK setup, certification tooling, or private graphics API calls.

Get the Partner Artifact

Each release publishes console-partner archives alongside the normal desktop tarballs:

  • goud-engine-console-v<version>-linux-x64.tar.gz
  • goud-engine-console-v<version>-osx-x64.tar.gz
  • goud-engine-console-v<version>-osx-arm64.tar.gz
  • goud-engine-console-v<version>-win-x64.tar.gz

Extract the archive for the host machine you build on. The layout is fixed:

PathLinux/macOSWindows
lib/lib/libgoud_engine.alib/goud_engine.lib
include/include/goud_engine.hinclude/goud_engine.h

The archive is minimal on purpose. It contains the static engine library and the generated public header, nothing else.

Point your console build at the extracted include/ directory and link the static library from lib/.

Generic example:

INCLUDE_DIR=/path/to/goud-engine-console-v<version>-osx-arm64/include
LIB_DIR=/path/to/goud-engine-console-v<version>-osx-arm64/lib
  • Add INCLUDE_DIR to the compiler include path.
  • Add the library file from LIB_DIR to the linker inputs.
  • Keep any platform SDK libraries in your private build scripts.

If your host toolchain is Windows, link goud_engine.lib. On Linux and macOS, link libgoud_engine.a.

Public Integration Points

The public repo already gives you three stable seams:

  1. The generated C ABI in goud_engine.h
  2. The provider traits in Rust, especially RenderProvider
  3. The platform abstractions used by the engine runtime

Most console ports need a thin private crate or module that does four jobs:

  • creates the platform window or surface
  • owns the swap chain and command submission path
  • implements RenderProvider
  • feeds platform events into the engine loop

For the renderer contract, use Console Render Backend Contract.

Porting Checklist

Rendering

  • Implement RenderProvider in a private backend crate.
  • Translate engine descriptors into platform pipeline, buffer, texture, and render-target objects.
  • Keep presentation inside end_frame().
  • Handle resize and lost-surface recovery in resize() and your private backend state.

Platform hooks

  • Create the console window, surface, or presentation target before ProviderLifecycle::init().
  • Feed platform input through the engine’s input path instead of bypassing it.
  • Keep thread-affinity rules in the private layer if the SDK requires them.

Build and packaging

  • Keep the public header untouched. Regenerate it from Rust when the public ABI changes.
  • Treat the static library and header as a matched pair from the same release.
  • Version your private integration layer separately from the public engine release if needed.

Certification Watch List

These are common review areas. The details vary by platform, but the categories do not.

Memory

  • avoid frame-to-frame leaks in transient GPU resources
  • release buffers, textures, and render targets in a predictable place
  • document any private allocator or pool usage that must be sized per title

Threading

  • keep SDK calls on the threads required by the platform
  • do not move window or presentation ownership between threads unless the SDK says it is safe
  • make fence and queue shutdown deterministic

Audio

  • verify suspend and resume behavior
  • verify sample-rate and channel-layout expectations for the platform
  • keep audio teardown separate from graphics teardown so one failure does not mask the other

Public and private boundary

  • keep NDA headers, libraries, and build files out of the public repo
  • do not add private handles or SDK structs to public FFI signatures
  • document platform assumptions in your private integration repo, not here

What Stays Out of the Public Repo

Keep these items private:

  • console SDK headers and libs
  • device and swap-chain wrapper code
  • certification scripts and checklists with platform-specific detail
  • performance captures, debug markers, and SDK validation layers

The public repo should stay limited to trait contracts, the generated C header, and release artifacts that are safe to publish.

Suggested Bring-Up Order

  1. Link the static library and confirm the title boots.
  2. Stand up a RenderProvider that can clear the screen and present.
  3. Add buffer, texture, and shader creation.
  4. Add sprite draws, then text, then mesh and particle paths.
  5. Fill in diagnostics after the frame loop is stable.

Web Platform Gotchas

Browser builds are close to the desktop TypeScript API, but they are not identical in behavior. This page collects the current rough edges so developers do not rediscover them by trial and error.

Keep game.run() synchronous

The Web target uses wasm-bindgen plus internal RefCell state. Do not await inside the game.run() callback and do not hand it an async function.

Bad:

game.run(async (_dt) => {
  await loadLevel();
});

Good:

const levelPromise = loadLevel();

game.run((_dt) => {
  // poll or react to already-started async work here
});

If you need async setup, do it before game.run() starts.

Key state is frame-based

Use isKeyPressed(...) and isMouseButtonPressed(...) from inside the frame loop. Do not assume browser DOM events map one-to-one with engine input state outside the loop.

Recommended pattern:

game.run((_dt) => {
  if (game.isKeyPressed(32)) {
    // Space
  }
});

This avoids the stale-key-state bugs that motivated the earlier WASM input fixes.

Asset paths resolve through the page origin

Web asset loading goes through fetch(). Relative paths are resolved from the page URL, not from the TypeScript source file.

Recommended:

const texture = await game.loadTexture("/assets/player.png");

or serve your page and assets from a directory layout where the relative URLs are obvious and stable.

Avoid opening the page with file://; use a real HTTP server such as npx serve ..

Mid-loop asset loading still has visible cost

Texture and font loading on the web path can stall or hitch if you start large loads after the frame loop is already running. The safest pattern is:

  1. Create the game.
  2. Call await game.preload([...]) for the path-based textures/fonts you know you need.
  3. Start game.run().

Example:

await game.preload(
  [
    '/assets/background-day.png',
    '/assets/pipe-green.png',
    '/assets/flappy.ttf',
  ],
  {
    onProgress(update) {
      console.log(update.progress);
    },
  },
);

Current limitation:

  • The generated preloader currently covers the TypeScript SDK’s path-based texture/font loaders.
  • It is not a generic preload system for every possible asset class yet.

Web networking is client-only

goudengine/web now supports browser WebSocket client connections.

Current state:

  • goudengine/node: host + client workflows, loopback/headless tests
  • goudengine/web: browser WebSocket client connections

Current limitation:

  • Browser hosting is not supported.
  • Use NetworkProtocol.WebSocket on the web target.
  • connect() returns before the socket is fully open, so wait for peerCount() > 0 before sending your first packet.

Recommended pattern:

const endpoint = new NetworkManager(game).connect(
  NetworkProtocol.WebSocket,
  "ws://127.0.0.1:9001",
  9001,
);

game.run(() => {
  endpoint.poll();
  if (endpoint.peerCount() > 0) {
    endpoint.send(new TextEncoder().encode("ping"));
  }
});

Touch maps to primary mouse input

The web backend maps touch input to mouse button 0. That is enough for the current examples, but multi-touch gestures are not exposed as a richer engine input model yet.

Use these before debugging your own browser build:

./dev.sh --sdk typescript --game flappy_bird_web
./dev.sh --sdk typescript --game sandbox_web

If both run cleanly, your environment, asset serving, and WASM packaging path are probably fine.

For the expected Sandbox HUD, networking copy, and browser-safe limitations, use Sandbox Guide.

FAQ and Troubleshooting

This page tracks common problems and fixes. It is organized by category and currently covers more than 10 recurring build, runtime, SDK, graphics, platform, and tutorial issues. To add another entry, open a PR against this file using the contribution format below.

Contribution format for new FAQ entries:

### [Category] Short failure description

Symptoms:
- ...

Cause:
- ...

Fix:
1. ...
2. ...

Verification:
- command or observable result

Build issues

[Build] cargo check fails after pulling latest changes

Symptoms:

  • New compile errors in generated SDK files.

Cause:

  • Generated artifacts are out of sync.

Fix:

  1. Run ./codegen.sh
  2. Run bash scripts/check-generated-artifacts.sh

Verification:

  • cargo check completes without generated-file drift errors.

[Build] scripts/check-generated-artifacts.sh fails

Symptoms:

  • Missing generated files or diff output under generated directories.

Cause:

  • Generated outputs were not refreshed.

Fix:

  1. Run ./codegen.sh
  2. Run python3 scripts/generate-doc-snippets.py
  3. Run cd sdks/typescript && node scripts/generate-doc-media.mjs
  4. Run python3 scripts/generate-showcase-docs.py
  5. Re-run bash scripts/check-generated-artifacts.sh

Verification:

  • Script prints Generated artifact check passed.

[Build] mdbook build fails

Symptoms:

  • Include path errors or broken links.

Cause:

  • Missing generated snippet/docs outputs or stale links.

Fix:

  1. Run cd sdks/typescript && node scripts/generate-doc-media.mjs
  2. Run python3 scripts/generate-doc-snippets.py
  3. Run python3 scripts/generate-showcase-docs.py
  4. Run mdbook build

Verification:

  • docs/book/ rebuilds with no fatal errors.

Runtime issues

[Runtime] goudengine.list_contexts returns no attachable routes

Symptoms:

  • cargo run -p goudengine-mcp starts, but goudengine.list_contexts returns an empty list.

Cause:

  • The target process was started without debugger mode enabled, or you launched a browser/WASM target that does not publish debugger routes in this batch.

Fix:

  1. Start a desktop or headless process with debugger mode enabled before creation.
  2. Use one of the shipped Feature Lab examples if you want a known-good route:
    • ./dev.sh --game feature_lab
    • python3 examples/python/feature_lab.py
    • cargo run -p feature-lab
    • ./dev.sh --sdk typescript --game feature_lab
  3. Re-run cargo run -p goudengine-mcp, then call goudengine.list_contexts and goudengine.attach_context.

Verification:

  • goudengine.list_contexts shows one of the stable Feature Lab labels such as feature-lab-csharp-headless, feature-lab-python-headless, feature-lab-rust-headless, or feature-lab-typescript-desktop.

[Runtime] Python bindings fail with symbol/load errors

Symptoms:

  • Import error or unresolved symbol when running Python examples.

Cause:

  • Loader found old native library.

Fix:

  1. cargo build --release
  2. GOUD_ENGINE_LIB=$PWD/target/release PYTHONPATH=$PWD/sdks/python python3 sdks/python/test_bindings.py

Verification:

  • Binding tests pass and examples start.

[Runtime] C# tests fail on Apple Silicon architecture mismatch

Symptoms:

  • Runtime load/test failures under x64 test paths on Apple Silicon.

Cause:

  • Host/runtime architecture mismatch.

Fix:

  1. Verify binaries:
    • file sdks/csharp/runtimes/osx-x64/native/libgoud_engine.dylib
    • file sdks/csharp/runtimes/osx-arm64/native/libgoud_engine.dylib
  2. Run x64-hosted tests:
    • DOTNET_ROOT_X64=/usr/local/share/dotnet/x64 /usr/local/share/dotnet/x64/dotnet test sdks/csharp.tests/GoudEngine.Tests.csproj -v minimal

Verification:

  • Tests pass on explicit x64 host path.

[Runtime] TypeScript web example starts but renders blank

Symptoms:

  • Page opens with no gameplay output.

Cause:

  • Wrong import-map/asset path or running from file://.

Fix:

  1. Use repo command: ./dev.sh --sdk typescript --game flappy_bird_web
  2. Confirm page is served by HTTP URL printed by dev.sh.

Verification:

  • Game renders and input responds.

SDK and codegen issues

[SDK] python3 codegen/validate_coverage.py fails

Symptoms:

  • Mapping coverage errors for FFI exports.

Cause:

  • Rust exports changed without codegen mapping updates.

Fix:

  1. Build once to refresh manifest.
  2. Run python3 codegen/validate.py
  3. Update codegen/goud_sdk.schema.json and/or codegen/ffi_mapping.json
  4. Run ./codegen.sh

Verification:

  • Both validation commands pass.

[SDK] TypeScript tests fail after codegen changes

Symptoms:

  • Missing generated type exports or runtime wrapper mismatches.

Cause:

  • Node/web generated entrypoints drifted.

Fix:

  1. Run ./codegen.sh
  2. Run cd sdks/typescript && npm test

Verification:

  • Test suite is green with regenerated outputs.

[SDK] Clean-room regenerate fails

Symptoms:

  • scripts/clean-room-regenerate.sh fails after deleting generated files.

Cause:

  • Regeneration pipeline no longer rebuilds from source truth.

Fix:

  1. Run bash scripts/clean-room-regenerate.sh
  2. If docs are required, run:
    • PATH="$HOME/.cargo/bin:$HOME/.dotnet/tools:$PATH" bash scripts/clean-room-regenerate.sh --docs
  3. Fix any step that fails before release.

Verification:

  • Script completes without manual file restoration.

Graphics and platform issues

[Graphics] OpenGL context errors in CI/headless runs

Symptoms:

  • Graphics tests fail in environments without display/GL context.

Cause:

  • Tests requiring GL context run in unsupported environment.

Fix:

  1. Prefer headless-safe smoke paths for CI.
  2. Use helper context setup where graphics tests require it.

Verification:

  • CI lane runs expected headless-safe coverage without GL crashes.

[Platform] Linux desktop app fails to start with GLFW/OpenGL errors

Symptoms:

  • Startup fails before first frame.

Cause:

  • Missing system packages for GLFW/OpenGL.

Fix:

  1. Install required OpenGL/GLFW runtime packages.
  2. Re-run the same command via dev.sh.

Verification:

  • Window opens and loop runs.

[Platform] Web networking behavior differs from native SDKs

Symptoms:

  • Browser networking path behaves differently from Node/native.

Cause:

  • Browser SDK uses WASM/browser APIs, not native transports.

Fix:

  1. Review Web Platform Gotchas
  2. Validate with web-specific smoke paths.

Verification:

  • Web networking behavior matches documented browser scope.

[Platform] TypeScript web networking fails to connect

Symptoms:

  • Browser client never reaches peerCount() > 0.

Cause:

  • Wrong protocol, wrong URL, or sending before the WebSocket connection is established.

Fix:

  1. Use NetworkProtocol.WebSocket
  2. Point it at a browser-reachable ws:// or wss:// URL
  3. Wait for peerCount() > 0 before first send
  4. Validate with:
    • ./dev.sh --sdk typescript --game sandbox_web
    • cd examples/typescript/feature_lab
    • npm run smoke:web-network

Verification:

  • Browser smoke passes with a real ping/pong roundtrip.

Example and tutorial issues

[Examples] Not sure which example to run first

Use this order:

  1. Flappy Bird for baseline behavior parity.
  2. Sandbox for the full cross-language feature tour.
  3. Feature Lab for supplemental smoke coverage.
  4. C# specialization demos for renderer/gameplay patterns.

Reference:

[Tutorial] Need the final code for Build Your First Game

Use shipped final projects:

  • examples/csharp/flappy_goud/
  • examples/python/flappy_bird.py
  • examples/typescript/flappy_bird/
  • examples/rust/flappy_bird/

If you need zip bundles, use the git archive commands in Build Your First Game.

Video Tutorials

This page ships one public getting-started recording for the TypeScript web flow on the hosted GoudEngine docs site.

Version marker: GoudEngine 0.0.828

TypeScript Web: Getting Started

  • Covered commands:
    • cd sdks/typescript && npm ci
    • ./dev.sh --sdk typescript --game flappy_bird_web
    • ./dev.sh --sdk typescript --game sandbox_web
    • cd examples/typescript/sandbox && npm run build:web
  • Scope: browser/WASM getting-started workflow for the current alpha branch
  • Public host: goudengine.aramhammoudeh.com
  • Captions: bundled beside the video on the docs site
  • Direct downloads:

Example Showcase

This page and preview media are generated from examples/showcase.manifest.json and scripts/generate-showcase-docs.py. Run python3 scripts/generate-showcase-docs.py after metadata or media changes.

Current release status:

  • Source links, run commands, and preview media are generated for entries in examples/.

Baseline parity game: Flappy Bird

Flappy Bird is the behavior baseline across SDKs.

ExampleSDKTargetPathRun commandDescriptionMediaSource
Flappy GoudC#Desktopexamples/csharp/flappy_goud./dev.sh --game flappy_goudFlappy Bird parity baseline.Flappy Goud C# desktop previewGitHub
Flappy BirdPythonDesktopexamples/python/flappy_bird.py./dev.sh --sdk python --game flappy_birdPython parity baseline.Flappy Bird Python desktop previewGitHub
Flappy BirdTypeScriptDesktopexamples/typescript/flappy_bird/desktop.ts./dev.sh --sdk typescript --game flappy_birdTypeScript desktop parity baseline.Flappy Bird TypeScript desktop previewGitHub
Flappy BirdTypeScriptWebexamples/typescript/flappy_bird/web./dev.sh --sdk typescript --game flappy_bird_webTypeScript web parity baseline.Flappy Bird TypeScript web previewGitHub
Flappy BirdRustDesktopexamples/rust/flappy_birdcargo run -p flappy-birdNative Rust parity baseline.Flappy Bird Rust desktop previewGitHub

Sandbox parity target

Sandbox is the shared cross-language app for the broader Alpha-001 feature surface. This branch is still closing the remaining recovery rows, so treat the execution CSV as the live truth source.

ExampleSDKTargetPathRun commandDescriptionMediaSource
SandboxC#Desktopexamples/csharp/sandbox./dev.sh --game sandboxManifest-driven desktop sandbox with the shared HUD and localhost peer contract. Current branch truth still tracks the remaining scene-flow recovery rows in the execution CSV.Sandbox C# desktop previewGitHub
SandboxPythonDesktopexamples/python/sandbox.py./dev.sh --sdk python --game sandboxManifest-driven Python sandbox following the shared asset pack, three-panel HUD, and peer packet contract on this branch.Sandbox Python desktop previewGitHub
SandboxTypeScriptDesktopexamples/typescript/sandbox/desktop.ts./dev.sh --sdk typescript --game sandboxManifest-driven TypeScript desktop sandbox following the shared HUD and peer packet contract on this branch.Sandbox TypeScript desktop previewGitHub
SandboxTypeScriptWebexamples/typescript/sandbox/web./dev.sh --sdk typescript --game sandbox_webBrowser/WASM sandbox with the same HUD structure and explicit capability-gated networking and renderer copy.Sandbox TypeScript web previewGitHub
SandboxRustDesktopexamples/rust/sandboxcargo run -p sandboxManifest-driven Rust sandbox using the shared asset pack and GoudGame::draw_text(...) for the branch’s recovered native HUD path.Sandbox Rust desktop previewGitHub

Feature Lab smoke coverage

Feature Lab stays in the repo as a supplemental smoke harness for wrapper and provider coverage, debugger enablement, and local attach workflow coverage.

ExampleSDKTargetPathRun commandDescriptionMediaSource
Feature LabC#Headlessexamples/csharp/feature_lab./dev.sh --game feature_labHeadless smoke for ECS and provider wrappers with debugger mode enabled and stable local attach coverage.Feature Lab C# headless previewGitHub
Feature LabPythonHeadlessexamples/python/feature_lab.pypython3 examples/python/feature_lab.pyHeadless smoke for generated wrappers with debugger mode enabled and stable local attach coverage.Feature Lab Python headless previewGitHub
Feature LabTypeScriptDesktopexamples/typescript/feature_lab/desktop.ts./dev.sh --sdk typescript --game feature_labDesktop smoke for SDK capability probes plus debugger manifest, snapshot, and local attach coverage.Feature Lab TypeScript desktop previewGitHub
Feature LabTypeScriptWebexamples/typescript/feature_lab/web./dev.sh --sdk typescript --game feature_lab_webWeb smoke with browser/WASM coverage. Debugger attach remains desktop-only in this batch.Feature Lab TypeScript web previewGitHub
Feature LabRustHeadlessexamples/rust/feature_labcargo run -p feature-labNative Rust headless smoke for SDK coverage with debugger enablement and stable local attach coverage.Feature Lab Rust headless previewGitHub

C# specialization demos

C#-specific gameplay and renderer examples.

ExampleSDKTargetPathRun commandDescriptionMediaSource
3D CubeC#Desktopexamples/csharp/3d_cube./dev.sh --game 3d_cube3D renderer demo.3D Cube C# desktop previewGitHub
Goud JumperC#Desktopexamples/csharp/goud_jumper./dev.sh --game goud_jumperPlatform movement and collision.Goud Jumper C# desktop previewGitHub
Isometric RPGC#Desktopexamples/csharp/isometric_rpg./dev.sh --game isometric_rpgIsometric camera and RPG systems.Isometric RPG C# desktop previewGitHub
Hello ECSC#Desktopexamples/csharp/hello_ecs./dev.sh --game hello_ecsMinimal ECS starter.Hello ECS C# desktop previewGitHub
Character SandboxC#Desktopexamples/csharp/character_sandbox./dev.sh --game character_sandbox3D character sandbox with model loading, skeletal animation, scene management.Character Sandbox C# desktop previewGitHub

Starter demos

Single-file SDK demos used for quick setup checks.

ExampleSDKTargetPathRun commandDescriptionMediaSource
Python SDK DemoPythonDesktopexamples/python/main.py./dev.sh --sdk python --game python_demoMinimal Python startup demo.Python SDK demo desktop previewGitHub

Notes

  • This page is generated. Update examples/showcase.manifest.json and rerun the generator.
  • Generator validation fails if examples are added or removed without manifest updates.

RFCs

RFCs (Request for Comments) are the mechanism for proposing and deciding on significant changes to GoudEngine. Use an RFC when a change affects public API, architecture, cross-cutting concerns, or long-term direction.

Small bug fixes, refactors that do not change behavior, and documentation updates do not need RFCs.

Numbering

RFCs use zero-padded 4-digit numbers: RFC-0001, RFC-0002, etc. Assign the next available number when opening the PR.

File Format

Each RFC lives at docs/rfcs/RFC-NNNN-short-title.md and starts with YAML front matter:

---
rfc: "0001"
title: "Short descriptive title"
status: draft
created: YYYY-MM-DD
authors: ["github-username"]
tracking-issue: "#123"
related-issues: ["#124", "#125"]  # optional
---

Status Lifecycle

draft → proposed → accepted → implemented → superseded
StatusMeaning
draftWork in progress, not ready for review
proposedPR open, ready for review
acceptedPR merged with review approval
implementedCode is shipped; RFC is complete
supersededReplaced by a later RFC (link to successor)

Acceptance requires at least one PR review approval from a maintainer. The proposedaccepted transition is automated: a GitHub Action (rfc-approve.yml) updates the front matter status when the PR merges. The implemented and superseded transitions remain manual.

Writing an RFC

  1. Copy the template: docs/rfcs/RFC-0000-template.md
  2. Assign the next number and rename the file
  3. Fill in: motivation, detailed design, drawbacks, alternatives considered
  4. Open a PR; set status: proposed in the front matter
  5. Address review feedback on the PR
  6. On merge: rfc-approve.yml automatically sets status: accepted; update the index below

Index

RFCTitleStatus
RFC-0001Provider Trait Patternaccepted
RFC-0002NetworkProvider Trait Designaccepted
RFC-0003UI Layout and Input Behaviordraft
RFC-0004Debugger Runtime, Snapshot Contract, and Local Attach Modelaccepted

RFC-0001: Provider Trait Pattern


rfc: “0001” title: Provider Trait Pattern status: accepted created: 2026-03-06 authors: [“aram-devdocs”] tracking-issue: “#217”

RFC-0001: Provider Trait Pattern

1. Summary

This RFC defines a universal provider abstraction for all GoudEngine subsystems. It replaces the hardcoded OpenGLBackend in GoudGame with configurable, swappable providers selected at engine initialization. The pattern applies uniformly to rendering, physics, audio, windowing, and input. SDK users select built-in providers via enums; Rust SDK users may supply custom implementations.


2. Motivation

GoudGame in goud_engine/src/sdk/game/instance.rs currently holds:

#![allow(unused)]
fn main() {
#[cfg(feature = "native")]
render_backend: Option<OpenGLBackend>,
#[cfg(feature = "native")]
sprite_batch: Option<SpriteBatch<OpenGLBackend>>,
}

This hardcoding creates several problems:

  • Adding a wgpu backend requires duplicating every code path that touches backend and sprite_batch.
  • Physics, audio, and windowing have the same problem — there is no swap point.
  • Cross-platform targets (consoles, mobile) need NDA-bound backends that cannot ship in the public repo. There is no way to inject them without forking engine internals.
  • Runtime renderer selection (e.g., falling back from Vulkan to OpenGL) is not possible.

PlatformBackend in goud_engine/src/libs/platform/mod.rs already solves this for the platform layer: GoudGame stores Option<Box<dyn PlatformBackend>>. This RFC extends that pattern to all subsystems.


3. Design

3.1 Provider Supertrait

All providers implement a common base trait:

#![allow(unused)]
fn main() {
pub trait Provider: Send + Sync + 'static {
    fn name(&self) -> &str;
    fn version(&self) -> &str;
    fn capabilities(&self) -> Box<dyn std::any::Any>;
}
}

Send + Sync + 'static is required because providers are stored in ProviderRegistry, which may be accessed from worker threads during asset streaming. The exception is WindowProvider (see §3.7). Subsystem traits extend Provider with domain-specific methods.


3.2 Subsystem Provider Traits

RenderProvider

#![allow(unused)]
fn main() {
pub trait RenderProvider: Provider {
    // Lifecycle — FrameContext token enforces begin/end pairing
    fn begin_frame(&mut self) -> GoudResult<FrameContext>;
    fn end_frame(&mut self, frame: FrameContext) -> GoudResult<()>;
    fn resize(&mut self, width: u32, height: u32) -> GoudResult<()>;
    // Resources
    fn create_texture(&mut self, desc: &TextureDesc) -> GoudResult<TextureHandle>;
    fn destroy_texture(&mut self, handle: TextureHandle);
    fn create_buffer(&mut self, desc: &BufferDesc) -> GoudResult<BufferHandle>;
    fn destroy_buffer(&mut self, handle: BufferHandle);
    fn create_shader(&mut self, desc: &ShaderDesc) -> GoudResult<ShaderHandle>;
    fn destroy_shader(&mut self, handle: ShaderHandle);
    fn create_pipeline(&mut self, desc: &PipelineDesc) -> GoudResult<PipelineHandle>;
    fn destroy_pipeline(&mut self, handle: PipelineHandle);
    fn create_render_target(&mut self, desc: &RenderTargetDesc) -> GoudResult<RenderTargetHandle>;
    fn destroy_render_target(&mut self, handle: RenderTargetHandle);
    // Drawing
    fn draw(&mut self, cmd: &DrawCommand) -> GoudResult<()>;
    fn draw_batch(&mut self, cmds: &[DrawCommand]) -> GoudResult<()>;
    fn draw_mesh(&mut self, cmd: &MeshDrawCommand) -> GoudResult<()>;
    fn draw_text(&mut self, cmd: &TextDrawCommand) -> GoudResult<()>;
    fn draw_particles(&mut self, cmd: &ParticleDrawCommand) -> GoudResult<()>;
    // State
    fn set_viewport(&mut self, x: i32, y: i32, width: u32, height: u32);
    fn set_camera(&mut self, camera: &CameraData);
    fn set_render_target(&mut self, handle: Option<RenderTargetHandle>);
    fn clear(&mut self, color: [f32; 4]);
}
}

FrameContext is an opaque token returned by begin_frame and consumed by end_frame, ensuring the caller cannot skip frame finalization. PipelineDesc abstracts render pipeline state (shader + vertex layout + blend mode) required by wgpu; OpenGL providers can map this to their internal state tracking.

Built-inFeature FlagNotes
OpenGLRenderProvidernativeWraps existing OpenGLBackend
WgpuRenderProviderwgpuF02-03; wraps existing wgpu_backend/ modules
NullRenderProvideralwaysNo-op, for headless tests

The existing RenderBackend trait (goud_engine/src/libs/graphics/backend/render_backend.rs) is NOT object-safe by design. It becomes an internal detail of OpenGLRenderProvider and WgpuRenderProvider and does not appear in the public provider API.

A partial wgpu backend already exists at goud_engine/src/libs/graphics/backend/wgpu_backend/ (frame, texture, shader, buffer, pipeline modules). F02-03 will wrap this existing code inside WgpuRenderProvider rather than rewriting it.

PhysicsProvider

#![allow(unused)]
fn main() {
pub trait PhysicsProvider: Provider {
    fn step(&mut self, delta: f32) -> GoudResult<()>;
    fn set_gravity(&mut self, gravity: Vec2);
    fn gravity(&self) -> Vec2;
    fn create_body(&mut self, desc: &BodyDesc) -> GoudResult<BodyHandle>;
    fn destroy_body(&mut self, handle: BodyHandle);
    fn body_position(&self, handle: BodyHandle) -> GoudResult<Vec2>;
    fn set_body_position(&mut self, handle: BodyHandle, pos: Vec2) -> GoudResult<()>;
    fn body_velocity(&self, handle: BodyHandle) -> GoudResult<Vec2>;
    fn set_body_velocity(&mut self, handle: BodyHandle, vel: Vec2) -> GoudResult<()>;
    fn apply_force(&mut self, handle: BodyHandle, force: Vec2) -> GoudResult<()>;
    fn apply_impulse(&mut self, handle: BodyHandle, impulse: Vec2) -> GoudResult<()>;
    fn create_collider(&mut self, body: BodyHandle, desc: &ColliderDesc) -> GoudResult<ColliderHandle>;
    fn destroy_collider(&mut self, handle: ColliderHandle);
    fn raycast(&self, origin: Vec2, dir: Vec2, max_dist: f32) -> Option<RaycastHit>;
    fn overlap_circle(&self, center: Vec2, radius: f32) -> Vec<BodyHandle>;
    fn drain_collision_events(&mut self) -> Vec<CollisionEvent>;
    fn contact_pairs(&self) -> Vec<ContactPair>;
    fn create_joint(&mut self, desc: &JointDesc) -> GoudResult<JointHandle>;
    fn destroy_joint(&mut self, handle: JointHandle);
    fn debug_shapes(&self) -> Vec<DebugShape>;
}
}

drain_collision_events returns owned Vec rather than a slice reference to avoid lifetime coupling between the event buffer and the provider borrow — callers process events after the physics step without holding a borrow on the provider.

Built-inFeature FlagNotes
Rapier2DPhysicsProviderrapier2d2D rigid-body physics
Rapier3DPhysicsProviderrapier3d3D rigid-body physics
SimplePhysicsProvideralwaysAABB collision + gravity only, no rapier dependency
NullPhysicsProvideralwaysNo-op passthrough

AudioProvider

#![allow(unused)]
fn main() {
pub trait AudioProvider: Provider {
    fn update(&mut self) -> GoudResult<()>;
    fn play(&mut self, handle: SoundHandle, config: &PlayConfig) -> GoudResult<PlaybackId>;
    fn stop(&mut self, id: PlaybackId) -> GoudResult<()>;
    fn pause(&mut self, id: PlaybackId) -> GoudResult<()>;
    fn resume(&mut self, id: PlaybackId) -> GoudResult<()>;
    fn is_playing(&self, id: PlaybackId) -> bool;
    fn set_volume(&mut self, id: PlaybackId, volume: f32) -> GoudResult<()>;
    fn set_master_volume(&mut self, volume: f32);
    fn set_channel_volume(&mut self, channel: AudioChannel, volume: f32);
    fn set_listener_position(&mut self, pos: Vec3);
    fn set_source_position(&mut self, id: PlaybackId, pos: Vec3) -> GoudResult<()>;
}
}
Built-inFeature FlagNotes
RodioAudioProvideraudioUses existing rodio integration
WebAudioProviderwebBrowser/WASM via web-sys
NullAudioProvideralwaysNo-op, for CI / headless

WindowProvider

WindowProvider extracts the surface-management part of PlatformBackend. It is NOT Send + Sync because GLFW requires all window calls on the main thread (see §3.7).

#![allow(unused)]
fn main() {
pub trait WindowProvider: 'static {  // NOT Send + Sync — see §3.7
    fn init(&mut self) -> GoudResult<()>;
    fn shutdown(&mut self);
    fn should_close(&self) -> bool;
    fn set_should_close(&mut self, value: bool);
    fn poll_events(&mut self);
    fn swap_buffers(&mut self);
    fn get_size(&self) -> (u32, u32);
    fn get_framebuffer_size(&self) -> (u32, u32);
}
}

WindowProvider does NOT extend Provider (which requires Send + Sync) but does include its own init()/shutdown() methods matching the ProviderLifecycle contract. It cannot implement ProviderLifecycle directly because that trait will likely require Provider as a supertrait.

Built-inFeature FlagNotes
GlfwWindowProvidernativeWraps existing GLFW platform layer
WinitWindowProviderwinitFuture — needed for mobile/web targets
NullWindowProvideralwaysNo-op for headless contexts

GLFW is the current platform layer (goud_engine/src/libs/platform/glfw_platform.rs). The roadmap targets winit for broader platform support (mobile, web); WinitWindowProvider will be added when that migration begins.

InputProvider

Extracted from PlatformBackend separately because input has a different update cadence and can be mocked without a real window (e.g., test harnesses injecting synthetic events). For the common GLFW case, a WindowInputBridge helper wires GLFW window events into the InputProvider interface.

#![allow(unused)]
fn main() {
pub trait InputProvider: Provider {
    fn key_pressed(&self, key: KeyCode) -> bool;
    fn key_just_pressed(&self, key: KeyCode) -> bool;
    fn key_just_released(&self, key: KeyCode) -> bool;
    fn mouse_position(&self) -> Vec2;
    fn mouse_delta(&self) -> Vec2;
    fn mouse_button_pressed(&self, button: MouseButton) -> bool;
    fn scroll_delta(&self) -> Vec2;
    fn gamepad_connected(&self, id: GamepadId) -> bool;
    fn gamepad_axis(&self, id: GamepadId, axis: GamepadAxis) -> f32;
    fn gamepad_button_pressed(&self, id: GamepadId, button: GamepadButton) -> bool;
}
}
Built-inFeature FlagNotes
GlfwInputProvidernativeReads from GLFW event queue
NullInputProvideralwaysAll buttons unpressed

3.3 Lifecycle Protocol

Every provider follows a five-phase lifecycle:

  1. Create — constructed with a config struct (RenderConfig, PhysicsConfig, etc.)
  2. Initinit() called during GoudGame::new(). Failure is fatal unless a fallback is configured (§3.9).
  3. Update — per-frame update(delta) for providers that need it (physics, audio).
  4. Shutdownshutdown() called during GoudGame::drop(). Must not fail.
  5. Drop — provider dropped after shutdown. All GPU/OS resources must be released before this point.
#![allow(unused)]
fn main() {
pub trait ProviderLifecycle {
    fn init(&mut self) -> GoudResult<()>;
    fn update(&mut self, delta: f32) -> GoudResult<()>;
    fn shutdown(&mut self);
}
}

All subsystem provider traits (RenderProvider, PhysicsProvider, etc.) extend both Provider and ProviderLifecycle. For example: pub trait RenderProvider: Provider + ProviderLifecycle { ... }.


3.4 Capability Query Pattern

Each subsystem defines a typed capability struct, building on the existing BackendCapabilities pattern in goud_engine/src/libs/graphics/backend/capabilities.rs:

#![allow(unused)]
fn main() {
pub struct RenderCapabilities {
    pub max_texture_units: u32,
    pub max_texture_size: u32,
    pub supports_instancing: bool,
    pub supports_compute: bool,
    pub supports_msaa: bool,
}
}

Each subsystem trait also provides a typed accessor that avoids the downcast:

#![allow(unused)]
fn main() {
pub trait RenderProvider: Provider + ProviderLifecycle {
    fn render_capabilities(&self) -> &RenderCapabilities;
    // ... other methods
}
}

The generic Provider::capabilities() returning Box<dyn Any> remains available for code that operates on providers generically (e.g., logging all provider capabilities at startup). For subsystem-specific code, prefer the typed accessor:

#![allow(unused)]
fn main() {
// Preferred — no downcast, no runtime failure path
let caps = render_provider.render_capabilities();
if caps.supports_instancing {
    renderer.use_instanced_path();
} else {
    renderer.use_fallback_path();
}
}

3.5 Registration and Selection

ProviderRegistry lives at Layer 2 (goud_engine/src/core/providers/registry.rs):

#![allow(unused)]
fn main() {
pub struct ProviderRegistry {
    pub render: Box<dyn RenderProvider>,
    pub physics: Box<dyn PhysicsProvider>,
    pub audio: Box<dyn AudioProvider>,
    pub input: Box<dyn InputProvider>,
    // WindowProvider is !Send+Sync, stored separately in GoudGame.
}
}

Rust SDK — builder pattern with Null*Provider defaults for unconfigured slots:

#![allow(unused)]
fn main() {
let game = GoudEngine::builder()
    .with_renderer(OpenGLRenderProvider::new(RenderConfig::default()))
    .with_physics(Rapier2DPhysicsProvider::new(PhysicsConfig {
        gravity: Vec2::new(0.0, -9.81), ..Default::default()
    }))
    .with_audio(RodioAudioProvider::new(AudioConfig::default()))
    .with_window(GlfwWindowProvider::new(WindowConfig {
        width: 1280, height: 720, title: "My Game".into(),
    }))
    .build()?;
}

FFI SDKs — enum-based selection (custom providers require the Rust SDK):

#![allow(unused)]
fn main() {
#[repr(C)]
pub enum GoudRendererType {
    WgpuAuto = 0,     // Auto-select best wgpu backend
    WgpuVulkan = 1,
    WgpuMetal = 2,
    WgpuDx12 = 3,
    WgpuWebGpu = 4,
    OpenGL = 10,
    Null = 99,
}

#[repr(C)]
pub enum GoudPhysicsType { Rapier2D = 0, Rapier3D = 1, Simple = 2, Null = 99 }

#[repr(C)]
pub enum GoudAudioType { Rodio = 0, WebAudio = 1, Null = 99 }
}

The renderer enum exposes wgpu backend sub-selection to allow SDK users to force a specific GPU API when needed (e.g., Vulkan for Linux, Metal for macOS).


3.6 Object Safety and Dispatch Strategy

All provider traits are object-safe: no associated types, no generic methods. Stored as Box<dyn RenderProvider> etc. Dynamic dispatch overhead is acceptable here because calls are coarse-grained (per-frame or per-batch), not per-vertex.

The existing RenderBackend trait (goud_engine/src/libs/graphics/backend/render_backend.rs) is intentionally NOT object-safe and remains an internal detail inside concrete providers. This mirrors AssetLoader/ErasedAssetLoader in goud_engine/src/assets/loader/traits.rs (same crate): typed generics for hot paths, erased trait objects for storage.

For future cross-provider resource sharing, providers MAY expose fn shared_resources(&self) -> Option<&dyn Any> as an extension point.

For performance-critical inner loops needing direct backend access, providers expose typed accessors:

#![allow(unused)]
fn main() {
impl OpenGLRenderProvider {
    pub(crate) fn backend(&mut self) -> &mut OpenGLBackend { ... }
}
}

3.7 Thread Safety

Default bound: Send + Sync + 'static on all provider traits.

Exception: WindowProvider. GLFW requires main-thread access, so WindowProvider is !Send + !Sync. The engine enforces this by storing it outside ProviderRegistry directly in GoudGame, making GoudGame itself !Send when a native window is present. This matches PlatformBackend in goud_engine/src/libs/platform/mod.rs. If an async executor is adopted in the future, WindowProvider calls must be scheduled on the main thread via a MainThreadScheduler that queues operations from async tasks.


3.8 Hot-Swap (Dev Mode)

Dev-mode only, gated behind #[cfg(debug_assertions)] or dev-tools feature. Protocol:

  1. shutdown() on active provider → drop it
  2. Create and init() replacement provider
  3. Invalidate all resource handles (textures, buffers, shaders) from old provider
  4. Trigger one-frame resource re-upload

Providers declare support with fn supports_hot_swap(&self) -> bool (default false). Implementation tracked in F02-08; this RFC defines constraints only.

Constraints: handle invalidation must produce errors (not silent UB), hot-swap must not be called from multiple threads, and replacement must pass init() before the old provider is dropped.

Expected invalidation mechanism: the engine already uses generational handles (see core/handle.rs). On hot-swap, the provider epoch increments; all existing handles carry the old epoch and fail validation on next use. This avoids scanning all live handles.


3.9 Error Handling

All fallible provider methods return GoudResult<T>. A new variant is added to GoudError (goud_engine/src/core/error/types.rs):

#![allow(unused)]
fn main() {
ProviderError { subsystem: &'static str, message: String }
}

This uses a struct variant (unlike the existing tuple variants like InitializationFailed(String)) because provider errors need the subsystem discriminator for FFI error code routing — error codes 600–609 for render, 610–619 for physics, etc. A single String would require parsing to extract the subsystem.

If init() fails and no fallback is configured, GoudGame::new() returns Err(GoudError::ProviderError { ... }). Fallback providers can be configured via .with_fallback_renderer(NullRenderProvider::new()).


3.10 Layer Placement

libs/ is a module within goud_engine/src/libs/, not a standalone workspace crate. Per CLAUDE.md, libs/ is Layer 1 (lowest) and must not import from Layer 2 (core/, assets/, sdk/) or higher.

ComponentLayerPath
Provider trait definitionsLayer 1goud_engine/src/libs/providers/ (new module)
Concrete implementationsLayer 1goud_engine/src/libs/providers/impls/
ProviderRegistryLayer 2goud_engine/src/core/providers/registry.rs
Builder (GoudEngine::builder())Layer 2goud_engine/src/core/providers/builder.rs
FFI enum selectionLayer 3goud_engine/src/ffi/providers.rs
SDK enum wrappersLayer 4generated via codegen from goud_sdk.schema.json

Provider traits in goud_engine/src/libs/providers/ may import from sibling Layer 1 modules (libs/graphics/, libs/ecs/) but must not import from core/, sdk/, or ffi/ — those are Layer 2+ and importing them would violate the downward-only rule.

Prerequisite: error type placement. Provider trait methods return GoudResult<T>, but GoudResult and GoudError currently live in goud_engine/src/core/error/ (Layer 2). Existing libs/ modules already import from core/error/ (e.g., libs/graphics/backend/ uses GoudResult), which is an existing Layer 1→2 violation. Before implementing this RFC, error types must be moved to a Layer 1 location (e.g., libs/error/) so that provider traits can reference them without upward imports. This is tracked as a prerequisite for F02-02.


3.11 FFI Boundary

SDK users never interact with provider traits. The FFI exposes enum parameters on init, capability query functions returning #[repr(C)] structs, and no provider handles. The high-level API (draw_sprite, play_sound) is unchanged.

#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn goud_game_create(
    width: u32, height: u32, title: *const c_char,
    renderer_type: GoudRendererType,
    physics_type: GoudPhysicsType,
    audio_type: GoudAudioType,
) -> *mut GoudGame { ... }
}

4. Alternatives Considered

Feature-flag-only selection (compile-time)

Selecting the backend at compile time with #[cfg(feature = "opengl")] / #[cfg(feature = "wgpu")] avoids dynamic dispatch. It does not support runtime fallback, cannot support NDA backends that cannot be checked in, and requires separate binaries for each backend configuration. Rejected.

Dynamic plugin system (.so/.dll loading)

Loading provider implementations from shared libraries at runtime would allow third-party backends without engine recompilation. It introduces significant complexity: platform differences in library loading, symbol resolution, versioning, and safety. The marginal benefit does not justify the cost for an engine at this stage. Rejected; revisit post-1.0.

Enum dispatch instead of trait objects

Wrapping all built-in providers in an enum and dispatching with match avoids dynamic dispatch costs. It prevents custom providers entirely and grows the match arms with every new backend. It also does not solve the NDA backend problem. Rejected.

Merge Window+Input into a single PlatformProvider

PlatformBackend currently handles both windowing and input together. Keeping them merged is simpler. However, input and windowing have different threading and testing requirements: input can be mocked without a real window, but a window cannot exist without being on the main thread. Splitting them enables cleaner headless testing. The split is the recommended design (see §3.2); merging is noted as an open question (§6) if implementation complexity proves too high.


5. Impact

Breaking Changes

  • GoudGame struct changes: Option<OpenGLBackend> and SpriteBatch<OpenGLBackend> fields are replaced by ProviderRegistry and Option<Box<dyn WindowProvider>>.
  • SpriteBatch<B: RenderBackend> generic parameter changes: SpriteBatch will receive a &mut dyn RenderProvider or a concrete backend reference via downcast. The public API surface of SpriteBatch may change.
  • GoudGame::new(width, height, title, renderer_type) signature expands to accept physics and audio provider selections.

FFI Changes

  • goud_game_create gains GoudPhysicsType and GoudAudioType parameters.
  • New capability query functions added to the FFI surface.
  • C# bindings regenerated via csbindgen after cargo build.
  • Minimum migration for existing games: pass GoudRendererType::OpenGL, GoudPhysicsType::Null, GoudAudioType::Null to preserve current behavior with no functional change.

SDK Changes

  • All three SDK wrappers (C#, Python, TypeScript) updated via codegen from the schema.
  • Init functions gain physics and audio type parameters.
  • Existing game code that uses the default OpenGL + Null physics + Null audio continues to work with updated init calls.

Examples

  • All C# examples in examples/csharp/ updated to pass the new init parameters.
  • Python and TypeScript examples updated in parallel.

Migration Path

Implementation proceeds in phases F02-02 through F02-09 as defined in ALPHA_ROADMAP.md:

  • F02-02: Define libs/providers/ module with all trait definitions.
  • F02-03: Implement OpenGLRenderProvider wrapping the existing backend.
  • F02-04: Implement Rapier2DPhysicsProvider.
  • F02-05: Implement RodioAudioProvider.
  • F02-06: Split PlatformBackend into WindowProvider + GlfwInputProvider.
  • F02-07: Wire ProviderRegistry into GoudGame, remove hardcoded fields.
  • F02-08: Hot-swap mechanism (dev-tools feature only).
  • F02-09: FFI enum selection + SDK codegen updates.

6. Resolved Decisions

  1. Window+Input: Keep Separate. WindowProvider and InputProvider remain separate traits. glfw_platform.rs pumps events via poll_events(&mut self, input: &mut InputManager) — tight event dispatch but loose storage. InputManager is an independent resource, and GamepadState infrastructure in core/input_manager/types.rs exists but isn’t wired to GLFW, proving input sources can be window-independent. Headless testing needs input without a window; gamepad/network inputs don’t come from windows. WindowProvider is !Send + !Sync (GLFW main-thread) while InputProvider can be Send + Sync. A WindowInputBridge helper wires GLFW events → InputProvider for the common case.

  2. Resource Ownership: Provider-Owned. Providers own their resources. A shared ResourcePool is deferred to post-stabilization. RenderBackend already uses handle-based resource management (create_buffer(), create_texture(), destroy_*()), and GoudGame is the single root owner. No cross-subsystem resource sharing exists today. Extension point: providers MAY expose fn shared_resources(&self) -> Option<&dyn Any> for future cross-provider sharing.

  3. NullProvider: Explicit Structs. Use explicit Null*Provider structs, not default method implementations. The codebase uses Option<T> for conditional features and AssetState enum with explicit states (NotLoaded, Loading, Failed) rather than silent defaults. Explicit structs are visible, debuggable, and testable (can count draw calls, track state). FFI enum values (GoudRendererType::Null) map cleanly to concrete structs. Trait methods stay without defaults — forces implementors to be explicit.

  4. Async & Main-Thread: Document Constraint, Defer. The !Send constraint on GoudGame when a native WindowProvider is present is documented and correct. No async resolution is needed now — there is no async code in the codebase, and parallelism is Rayon scope-based (parallel.rs). GLFW requires main-thread access (glfw_platform.rs lines 7–10) and GoudContext is already !Send + !Sync. If an async executor is adopted later, WindowProvider calls must be scheduled on the main thread via a MainThreadScheduler that queues operations from async tasks.

RFC-0002: NetworkProvider Trait


rfc: “0002” title: “NetworkProvider Trait Design” status: accepted created: 2026-03-06 authors: [“aram-devdocs”] tracking-issue: “#356”

RFC-0002: NetworkProvider Trait Design

1. Summary

This RFC defines NetworkProvider, a new subsystem trait following the provider pattern established in RFC-0001. It abstracts transport backends (UDP for desktop, WebSocket for web) behind a unified interface covering connection lifecycle, message passing over typed channels, and event polling. NetworkProvider extends both Provider and ProviderLifecycle supertraits per RFC-0001 §3.3, and integrates with ProviderRegistry as an optional field, leaving games that do not need networking unaffected.


2. Motivation

GoudEngine has no networking subsystem. Parent issue #140 requires a full networking system; this RFC specifies the trait boundary before implementation begins.

RFC-0001 established the provider pattern for rendering, physics, audio, windowing, and input. NetworkProvider must follow that same pattern: a trait in libs/providers/, concrete implementations in libs/providers/impls/network/, registration in ProviderRegistry at Layer 2, and FFI exposure at Layer 3. Diverging from this pattern would fragment the architecture.

Multiplayer games target different transports by platform. A desktop build uses UDP (low latency, no browser restrictions); a web/WASM build uses WebSockets (only transport available in browsers). The game layer must not know which transport is active. NetworkProvider is the swap point.

Integration attaches to GoudGame in goud_engine/src/sdk/game/instance.rs, the same file RFC-0001 identified as the central integration point for all providers.


3. Design

3.1 NetworkProvider Trait

#![allow(unused)]
fn main() {
pub trait NetworkProvider: Provider + ProviderLifecycle {
    /// Begin accepting inbound connections on the given config.
    ///
    /// Calling `host` on an already-hosting provider returns an error.
    fn host(&mut self, config: &HostConfig) -> GoudResult<()>;

    /// Open a connection to the given address.
    ///
    /// Returns a `ConnectionId` that is valid until the connection closes.
    /// The connection may not be fully established when this returns; poll
    /// `drain_events` for `NetworkEvent::Connected`.
    fn connect(&mut self, addr: &str) -> GoudResult<ConnectionId>;

    /// Close a specific connection.
    fn disconnect(&mut self, conn: ConnectionId) -> GoudResult<()>;

    /// Close all active connections.
    fn disconnect_all(&mut self) -> GoudResult<()>;

    /// Send raw bytes to one connection on the given channel.
    ///
    /// The provider does not inspect or frame the bytes. Serialization
    /// is the caller's responsibility.
    fn send(&mut self, conn: ConnectionId, channel: Channel, data: &[u8]) -> GoudResult<()>;

    /// Send raw bytes to all active connections on the given channel.
    fn broadcast(&mut self, channel: Channel, data: &[u8]) -> GoudResult<()>;

    /// Return all buffered network events and clear the internal buffer.
    ///
    /// Must be called once per frame. Returns owned `Vec` to avoid holding
    /// a borrow on the provider while the caller processes events.
    fn drain_events(&mut self) -> Vec<NetworkEvent>;

    /// Return the current list of active connection IDs.
    fn connections(&self) -> &[ConnectionId];

    /// Return the state of a specific connection.
    fn connection_state(&self, conn: ConnectionId) -> ConnectionState;

    /// Return this peer's own `ConnectionId`, if the provider has been assigned one.
    fn local_id(&self) -> Option<ConnectionId>;

    /// Return static capability flags for this provider.
    fn network_capabilities(&self) -> &NetworkCapabilities;

    /// Return aggregate network statistics.
    fn stats(&self) -> NetworkStats;

    /// Return per-connection statistics, or `None` if the ID is unknown.
    fn connection_stats(&self, conn: ConnectionId) -> Option<ConnectionStats>;
}
}

Design decisions:

  • Raw bytes, not typed messages. Generic methods break object safety. Serialization (serde, bitcode, etc.) is a game-level concern; the provider passes bytes through.
  • Poll, not callbacks. Callbacks are not object-safe and require 'static closures that complicate borrow lifetimes. drain_events matches PhysicsProvider::drain_collision_events from RFC-0001.
  • ConnectionId not PeerId. ConnectionId is a transport-level concept. Game-level peer identity (player IDs, lobby slots) belongs above the provider boundary.
  • No async. The engine has no async runtime. Background I/O threads communicate with the main thread via channels; drain_events collects that work synchronously (see §3.5).
  • Owned Vec return accepted. drain_events returns Vec<NetworkEvent> where Received variants contain data: Vec<u8>, causing per-message heap allocations. This mirrors PhysicsProvider::drain_collision_events from RFC-0001, which justified the pattern to avoid lifetime coupling. Network payloads are larger than collision events, but drain_events runs once per frame (not per-packet), and the Vec is short-lived. If profiling shows allocation pressure, a future optimization can reuse a scratch buffer inside the provider and return &[NetworkEvent] with a borrow — the trait can evolve without breaking the FFI boundary, which already uses caller-provided buffers (§3.8).

3.2 Supporting Types

#![allow(unused)]
fn main() {
/// Opaque transport-level connection identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConnectionId(u64);

/// Lifecycle state of a connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
    Disconnected,
    Connecting,
    Connected,
    Disconnecting,
    Error,
}

/// Named channel for message routing.
///
/// Channels map to transport QoS settings (reliable/unreliable, ordered/unordered).
/// Channel 0 is always reliable-ordered. Higher channels are provider-defined.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Channel(pub u8);

/// An event produced by the network provider during `drain_events`.
#[derive(Debug, Clone)]
pub enum NetworkEvent {
    Connected { conn: ConnectionId },
    Disconnected { conn: ConnectionId, reason: DisconnectReason },
    Received { conn: ConnectionId, channel: Channel, data: Vec<u8> },
    Error { conn: ConnectionId, message: String },
}

/// Why a connection closed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
    LocalClose,
    RemoteClose,
    Timeout,
    Error(String),
}

/// Configuration for hosting (accepting inbound connections).
#[derive(Debug, Clone)]
pub struct HostConfig {
    pub bind_address: String,
    pub port: u16,
    pub max_connections: u32,
}

/// Static capability flags for a network provider.
#[derive(Debug, Clone)]
pub struct NetworkCapabilities {
    pub supports_hosting: bool,
    pub max_connections: u32,
    pub max_channels: u8,
    pub max_message_size: usize,
}

/// Aggregate statistics for the provider.
#[derive(Debug, Clone, Default)]
pub struct NetworkStats {
    pub bytes_sent: u64,
    pub bytes_received: u64,
    pub packets_sent: u64,
    pub packets_received: u64,
    pub packets_lost: u64,
}

/// Per-connection statistics.
#[derive(Debug, Clone, Default)]
pub struct ConnectionStats {
    pub round_trip_ms: f32,
    pub bytes_sent: u64,
    pub bytes_received: u64,
    pub packets_lost: u64,
}
}

3.3 Connection Lifecycle

Disconnected
     |
     | connect() / host() receives client
     v
 Connecting
     |              \
     | handshake ok  | handshake fail
     v               v
 Connected         Error
     |
     | disconnect() / remote close / timeout
     v
Disconnecting
     |
     v
Disconnected

drain_events emits NetworkEvent::Connected on the Connecting -> Connected transition and NetworkEvent::Disconnected on the Disconnecting -> Disconnected transition. NetworkEvent::Error does not automatically close the connection; call disconnect after receiving it.


3.4 Built-in Implementations

ImplementationFeature FlagNotes
UdpNetProvidernet-udpUDP transport; desktop targets
WebSocketNetProvidernet-wsWebSocket transport; web/WASM targets
NullNetProvideralwaysNo-op; for games that do not use networking

NullNetProvider satisfies the optional field in ProviderRegistry without adding a dependency or panicking. drain_events returns an empty Vec; all other methods return Ok(()) or type-appropriate defaults.


3.5 Thread Safety

NetworkProvider extends Provider, which requires Send + Sync + 'static. Concrete implementations use background I/O threads that communicate with the main thread via std::sync::mpsc channels. drain_events drains the receiver end of that channel each frame. No locking is required on the call sites.

ProviderLifecycle::update(delta) is called once per frame by the engine loop. For network providers, update flushes the outbound send queue and transfers inbound data from the I/O thread channel into the event buffer that drain_events returns. Providers that do not need per-frame work (e.g., NullNetProvider) implement update as a no-op returning Ok(()).


3.6 Layer Placement

ComponentLayerPath
NetworkProvider trait + supporting typesLayer 1goud_engine/src/libs/providers/network.rs
Concrete implementationsLayer 1goud_engine/src/libs/providers/impls/network/
ProviderRegistry (gains network field)Layer 2goud_engine/src/core/providers/registry.rs
FFI functionsLayer 3goud_engine/src/ffi/network.rs
SDK wrappersLayer 4generated via codegen from goud_sdk.schema.json

ProviderRegistry gains one optional field:

#![allow(unused)]
fn main() {
pub struct ProviderRegistry {
    pub render: Box<dyn RenderProvider>,
    pub physics: Box<dyn PhysicsProvider>,
    pub audio: Box<dyn AudioProvider>,
    pub input: Box<dyn InputProvider>,
    pub network: Option<Box<dyn NetworkProvider>>, // new
}
}

The field is Option because networking is not required — unlike render, physics, audio, and input, which every game needs at minimum as a Null*Provider. Most single-player games never touch networking. Making it Option avoids forcing a NullNetProvider allocation on every game and makes “networking not configured” a distinct, type-level state rather than a silent no-op. Call sites use if let Some(net) = registry.network.as_mut() (see §3.9). Open Question 1 tracks whether this should change.

The error type prerequisite from RFC-0001 §3.10 applies here too: GoudError must move to Layer 1 before NetworkProvider trait methods can return GoudResult<T> without a Layer 1 → Layer 2 import violation.


3.7 Error Handling

Network errors use the ProviderError variant established in RFC-0001 §3.9:

#![allow(unused)]
fn main() {
ProviderError { subsystem: "network", message: String }
}

Error codes 700–709 are reserved for the network subsystem, following the 10-codes-per-subsystem granularity established in RFC-0001 (600–609 render, 610–619 physics, etc.). The error_code match arm routes ProviderError by subsystem discriminator to the appropriate code range. The existing pattern in goud_engine/src/core/error/types.rs applies: the message carries a human-readable description; the code carries the machine-readable category.


3.8 FFI Boundary

SDK users select the transport at engine initialization. Per RFC-0001 §5, goud_game_create gains provider-type parameters; networking adds GoudNetworkType to that signature. Passing GoudNetworkType::Null leaves ProviderRegistry.network as None. SDK-level overloads may default to Null when the parameter is omitted.

Rust SDK users configure networking through the builder pattern established in RFC-0001 §3.5:

#![allow(unused)]
fn main() {
let game = GoudEngine::builder()
    .with_renderer(OpenGLRenderProvider::new(RenderConfig::default()))
    .with_network(UdpNetProvider::new(NetworkConfig::default()))
    .build()?;
}
#![allow(unused)]
fn main() {
#[repr(C)]
pub enum GoudNetworkType {
    Udp = 0,
    WebSocket = 1,
    Null = 99,
}
}

The FFI surface exposes init, connection management, send, and event drain:

#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn goud_network_host(
    game: *mut GoudGame,
    port: u16,
    max_connections: u32,
) -> i32 { ... }

#[no_mangle]
pub unsafe extern "C" fn goud_network_connect(
    game: *mut GoudGame,
    addr: *const c_char,
    out_conn: *mut u64,
) -> i32 { ... }

#[no_mangle]
pub unsafe extern "C" fn goud_network_send(
    game: *mut GoudGame,
    conn: u64,
    channel: u8,
    data: *const u8,
    len: usize,
) -> i32 { ... }

#[no_mangle]
pub unsafe extern "C" fn goud_network_drain_events(
    game: *mut GoudGame,
    out_buf: *mut GoudNetworkEvent,
    buf_len: usize,
    out_count: *mut usize,
) -> i32 { ... }
}

GoudNetworkEvent is a #[repr(C)] flat struct with a discriminant field:

#![allow(unused)]
fn main() {
#[repr(u32)]
pub enum GoudNetworkEventKind {
    Connected    = 0,
    Disconnected = 1,
    Received     = 2,
    Error        = 3,
}

/// C-compatible network event. Fields are variant-dependent:
/// - Connected/Disconnected: `conn` is set; `data`/`data_len` are zero/null.
/// - Received: `conn`, `channel`, `data`, `data_len` are set.
/// - Error: `conn` is set; `message` points to a null-terminated string.
#[repr(C)]
pub struct GoudNetworkEvent {
    pub kind: GoudNetworkEventKind,
    pub conn: u64,
    pub channel: u8,
    pub data: *const u8,
    pub data_len: usize,
    pub message: *const c_char,
}
}

All pointer parameters require null checks before dereferencing; each unsafe block carries a // SAFETY: comment per the FFI patterns rule. The data and message pointers are valid only until the next call to goud_network_drain_events; the provider owns the backing memory.


3.9 ECS Integration

ProviderRegistry is stored as a World resource. Systems access NetworkProvider through ProviderRegistry:

#![allow(unused)]
fn main() {
fn network_system(registry: &mut ProviderRegistry) {
    if let Some(net) = registry.network.as_mut() {
        for event in net.drain_events() {
            // handle event
        }
    }
}
}

No separate ECS component type is needed. The provider is a singleton resource, not a per-entity component.


3.10 Network Simulation (Deferred)

A NetworkSimWrapper decorator will wrap any NetworkProvider implementation and inject configurable latency, jitter, and packet loss for local development and testing. It implements NetworkProvider and delegates to the inner provider after applying simulation parameters.

Implementation is deferred to F25-13. This RFC defines the trait boundary that NetworkSimWrapper will target.


4. Alternatives Considered

Callback-based events

Passing closures or function pointers into the provider for event dispatch avoids the drain_events polling step. Closures with non-'static lifetimes are not object-safe, and 'static closures make it difficult to borrow game state during the callback. PhysicsProvider::drain_collision_events in RFC-0001 set the precedent; networking follows the same model.

Typed messages with generics

Generic methods (fn send<M: Serialize>) cannot appear in an object-safe trait. Serialization format is not a transport concern. Games choose their serialization layer independently of the transport.

Monolithic NetworkManager

A single NetworkManager struct hardcoded to one transport would not support transport swapping between desktop and web targets and would violate the provider pattern established in RFC-0001.

Async trait methods

No async runtime exists in the engine. Async traits via async-trait produce non-object-safe signatures. Background threads with channel-based communication achieve the same non-blocking I/O without an executor dependency.

PeerId instead of ConnectionId

A PeerId implies game-level identity (player slot, lobby position). ConnectionId is transport-level. Conflating them forces the network layer to understand game concepts. Game code maps ConnectionId to player identity; the provider does not.


5. Impact

This RFC introduces a new subsystem with no breaking changes to existing code.

  • ProviderRegistry: gains network: Option<Box<dyn NetworkProvider>>. Existing construction code continues to compile; the field defaults to None.
  • Error types: network errors use ProviderError { subsystem: "network", message } from RFC-0001; error codes 700–709 reserved. No new GoudError variant needed.
  • FFI: new goud_network_* functions in goud_engine/src/ffi/network.rs. No existing FFI function signatures change. C# bindings regenerate automatically on cargo build. Python generated/_ffi.py requires manual update.
  • SDK wrappers: generated from goud_sdk.schema.json for C#, Python, and TypeScript. No existing wrapper changes.
  • Examples: unaffected unless they explicitly opt in to networking.

Prerequisite: error type Layer 1 move, shared with RFC-0001 §3.10.


6. Open Questions

  1. Optional vs mandatory in ProviderRegistry. Using Option<Box<dyn NetworkProvider>> requires every network call site to unwrap. An alternative is always storing NullNetProvider and removing the Option. The choice affects how the engine signals “networking not configured” vs “networking failed.”

  2. Async I/O strategy. Background std::thread per provider is simple but wastes a thread for games not using networking. A shared I/O thread pool (or eventual tokio adoption) would be more efficient. The trait boundary does not constrain this; implementations may evolve.

  3. ConnectionId reuse policy. After a connection closes, can its ConnectionId value be assigned to a new connection? Reuse risks use-after-close bugs. Generational IDs (epoch + index) would eliminate the ambiguity at the cost of a wider type.

  4. Max message size enforcement. NetworkCapabilities::max_message_size declares the limit but the trait does not specify whether send silently truncates, returns an error, or panics on oversized messages. The current leaning is that implementations MUST return Err(ProviderError) on oversized data (no silent truncation, no panics), making a caller-side bounds check optional but safe. This must be confirmed before implementation.

  5. Encryption and TLS surface. HostConfig and connect have no TLS parameters. If encryption is required, HostConfig needs certificate paths or raw key material. Deferring this leaves a gap for any game that needs secure transport.

RFC-0003: UI Layout and Input


rfc: “0003” title: “UI Layout and Input Behavior” status: draft created: 2026-03-09 authors: [“aram-devdocs”] tracking-issue: “#236”

RFC-0003: UI Layout and Input Behavior

1. Summary

This RFC defines engine-internal UI layout and input behavior for UiNode trees. It covers anchors, margin/padding, flex row/column rules, layout recompute triggers, and input routing semantics (hit testing, hover, focus, activation, and event consumption). The scope is internal Rust engine behavior, and it does not add FFI or SDK surface yet.


2. Motivation

Issue #123 requires a clear UI behavior contract before implementation proceeds. Today, layout and input expectations are implicit, which can create inconsistent behavior across render/input code paths and follow-up features.

The engine needs:

  • Deterministic positioning rules for common anchoring and container layouts.
  • Predictable recomputation boundaries when UI trees change.
  • Deterministic input dispatch so UI and game input do not both process the same event in one frame.

3. Design

3.1 Scope and Non-Goals

  • Scope: Rust engine internal UI runtime behavior (UiNode layout + input dispatch semantics).
  • Non-goal: adding or freezing public FFI/SDK APIs in this RFC.
  • Non-goal: advanced text layout, grid layout, animation system, or styling/theme APIs.

3.2 UiNode Layout Properties

Each UiNode participates in layout with these conceptual properties:

  • anchor: TopLeft | Center | BottomRight | Stretch
  • margin: { left, right, top, bottom } (pixels)
  • padding: { left, right, top, bottom } (pixels)
  • layout: None | Flex { direction, justify, align_items, spacing }

Definitions:

  • node_rect: node border box in parent space.
  • content_rect: node_rect inset by padding; child layout is computed in content_rect.
  • Margins affect placement/sizing relative to parent content_rect.

3.3 Anchor Semantics

Anchors are resolved in parent content_rect:

  • TopLeft:
    • x = parent.x + margin.left
    • y = parent.y + margin.top
  • Center:
    • Node is centered in parent, then offset by margins:
    • x = parent.center_x - node.width/2 + margin.left - margin.right
    • y = parent.center_y - node.height/2 + margin.top - margin.bottom
  • BottomRight:
    • x = parent.max_x - margin.right - node.width
    • y = parent.max_y - margin.bottom - node.height
  • Stretch:
    • x = parent.x + margin.left
    • y = parent.y + margin.top
    • width = max(0, parent.width - margin.left - margin.right)
    • height = max(0, parent.height - margin.top - margin.bottom)

If Stretch applies on an axis, explicit width/height on that axis is ignored.

3.4 Margin and Padding Behavior

  • Margin is external spacing between a node and its parent’s content_rect.
  • Padding is internal spacing between node border and node child content.
  • Hit testing uses node_rect (not just content_rect).
  • Child layout never uses parent node_rect directly; it always uses parent content_rect.

3.5 Flex Layout

Flex applies to a node’s content_rect and lays out direct children that are visible and layout-participating.

  • direction:
    • Row: main axis = X, cross axis = Y
    • Column: main axis = Y, cross axis = X
  • justify:
    • Start | Center | End
    • Controls child group offset on main axis after total child size + spacing is known.
  • align_items:
    • Start | Center | End | Stretch
    • Controls each child on cross axis.
    • Stretch sets child cross size to remaining cross-axis space after margins.
  • spacing:
    • Fixed gap inserted between adjacent children.
    • Total gap = spacing * (child_count - 1) when child_count > 1.

Children are placed in tree order.

3.6 Layout Dirty/Recompute Rules

Layout recomputation uses dirty flags and runs on demand.

Mark layout dirty when:

  • Tree mutation: add/remove/reparent child.
  • Layout-affecting property mutation: anchor, margin, padding, size constraints, flex settings, visibility/layout participation.
  • Root viewport/window resize.

Rules:

  • Dirty state propagates from the changed node up to root.
  • The engine resolves layout at most once per frame, before render and before UI hit testing.
  • Multiple dirty events in one frame coalesce into one recompute pass.
  • Window resize forces a full root layout recompute.

3.7 Input Semantics

Input dispatch order for each frame is: OS events -> UI system -> game input system.

Topmost hit testing

  • Pointer hit testing uses final layout rects from this frame.
  • Traversal order for picking is reverse paint order (topmost visual node wins).
  • Only visible and input-enabled nodes are hit candidates.

Hover enter/leave

  • Engine tracks current hovered node per pointer.
  • On pointer move, if hovered target changes:
    • Dispatch HoverLeave for previous node.
    • Dispatch HoverEnter for new node.
  • Order is always leave then enter in the same frame.

Focus traversal (Tab)

  • Focusable set: visible + enabled + focusable nodes.
  • Traversal order: deterministic tree order.
  • Tab moves focus forward; Shift+Tab moves backward.
  • Traversal wraps at ends (last -> first, first -> last).

Keyboard activation for focused button

  • If focused node is a button:
    • Enter triggers activation.
    • Space triggers activation.
  • Activation emits the same logical action as pointer click on that button.

Event consumption boundary

  • When UI handles an event (pointer hit on interactive node, focus traversal, button activation), it marks that event consumed for the current frame.
  • Consumed events MUST NOT be forwarded to game input in the same frame.
  • Unhandled UI events continue to game input processing.

4. Alternatives Considered

  1. Immediate-mode UI only (no retained UiNode tree). Rejected because current engine direction already uses retained node/component state and requires persistent focus/hover behavior.

  2. Single-pass input without consumption. Rejected because UI and gameplay would both react to the same input, causing double-activation bugs.

  3. Public FFI-first UI API in this RFC. Rejected because behavior needs stabilization internally before freezing cross-language surface.


5. Impact

  • Engine internals gain a precise behavior contract for UI layout and input.
  • No new FFI exports or SDK wrappers are defined by this RFC.
  • Existing game input paths must honor UI event consumption to avoid duplicate handling.
  • Implementation work can proceed behind internal interfaces, with FFI/API shape deferred to a later RFC.

6. Open Questions

  1. Should focus traversal order be configurable (tree order vs explicit tab index) in a follow-up?
  2. Should button activation differentiate keydown/keyup timing for Space for stricter accessibility parity?
  3. Should clipping and scroll containers alter hit testing rules in a follow-up RFC?

RFC-0004: Debugger Runtime


RFC-0004: Debugger Runtime, Snapshot Contract, and Local Attach Model

1. Summary

Phase 2.5.1 needs a fixed contract for GoudEngine’s debugger stack. This RFC sets the runtime topology to one Rust-owned debugger service per process in dev mode, one out-of-process goudengine-mcp bridge that speaks MCP over stdio, one shared snapshot and service-health schema, one local-only attach model, and one debugger enablement contract that spans GameConfig, EngineConfig, and a future config-based GoudContext path. The batch only sets the contract. It does not implement the debugger runtime, FFI, SDK rollout, capture, replay, or the MCP bridge.

2. Motivation

Phase 2.5 pulls debugger, profiling, replay, observability, and AI-agent runtime tooling into Alpha v1. The later implementation batches need a fixed contract before code lands. Without that contract, the runtime service, FFI layer, SDK wrappers, overlays, Feature Lab, and goudengine-mcp bridge can drift into incompatible shapes.

The current codebase already exposes several narrow debug surfaces:

  • goud_engine/src/sdk/game_config.rs carries show_fps_overlay, physics_debug, and diagnostic_mode.
  • goud_engine/src/sdk/engine_config.rs forwards part of that config model for windowed game creation.
  • goud_engine/src/sdk/context.rs exposes a bare GoudContext lifecycle API with no config object.
  • goud_engine/src/ffi/debug.rs and goud_engine/src/ffi/window/state.rs expose point solutions for FPS and diagnostic state.

Those pieces are useful, but they do not define:

  • one process-wide debugger runtime,
  • one route identity for multi-context processes,
  • one semantic snapshot schema for agents and overlays,
  • one local attach protocol,
  • or one debugger-mode contract across init surfaces.

The document resolves four blocking issues:

  • #511 debugger singleton architecture and MCP contract,
  • #513 snapshot schema and service-health model,
  • #517 local transport, attach protocol, and local-only security policy,
  • #520 shared debugger enablement contract.

3. Design

3.1 Scope and Non-Goals

Scope:

  • Process model for the debugger runtime and the goudengine-mcp bridge.
  • Shared type contract for runtime identity, local discovery, attach, snapshots, and service health.
  • Enablement contract for GameConfig, EngineConfig, and a future config-based GoudContext flow.
  • Approval gate that later Phase 2.5 batches must follow.

Non-goals:

  • Implementing the debugger runtime service (#512).
  • Implementing the MCP bridge (#518).
  • Adding FFI exports, codegen schema, or SDK wrappers (#230, #521-#524).
  • Embedding an MCP stdio server inside game processes.
  • Making TypeScript web or browser remote attach a Phase 2.5 gate.
  • Defining a remote network protocol. This phase is local-only.

3.2 Runtime Topology and Ownership

The debugger stack has exactly two runtime participants in this phase:

ComponentProcessOwnerResponsibility
DebuggerRuntimeGame processRust engineProduces snapshots, service health, stats, control hooks, capture/replay hooks, and route registration
goudengine-mcpSeparate local processLocal developer toolSpeaks MCP over stdio and forwards requests over local attach transport

Topology rules:

  • One DebuggerRuntime exists per process when debugger mode is enabled in a supported dev-mode flow.
  • The runtime is Rust-owned and lives in Layer 2. SDKs do not create parallel debugger services.
  • The runtime registers one or more debuggable routes for the contexts in that process.
  • goudengine-mcp is thin. It translates MCP requests into debugger attach, snapshot, inspection, control, capture, and replay requests. It does not own engine state.
  • The game process does not implement MCP directly and never opens a stdio MCP endpoint for agents.

Conceptual flow: agent <-> stdio <-> goudengine-mcp <-> local socket/pipe <-> DebuggerRuntime, with overlays, FFI exports, SDK convenience APIs, and Feature Lab all consuming the same runtime contract.

3.3 Route Identity and Lifecycle

Each debuggable context in a process gets a stable route identifier for the lifetime of that process:

#![allow(unused)]
fn main() {
pub struct RuntimeRouteId {
    pub process_nonce: u64,
    pub context_id: u64,
    pub surface_kind: RuntimeSurfaceKind,
}

pub enum RuntimeSurfaceKind {
    WindowedGame,
    HeadlessContext,
    ToolContext,
}
}

Rules:

  • process_nonce is generated once at runtime-service startup and changes on each process start.
  • context_id maps to the existing engine context identity used by runtime code and FFI.
  • surface_kind disambiguates windowed and headless flows that may share engine infrastructure. Wire serialization of RuntimeSurfaceKind must use "windowed_game", "headless_context", and "tool_context", and parsers must reject other spellings such as WindowedGame.
  • A route is stable until that context detaches or the process exits.
  • A detached route disappears from discovery and returns an attach error if selected later.

Lifecycle:

  1. Debugger mode is enabled during engine/context creation.
  2. The process creates or reuses the process-wide DebuggerRuntime.
  3. Each eligible context registers a RuntimeRouteId with the runtime.
  4. The runtime publishes discovery metadata for the process and its routes only when publish_local_attach = true and at least one attachable route exists.
  5. Local tools attach to one route at a time.
  6. Context shutdown removes that route.
  7. Process shutdown removes discovery metadata and closes any published local transport endpoint.

3.4 Capability Surface

The debugger runtime exposes one canonical capability surface. Later batches may implement pieces incrementally, but they must preserve these categories and names:

CapabilityConsumer examplesProducer/owner
Snapshotsoverlays, MCP, SDK helpersdebugger runtime core
Profilingoverlays, MCP, exportsprofiler subsystem through debugger runtime
Render statsoverlays, MCPrenderer adapter through debugger runtime
Memory statsoverlays, MCPmemory/statistics adapter through debugger runtime
Entity inspectionoverlays, MCP, SDK helpersscene/entity inspector through debugger runtime
Debug drawoverlays, SDK helpersdebug-draw/control layer
Control planeoverlays, MCP, SDK helpersdebugger runtime control layer
ReplayMCP, SDK helpersreplay subsystem through debugger runtime
CaptureMCP, SDK helperscapture subsystem through debugger runtime
SDK knowledgeMCP resources/promptsgoudengine-mcp bridge only; not route-scoped

Route wire keys are snapshots, profiling, render_stats, memory_stats, entity_inspection, debug_draw, control_plane, replay, and capture. sdk_knowledge is a bridge-only capability key and is not part of runtime-published route capability maps.

Control plane scope in this phase covers pause/resume, single-step, time-scale changes, route selection, entity selection, debug toggle state, and input injection where supported by later implementation work.

3.5 Runtime Service, FFI Surface, SDK APIs, and Bridge

The ownership chain is fixed:

  1. DebuggerRuntime is the source of truth.
  2. FFI exports expose runtime-owned operations and snapshots to non-Rust SDKs.
  3. SDK convenience APIs wrap the FFI or Rust runtime surface. They do not invent SDK-local debugger behavior.
  4. goudengine-mcp attaches through the same local runtime contract and does not bypass the runtime with private side channels.

Implications:

  • overlays, FFI, SDK helpers, Feature Lab, and the bridge all consume the same route IDs, capability names, snapshot schema, and health model;
  • future public APIs may be ergonomic, but they must not redefine the underlying contract;
  • later docs and SDK guides may specialize for language ergonomics, but not for topology.

3.6 Shared Debugger Enablement Contract

Debugger mode is a Rust-owned pre-init concept. The canonical types are:

#![allow(unused)]
fn main() {
pub struct DebuggerConfig {
    pub enabled: bool,
    pub publish_local_attach: bool,
    pub route_label: Option<String>,
}

pub struct ContextConfig {
    pub debugger: DebuggerConfig,
}
}

Contract rules:

  • DebuggerConfig is the canonical debugger-mode value object.
  • enabled = false means no debugger runtime startup, no route registration, and near-zero overhead outside existing debug features.
  • publish_local_attach = true allows the runtime to publish local discovery metadata and accept local attachments.
  • route_label is optional display metadata for tools. It is not the stable route identity.

Init-surface mapping:

SurfaceContract
GameConfigGains debugger: DebuggerConfig as the source of debugger-mode settings for windowed Rust flows
EngineConfigWraps the same GameConfig.debugger contract and exposes builder helpers without redefining it
GoudContextGains a future config-based constructor path such as GoudContext::with_config(ContextConfig) while the bare constructor remains shorthand for defaults

Compatibility rules:

  • The bare GoudContext() / goud_context_create() path remains valid and defaults to debugger disabled.
  • The canonical path for debugger mode is pre-init configuration, not a post-create toggle.
  • Later FFI and codegen work must expose the same Rust-owned DebuggerConfig / ContextConfig model instead of per-SDK divergence.
  • TypeScript desktop follows the shared config model. TypeScript web/browser remote attach remains out of gate scope.

3.7 Local Discovery Contract

Discovery is manifest-based and snapshot-oriented, not streaming. Each process with debugger mode enabled and publish_local_attach = true publishes one manifest while at least one attachable route exists.

#![allow(unused)]
fn main() {
pub struct LocalEndpointV1 {
    pub transport: String,
    pub location: String,
}

pub struct RouteSummaryV1 {
    pub route_id: RuntimeRouteId,
    pub label: Option<String>,
    pub attachable: bool,
    pub capabilities: std::collections::BTreeMap<String, CapabilityStateV1>,
}

pub struct RuntimeManifestV1 {
    pub manifest_version: u32,
    pub pid: u32,
    pub process_nonce: u64,
    pub executable: String,
    pub endpoint: LocalEndpointV1,
    pub routes: Vec<RouteSummaryV1>,
    pub published_at_unix_ms: u64,
}
}

LocalEndpointV1.transport is "unix" on macOS/Linux and "named_pipe" on Windows. location is an absolute socket path or a full pipe name. RouteSummaryV1.route_id.surface_kind is the only surface discriminator. RouteSummaryV1.capabilities must include every route-scoped wire key from Section 3.4 and use the states from Section 3.10.1; unsupported features stay present with disabled or unavailable instead of omission. sdk_knowledge is bridge-only and excluded from runtime-published route maps. RuntimeManifestV1.manifest_version is fixed to 1 in this phase.

Manifest placement:

  • macOS/Linux: publish in $XDG_RUNTIME_DIR/goudengine/ when available, otherwise /tmp/goudengine-<uid>/.
  • Windows: publish in %LOCALAPPDATA%\\GoudEngine\\runtime\\.
  • If a Unix socket path would exceed platform limits, implementations must fall back to a hashed basename under a short root such as /tmp/goudengine-<uid>/s/, while still encoding pid and process_nonce in the manifest location. Operational rules:
  • One manifest per process, not per context, and no manifest when publish_local_attach = false or no attachable routes exist.
  • The manifest lists all current routes for that process and is rewritten with a new, strictly monotonic published_at_unix_ms = max(now_ms, last_published_at_unix_ms + 1) on any manifest field change.
  • Manifest updates must use atomic replace semantics, such as write-temp-then-rename, so tools never observe partial files.
  • For the same pid and process_nonce, the highest published_at_unix_ms is authoritative; if multiple updates fall in the same millisecond, the runtime must increment the value to preserve strict monotonicity, and older copies are stale.
  • Manifest filenames and LocalEndpointV1.location must include both pid and process_nonce.
  • Readers must treat manifests as stale when the pid is no longer live.
  • An endpoint-open failure requires exactly one local retry after 100 ms before the manifest is ignored for the current discovery invocation. Readers SHOULD perform that retry asynchronously, and any success from that retry may appear only on the next explicit manifest-directory scan rather than mutating results already returned. Pruning is reserved for dead-pid cases.
  • The manifest is local developer metadata only. It is never a remote discovery protocol.

3.8 Local Attach Transport and Handshake

Transport is OS-local IPC: Unix domain sockets on macOS/Linux and named pipes on Windows. Out of scope for this phase: TCP listen sockets, WebSocket listeners, remote host binding, and any release gate that depends on cross-machine attach.

  • Frame taxonomy: v1 allows only client requests, runtime responses, {"type":"heartbeat"}, and {"type":"heartbeat_ack"}. Unsolicited runtime->client notifications are forbidden.
  • Framing: every message is one 4-byte little-endian length prefix plus one UTF-8 JSON object, with a 1 MiB maximum frame size. Lengths above the limit must return protocol_error and immediately close the session without draining the frame.
  • Versioning: AttachHelloV1.protocol_version and AttachAcceptedV1.protocol_version are fixed to 1 in this phase.
  • Heartbeat sender: goudengine-mcp sends {"type":"heartbeat"} and the runtime only replies with {"type":"heartbeat_ack"}; heartbeat frames are out-of-band and do not count against the one in-flight request limit.
  • Heartbeat interval: if heartbeat_interval_ms is 0, heartbeats are disabled. Otherwise the client sends a heartbeat after one idle interval without client->runtime traffic.
  • Heartbeat timeout: a sent heartbeat is satisfied only by {"type":"heartbeat_ack"}; other runtime->client frames do not clear it. The client closes when a sent heartbeat is not acknowledged within exactly 2 * heartbeat_interval_ms, measured from send time on a monotonic clock rather than wall time.
  • Error handling: attach sessions allow only one in-flight request at a time, and a pipelined request before the active response completes is protocol_error plus immediate session close. Failures use {"type":"error","code":"...","message":"..."} with codes from protocol_error, version_mismatch, route_not_found, route_not_attachable, and attach_disabled; unknown codes are extensions and still fail the active request. The first request/response pair uses:
#![allow(unused)]
fn main() {
pub struct AttachHelloV1 {
    pub protocol_version: u32,
    pub client_name: String,
    pub client_pid: u32,
    pub route_id: RuntimeRouteId,
}

pub struct AttachAcceptedV1 {
    pub protocol_version: u32,
    pub session_id: u64,
    pub route_id: RuntimeRouteId,
    pub snapshot_schema: String,
    pub heartbeat_interval_ms: u32,
}
}

Handshake rules:

  • route_id is required. Multi-route processes must not rely on implicit selection.
  • The runtime rejects unknown or detached routes.
  • The runtime rejects protocol-version mismatches.
  • The runtime may reject attach when debugger mode is disabled or local attach publication is disabled.
  • Accepted sessions are local only and scoped to one selected route.
  • The bridge may open multiple sessions to different routes, but each session binds to one route ID.

3.9 Local-Only Security Policy

Security posture for this phase:

  • opt-in only through debugger-mode configuration,
  • local IPC only,
  • no remote bind,
  • no unauthenticated network-facing debugger endpoint,
  • and no browser remote attach requirement.

Trust boundaries:

  • The local machine user account is the trust boundary for this phase.
  • goudengine-mcp is trusted as a local developer tool once it reaches the local IPC endpoint.
  • The manifest reveals only local debugging metadata needed to choose a route.

Required behavior:

  • Release-oriented builds may ignore debugger enablement or compile out discovery publication.
  • Local attach is disabled unless debugger mode is enabled.
  • Manifest directories must be owner-only where the OS supports it: 0700 directories and 0600 manifest files on Unix-like systems, restrictive per-user ACLs under %LOCALAPPDATA% on Windows.
  • Unix socket endpoints must live inside an owner-only directory; the runtime must refuse to publish a socket in a broader directory.
  • Windows named pipes must grant access only to the current user, SYSTEM, and local Administrators; the runtime must refuse broader pipe ACLs.
  • Future remote attach, auth, or sandboxing work is follow-up scope and must not be backported into this phase as an implicit gate.

3.10 Snapshot and Service-Health Schema

3.10.1 Capability state model

All route capabilities use one state enum:

StateMeaning
readyData or control path is available and functioning
disabledFeature exists but is turned off for this runtime
unavailableFeature does not exist for this platform/runtime/provider combination
faultedFeature should exist, but the runtime detected an error or degraded state

3.10.2 Service health

ServiceHealthV1 is the shared status shape for subsystems and debugger-owned services:

#![allow(unused)]
fn main() {
pub struct ServiceHealthV1 {
    pub name: String,
    pub state: CapabilityStateV1,
    pub owner: String,
    pub detail: Option<String>,
    pub updated_frame: u64,
}
}

services must include exactly one entry for each of renderer, memory, profiling, physics, audio, network, window, assets, capture, replay, and debugger, using disabled, unavailable, or faulted instead of omission.

owner is a closed wire-literal set: renderer_adapter, memory_adapter, physics_adapter, audio_adapter, network_adapter, window_adapter, asset_manager, capture_subsystem, replay_subsystem, and debugger_runtime. Required mappings are renderer -> renderer_adapter, memory -> memory_adapter, physics -> physics_adapter, audio -> audio_adapter, network -> network_adapter, window -> window_adapter, assets -> asset_manager, capture -> capture_subsystem, replay -> replay_subsystem, and debugger -> debugger_runtime.

3.10.3 Snapshot shape

DebuggerSnapshotV1 is the canonical semantic view of one route. snapshot_version is fixed to 1 in this phase:

#![allow(unused)]
fn main() {
pub struct DebuggerSnapshotV1 {
    pub snapshot_version: u32,
    pub route_id: RuntimeRouteId,
    pub frame: FrameStateV1,
    pub selection: SelectionStateV1,
    pub scene: SceneStateV1,
    pub entities: Vec<EntityStateV1>,
    pub services: Vec<ServiceHealthV1>,
    pub stats: SnapshotStatsV1,
    pub diagnostics: DiagnosticsStateV1,
    pub debugger: DebuggerStateV1,
}
}
#![allow(unused)]
fn main() {
pub struct FrameStateV1 { pub index: u64, pub delta_seconds: f32, pub total_seconds: f64 }
pub struct SelectionStateV1 { pub scene_id: String, pub entity_id: Option<u64> }
pub struct SceneStateV1 { pub active_scene: String, pub entity_count: u32 }
pub struct EntityStateV1 { pub entity_id: u64, pub name: Option<String>, pub components: std::collections::BTreeMap<String, serde_json::Value> }
pub struct RenderStatsV1 { pub draw_calls: u32 }
pub struct MemoryStatsV1 { pub tracked_bytes: u64 }
pub struct NetworkStatsV1 { pub bytes_sent: u64, pub bytes_received: u64 }
pub struct SnapshotStatsV1 { pub render: RenderStatsV1, pub memory: MemoryStatsV1, pub network: NetworkStatsV1 }
pub struct DiagnosticsStateV1 { pub errors: Vec<String>, pub last_fault: Option<String> }
pub struct DebuggerStateV1 { pub paused: bool, pub time_scale: f32, pub attached_clients: u32 }
}

All fields are required unless wrapped in Option<T>; entities and components maps may be empty, but required services and capability maps must be present, complete, and unique by name/key. Field ownership:

Snapshot sectionProduced by
route_id, debugger, services.debuggerdebugger runtime
framedebugger runtime frame coordinator
selection, scene, entitiesscene/entity inspector
services.renderer, stats.renderrenderer adapter
services.memory, stats.memorymemory/statistics adapter
services.profilingprofiler subsystem through debugger runtime
services.physicsphysics adapter
services.audioaudio adapter
services.network, stats.networknetwork adapter
services.windowwindow/platform adapter
services.assetsasset manager
services.capturecapture subsystem
services.replayreplay subsystem
diagnosticserror/diagnostic subsystem plus debugger runtime aggregation

Minimum semantic coverage includes frame timing and frame index, selected scene/entity state, inspected component state for the current entity selection, provider capability and health, render/memory/network stats, replay and capture status, debugger health, and current errors or diagnostics suitable for agent consumption. When one of the modeled stats producers (render, memory, or network) is disabled or unavailable, its stats object remains present with zero/default values and the service state is authoritative.

3.10.4 JSON example: ServiceHealthV1

{
  "name": "renderer",
  "state": "ready",
  "owner": "renderer_adapter",
  "detail": null,
  "updated_frame": 4812
}

3.10.5 JSON excerpt: DebuggerSnapshotV1

The services array below is abbreviated for brevity; conforming snapshots still include exactly one entry for every required service name.

{
  "snapshot_version": 1,
  "route_id": {
    "process_nonce": 44199288,
    "context_id": 3,
    "surface_kind": "windowed_game"
  },
  "frame": {
    "index": 4812,
    "delta_seconds": 0.0166,
    "total_seconds": 79.84
  },
  "selection": {
    "scene_id": "default",
    "entity_id": 42
  },
  "scene": {
    "active_scene": "default",
    "entity_count": 118
  },
  "entities": [
    {
      "entity_id": 42,
      "name": "Player",
      "components": {
        "Transform2D": {
          "x": 144.0,
          "y": 96.0,
          "rotation_deg": 0.0
        },
        "Sprite": {
          "texture": "player_idle"
        }
      }
    }
  ],
  "services": [
    {
      "name": "renderer",
      "state": "ready",
      "owner": "renderer_adapter",
      "detail": null,
      "updated_frame": 4812
    },
    {
      "name": "replay",
      "state": "disabled",
      "owner": "replay_subsystem",
      "detail": "replay not active for this route",
      "updated_frame": 4812
    }
  ],
  "stats": {
    "render": {
      "draw_calls": 88
    },
    "memory": {
      "tracked_bytes": 12582912
    },
    "network": {
      "bytes_sent": 0,
      "bytes_received": 0
    }
  },
  "diagnostics": {
    "errors": [],
    "last_fault": null
  },
  "debugger": {
    "paused": false,
    "time_scale": 1.0,
    "attached_clients": 1
  }
}

3.11 Approval Gate for Later Phase 2.5 Batches

Phase 2.5.2 through 2.5.5 are approved to proceed only if they preserve this RFC’s fixed decisions:

BatchMust preserve
2.5.2 engine substrateone process-wide runtime, route registration, runtime-owned capabilities
2.5.3 control/protocol/replaylocal-only attach model, route-scoped sessions, snapshot/service-health names
2.5.4 public surface rolloutone Rust-owned enablement model and no SDK-local debugger runtimes
2.5.5 DX/docs/Feature Labbridge-first local attach workflow and TypeScript web out-of-gate wording

Any later work that proposes embedded MCP in the game process, remote bind as a release gate, a post-create-only debugger toggle as the canonical path, or a different snapshot-schema or route-identity model must first update or supersede this RFC.


4. Alternatives Considered

  1. Embedded MCP stdio server inside each game process. Rejected because it couples agent protocol concerns to the runtime process and creates multiple MCP hosts instead of one thin bridge.

  2. Separate debugger enablement models per SDK. Rejected because the later FFI and codegen rollout would fragment the contract and create SDK-only behavior.

  3. Post-create debugger enablement as the canonical GoudContext path. Rejected because GameConfig and EngineConfig are already pre-init configuration surfaces. A future config-based ContextConfig keeps the model aligned.

  4. Remote TCP attach in this phase. Rejected because the acceptance criteria only require local developer attach and explicitly exclude remote bind as a release gate.

  5. Separate schema file in this batch. Rejected because this batch is a contract gate, not an implementation batch. A structured appendix in the RFC is enough to unblock runtime, FFI, SDK, and MCP work without adding a second source of truth yet.

5. Impact

  • This RFC is docs only. No engine, FFI, SDK, codegen, or example behavior changes land in this batch.
  • Later implementation work must add one runtime-owned debugger path instead of extending today’s point debug features independently.
  • GameConfig and EngineConfig will converge on DebuggerConfig.
  • Standalone GoudContext will need a future config-based constructor path, while existing bare constructors remain valid with debugger disabled by default.
  • Desktop native flows are the Phase 2.5 gate. TypeScript web/browser attach remains follow-up work.

6. Open Questions

  1. Should future attach sessions expose one multiplexed connection per process or keep one session per route even after the bridge supports route switching?

API Reference

API documentation is generated from source code and is not checked into the repository. Generate it locally using the commands below.

Reusable code snippets are generated separately from validated example sources: Reusable Snippets (Generated)

Rust

Full Rust API docs generated by cargo doc.

cargo doc --open                    # Build and open in browser
cargo doc --no-deps --open          # Skip dependency docs

Output is written to target/doc/. Open target/doc/goud_engine/index.html in a browser.

C#

C# API docs can be generated with DocFX from XML doc comments:

cd sdks/csharp
dotnet build -p:GenerateDocumentationFile=true
# Then run DocFX or similar tooling against the generated XML

Python

Python API docs can be generated with pdoc from docstrings:

pdoc sdks/python/goudengine --output-dir docs/api/python

TypeScript

TypeScript API docs can be generated with TypeDoc from JSDoc comments:

cd sdks/typescript
npx typedoc --out ../../docs/api/typescript src/index.ts

Reusable Snippets (Generated)

This page is generated from validated sources:

  • Sandbox examples exercised by parity checks
  • SDK generated entrypoints emitted by codegen

Rust Sandbox Setup

Source: examples/rust/sandbox/src/main.rs

#![allow(unused)]
fn main() {
    let mut game = GoudGame::with_platform(
        GameConfig::new("GoudEngine Sandbox - Rust", WINDOW_WIDTH, WINDOW_HEIGHT).with_debugger(
            DebuggerConfig {
                enabled: true,
                publish_local_attach: true,
                route_label: Some("sandbox-rust".to_string()),
            },
        ),
}

C# Sandbox Setup

Source: examples/csharp/sandbox/Program.cs

        using var engineConfig = new EngineConfig()
            .SetTitle($"{assets.Title} - C#")
            .SetSize(WindowWidth, WindowHeight)
            .SetDebugger(new DebuggerConfig(true, true, "sandbox-csharp"));
        using var game = engineConfig.Build();
        using var sceneContext = new GoudContext();
        using var network = new NetworkState(assets.Port);
        using var ui = BuildUi();

Python Sandbox Startup

Source: examples/python/sandbox.py

    config = EngineConfig()
    config.set_title("GoudEngine Sandbox - Python")
    config.set_size(WINDOW_WIDTH, WINDOW_HEIGHT)
    config.set_debugger(DebuggerConfig(
        enabled=True,
        publish_local_attach=True,
        route_label="sandbox-python",
    ))

TypeScript Sandbox Asset Loading

Source: examples/typescript/sandbox/sandbox.ts

    const background = await game.loadTexture(config.background);
    const sprite = await game.loadTexture(config.sprite);
    const accentSprite = await game.loadTexture(config.accentSprite);
    const font = await game.loadFont(config.font);
    return new SandboxApp(game, ui, target, config, network, options?.maxRuntimeSec ?? 0, {
      background,
      sprite,
      accentSprite,

Python Generated Public Exports

Source: sdks/python/goudengine/generated/__init__.py

"""This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT."""

from ._types import Color, Vec2, Rect, Transform2D, Sprite, Entity, Transform2DBuilder, SpriteBuilder
from ._keys import BlendMode, BodyType, CoordinateOrigin, DebuggerStepKind, EasingType, EventPayloadType, Key, MouseButton, NetworkProtocol, OverlayCorner, PhysicsBackend2D, PlaybackMode, RenderBackendKind, RpcDirection, ShapeType, TextAlignment, TextDirection, TransitionType, WindowBackendKind
from ._game import GoudGame, GoudContext, PhysicsWorld2D, PhysicsWorld3D, EngineConfig, UiManager
from ._diagnostic import DiagnosticMode

__all__ = [
    "GoudGame",
    "GoudContext",

TypeScript Generated Public Exports

Source: sdks/typescript/src/generated/node/index.g.ts

// This file is AUTO-GENERATED by GoudEngine codegen. DO NOT EDIT.

import type { IGoudGame, IUiManager, IUiStyle, IUiEvent, UiNodeId, IEntity, IColor, IVec2, IVec3, ITransform2DData, ISpriteData, IRenderStats, IContact, IFpsStats, IRenderMetrics, IFramePhaseTimings, IDebuggerConfig, IContextConfig, IMemoryCategoryStats, IMemorySummary, IDebuggerCapture, IDebuggerReplayArtifact, IPhysicsRaycastHit2D, IPhysicsCollisionEvent2D, IAnimationEventData, IPreloadAssetRequest, IPreloadOptions, IPreloadProgress, IRenderCapabilities, IPhysicsCapabilities, IAudioCapabilities, IInputCapabilities, INetworkCapabilities, INetworkStats, INetworkSimulationConfig, IPhysicsWorld2D, IPhysicsWorld3D, IP2pMeshConfig, IRollbackConfig, IBoundingBox3D, ICharacterMoveResult, PreloadAssetInput, PreloadAssetKind, ISpriteCmd, ITextCmd } from '../types/engine.g.js';
import { PhysicsBackend2D, RenderBackendKind, WindowBackendKind } from '../types/input.g.js';
import { Color, Vec2, Vec3 } from '../types/math.g.js';
export { Color, Vec2, Vec3 } from '../types/math.g.js';
export { Key, MouseButton, PhysicsBackend2D, RenderBackendKind, WindowBackendKind } from '../types/input.g.js';
export type { IGoudGame, IUiManager, IUiStyle, IUiEvent, UiNodeId, IEntity, IColor, IVec2, IVec3, ITransform2DData, ISpriteData, IRenderStats, IContact, IFpsStats, IRenderMetrics, IFramePhaseTimings, IDebuggerConfig, IContextConfig, IMemoryCategoryStats, IMemorySummary, IDebuggerCapture, IDebuggerReplayArtifact, IPhysicsRaycastHit2D, IPhysicsCollisionEvent2D, IAnimationEventData, IRenderCapabilities, IPhysicsCapabilities, IAudioCapabilities, IInputCapabilities, INetworkCapabilities, INetworkStats, INetworkSimulationConfig, IPhysicsWorld2D, IPhysicsWorld3D, IP2pMeshConfig, IRollbackConfig, IBoundingBox3D, ICharacterMoveResult } from '../types/engine.g.js';

export interface INetworkConnectResult { handle: number; peerId: number; }
export interface INetworkPacket { peerId: number; data: Uint8Array; }
export interface IGoudContext {
  destroy(): boolean;
  isValid(): boolean;

husky-rs

CI Crates.io Documentation License

husky-rs is a Git hooks management tool for Rust projects, inspired by Husky.

Features

  • Easy setup and configuration
  • Automatic installation of Git hooks
  • Support for all Git hooks
  • Cross-platform compatibility (Unix-like systems and Windows)

Quick Start

  1. Adding husky-rs to your project:

    You have several options:

    # Option 1: Add as a Regular Dependency
    cargo add husky-rs
    
    # Option 2: Add as a Dev Dependency
    cargo add --dev husky-rs
    
    # Option 3: Use the Main Branch
    cargo add --git https://github.com/pplmx/husky-rs --branch main
    cargo add --dev --git https://github.com/pplmx/husky-rs --branch main
    
  2. Create hooks directory:

    mkdir -p .husky/hooks
    
  3. Add a hook (e.g., pre-commit):

    echo '#!/bin/sh\necho "Running pre-commit hook"' > .husky/hooks/pre-commit
    
  4. Install hooks:

    Note: Due to the execution mechanism of build.rs, running cargo clean is required when installing or updating hooks.

    cargo clean && cargo test
    

Tip: If you add this library to the [dependencies] section, both cargo build and cargo test will work. However, if it’s added under [dev-dependencies], only cargo test will function as expected.

Usage

Supported Git Hooks

husky-rs aims to support a wide range of Git hooks, including:

  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • pre-push

For a complete list of supported hooks, refer to the Git documentation.

If you encounter any unsupported hooks, please don’t hesitate to open an issue.

Configuration

To skip hook installation:

NO_HUSKY_HOOKS=1 cargo build

Best Practices

  • Keep hooks lightweight to avoid slowing down Git operations
  • Use hooks for tasks like running tests, linting code, and validating commit messages
  • Non-zero exit status in a hook script will abort the Git operation

Development

For information on setting up the development environment, running tests, and contributing to the project, please refer to our Development Guide.

Troubleshooting

If you encounter any issues while using husky-rs, please check our Troubleshooting Guide for common problems and their solutions. If you can’t find a solution to your problem, please open an issue on our GitHub repository.

Contributing

We welcome contributions! Please see our Contributing Guide for details on how to submit pull requests, report issues, or suggest improvements.

License

This project is licensed under either of:

  • Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)

at your option.

Changelog

For a detailed history of changes to this project, please refer to our CHANGELOG.md.

Acknowledgments

  • Inspired by cargo-husky
  • Thanks to the Rust community for their amazing tools and libraries

cbindgen User Guide

cbindgen creates C/C++11 headers for Rust libraries which expose a public C API.

While you could do this by hand, it’s not a particularly good use of your time. It’s also much more likely to be error-prone than machine-generated headers that are based on your actual Rust code. The cbindgen developers have also worked closely with the developers of Rust to ensure that the headers we generate reflect actual guarantees about Rust’s type layout and ABI.

C++ headers are nice because we can use operator overloads, constructors, enum classes, and templates to make the API more ergonomic and Rust-like. C headers are nice because you can be more confident that whoever you’re interoperating with can handle them. With cbindgen you don’t need to choose! You can just tell it to emit both from the same Rust library.

There are two ways to use cbindgen: as a standalone program, or as a library (presumably in your build.rs). There isn’t really much practical difference, because cbindgen is a simple rust library with no interesting dependencies. Using it as a program means people building your software will need it installed. Using it in your library means people may have to build cbindgen more frequently (e.g. every time they update their rust compiler).

It’s worth noting that the development of cbindgen has been largely adhoc, as features have been added to support the usecases of the maintainers. This means cbindgen may randomly fail to support some particular situation simply because no one has put in the effort to handle it yet. Please file an issue if you run into such a situation. Although since we all have other jobs, you might need to do the implementation work too :)

Quick Start

To install cbindgen, you just need to run

cargo install --force cbindgen

(–force just makes it update to the latest cbindgen if it’s already installed)

To use cbindgen you need two things:

  • A configuration (cbindgen.toml, which can be empty to start)
  • A Rust crate with a public C API

Then all you need to do is run it:

cbindgen --config cbindgen.toml --crate my_rust_library --output my_header.h

This produces a header file for C++. For C, add the --lang c switch.
cbindgen also supports generation of Cython bindings, use --lang cython for that.

See cbindgen --help for more options.

Get a template cbindgen.toml here.

build.rs

If you don’t want to use cbindgen as an application, here’s an example build.rs script:

extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    cbindgen::Builder::new()
      .with_crate(crate_dir)
      .generate()
      .expect("Unable to generate bindings")
      .write_to_file("bindings.h");
}

You can add configuration options using the Builder interface.

When actively working on code, you likely don’t want cbindgen to fail the entire build. Instead of expect-ing the result of the header generation, you could ignore parse errors and let rustc or your code analysis bring up:

#![allow(unused)]
fn main() {
    // ...
    .generate()
    .map_or_else(
        |error| match error {
            cbindgen::Error::ParseSyntaxError { .. } => {}
            e => panic!("{:?}", e),
        },
        |bindings| {
            bindings.write_to_file("target/include/bindings.h");
        },
    );
}
}

Be sure to add the following section to your Cargo.toml:

[build-dependencies]
cbindgen = "0.24.0"

If you’d like to use a build.rs script with a cbindgen.toml, consider using cbindgen::generate() instead.

Writing Your C API

cbindgen has a simple but effective strategy. It walks through your crate looking for:

  • #[no_mangle] pub extern fn (“functions”)
  • #[no_mangle] pub static (“globals”)
  • pub const (“constants”)

and generates a header declaring those items. But to declare those items, it needs to also be able to describe the layout and ABI of the types that appear in their signatures. So it will also spider through your crate (and optionally its dependencies) to try to find the definitions of every type used in your public API.

🚨 NOTE: A major limitation of cbindgen is that it does not understand Rust’s module system or namespacing. This means that if cbindgen sees that it needs the definition for MyType and there exists two things in your project with the type name MyType, it won’t know what to do. Currently, cbindgen’s behaviour is unspecified if this happens. However this may be ok if they have different cfgs.

If a type is determined to have a guaranteed layout, a full definition will be emitted in the header. If the type doesn’t have a guaranteed layout, only a forward declaration will be emitted. This may be fine if the type is intended to be passed around opaquely and by reference.

Examples

🚧 🏗

It would be really nice to have some curated and clean examples, but we don’t have those yet.

The README has some useful links though.

Supported Types

Most things in Rust don’t have a guaranteed layout by default. In most cases this is nice because it enables layout to be optimized in the majority of cases where type layout isn’t that interesting. However this is problematic for our purposes. Thankfully Rust lets us opt into guaranteed layouts with the repr attribute.

You can learn about all of the different repr attributes by reading Rust’s reference, but here’s a quick summary:

  • #[repr(C)]: give this struct/union/enum the same layout and ABI C would
  • #[repr(u8, u16, ... etc)]: give this enum the same layout and ABI as the given integer type
  • #[repr(transparent)]: give this single-field struct the same ABI as its field (useful for newtyping integers but keeping the integer ABI)

cbindgen supports the #[repr(align(N))] and #[repr(packed)] attributes, but currently does not support #[repr(packed(N))].

cbindgen also supports using repr(C)/repr(u8) on non-C-like enums (enums with fields). This gives a C-compatible tagged union layout, as defined by this RFC 2195. repr(C) will give a simpler layout that is perhaps more intuitive, while repr(u8) will produce a more compact layout.

If you ensure everything has a guaranteed repr, then cbindgen will generate definitions for:

  • struct (named-style or tuple-style)
  • enum (fieldless or with fields)
  • union
  • type
  • [T; n] (arrays always have a guaranteed C-compatible layout)
  • &T, &mut T, *const T, *mut T, Option<&T>, Option<&mut T> (all have the same pointer ABI)
  • fn() (as an actual function pointer)
  • bitflags! { ... } (if macro_expansion.bitflags is enabled)

structs, enums, unions, and type aliases may be generic, although certain generic substitutions may fail to resolve under certain configurations. In C mode generics are resolved through monomorphization and mangling, while in C++ mode generics are resolved with templates. cbindgen cannot support generic functions, as they do not actually have a single defined symbol.

cbindgen sadly cannot ever support anonymous tuples (A, B, ...), as there is no way to guarantee their layout. You must use a tuple struct.

cbindgen also cannot support wide pointers like &dyn Trait or &[T], as their layout and ABI is not guaranteed. In the case of slices you can at least decompose them into a pointer and length, and reconstruct them with slice::from_raw_parts.

If cbindgen determines that a type is zero-sized, it will erase all references to that type (so fields of that type simply won’t be emitted). This won’t work if that type appears as a function argument because C, C++, and Rust all have different definitions of what it means for a type to be empty.

Don’t use the [u64; 0] trick to over-align a struct, we don’t support this.

cbindgen contains the following hardcoded mappings (again completely ignoring namespacing, literally just looking at the name of the type):

std types

  • bool => bool
  • char => uint32_t
  • u8 => uint8_t
  • u16 => uint16_t
  • u32 => uint32_t
  • u64 => uint64_t
  • usize => uintptr_t
  • i8 => int8_t
  • i16 => int16_t
  • i32 => int32_t
  • i64 => int64_t
  • isize => intptr_t
  • f32 => float
  • f64 => double
  • VaList => va_list
  • RawFd => int
  • PhantomData => evaporates, can only appear as the field of a type
  • PhantomPinned => evaporates, can only appear as the field of a type
  • () => evaporates, can only appear as the field of a type
  • MaybeUninit<T>, ManuallyDrop<T>, and Pin<T> => T

libc types

  • c_void => void
  • c_char => char
  • c_schar => signed char
  • c_uchar => unsigned char
  • c_float => float
  • c_double => double
  • c_short => short
  • c_int => int
  • c_long => long
  • c_longlong => long long
  • c_ushort => unsigned short
  • c_uint => unsigned int
  • c_ulong => unsigned long
  • c_ulonglong => unsigned long long

stdint types

  • uint8_t => uint8_t
  • uint16_t => uint16_t
  • uint32_t => uint32_t
  • uint64_t => uint64_t
  • uintptr_t => uintptr_t
  • size_t => size_t
  • int8_t => int8_t
  • int16_t => int16_t
  • int32_t => int32_t
  • int64_t => int64_t
  • intptr_t => intptr_t
  • ssize_t => ssize_t
  • ptrdiff_t => ptrdiff_t

Configuring Your Header

cbindgen supports several different options for configuring the output of your header, including target language, styling, mangling, prefixing, includes, and defines.

Defines and Cfgs

As cbindgen spiders through your crate, it will make note of all the cfgs it found on the path to every item. If it finds multiple declarations that share a single name but have different cfgs, it will then try to emit every version it found wrapped in defines that correspond to those cfgs. In this way platform-specific APIs or representations can be properly supported.

However cbindgen has no way of knowing how you want to map those cfgs to defines. You will need to use the [defines] section in your cbindgen.toml to specify all the different mappings. It natively understands concepts like any() and all(), so you only need to tell it how you want to translate base concepts like target_os = "freebsd" or feature = "serde".

Note that because cbindgen just parses the source of your crate, you mostly don’t need to worry about what crate features or what platform you’re targetting. Every possible configuration should be visible to the parser. Our primitive mappings should also be completely platform agnostic (i32 is int32_t regardless of your target).

While modules within a crate form a tree with uniquely defined paths to each item, and therefore uniquely defined cfgs for those items, dependencies do not. If you depend on a crate in multiple ways, and those ways produce different cfgs, one of them will be arbitrarily chosen for any types found in that crate.

Annotations

While output configuration is primarily done through the cbindgen.toml, in some cases you need to manually override your global settings. In those cases you can add inline annotations to your types, which are doc comments that start with cbindgen:. Here’s an example of using annotations to rename a struct’s fields and opt into overloading operator==:

#![allow(unused)]
fn main() {
/// cbindgen:field-names=[x, y]
/// cbindgen:derive-eq
#[repr(C)]
pub struct Point(pub f32, pub f32);
}

An annotation may be a bool, string (no quotes), or list of strings. If just the annotation’s name is provided, =true is assumed. The annotation parser is currently fairly naive and lacks any capacity for escaping, so don’t try to make any strings with =, ,, [ or ].

Most annotations are just local overrides for identical settings in the cbindgen.toml, but a few are unique because they don’t make sense in a global context. The set of supported annotation are as follows:

Ignore annotation

cbindgen will automatically ignore any #[test] or #[cfg(test)] item it finds. You can manually ignore other stuff with the ignore annotation attribute:

#![allow(unused)]
fn main() {
pub mod my_interesting_mod;

/// cbindgen:ignore
pub mod my_uninteresting_mod; // This won't be scanned by cbindgen.
}

No export annotation

cbindgen will usually emit all items it finds, as instructed by the parse and export config sections. This annotation will make cbindgen skip this item from the output, while still being aware of it. This is useful for a) suppressing “Can’t find” errors and b) emitting struct my_struct for types in a different header (rather than a bare my_struct).

There is no equivalent config for this annotation - by comparison, the export exclude config will cause cbindgen to not be aware of the item at all.

Note that cbindgen will still traverse no-export structs that are repr(C) to emit types present in the fields. You will need to manually exclude those types in your config if desired.

/// cbindgen:no-export
#[repr(C)]
pub struct Foo { .. }; // This won't be emitted by cbindgen in the header

#[repr(C)]
fn bar() -> Foo { .. } // Will be emitted as `struct foo bar();`

Struct Annotations

  • field-names=[field1, field2, …] – sets the names of all the fields in the output struct. These names will be output verbatim, and are not eligible for renaming.

The rest are just local overrides for the same options found in the cbindgen.toml:

  • rename-all=RenameRule
  • derive-constructor
  • derive-eq
  • derive-neq
  • derive-lt
  • derive-lte
  • derive-gt
  • derive-gte
  • {eq,neq,lt,lte,gt,gte}-attributes: Takes a single identifier which will be emitted before the signature of the auto-generated operator== / operator!= / etc(if any). The idea is for this to be used to annotate the operator with attributes, for example:
#![allow(unused)]
fn main() {
/// cbindgen:eq-attributes=MY_ATTRIBUTES
#[repr(C)]
pub struct Foo { .. }
}

Will generate something like:

  MY_ATTRIBUTES bool operator==(const Foo& other) const {
    ...
  }

Combined with something like:

#define MY_ATTRIBUTES [[nodiscard]]

for example.

Enum Annotations

  • enum-trailing-values=[variant1, variant2, …] – add the following fieldless enum variants to the end of the enum’s definition. These variant names will have the enum’s renaming rules applied.

WARNING: if any of these values are ever passed into Rust, behaviour will be Undefined. Rust does not know about them, and will assume they cannot happen.

The rest are just local overrides for the same options found in the cbindgen.toml:

  • rename-all=RenameRule
  • add-sentinel
  • derive-helper-methods
  • derive-const-casts
  • derive-mut-casts
  • derive-tagged-enum-destructor
  • derive-tagged-enum-copy-constructor
  • enum-class
  • prefix-with-name
  • private-default-tagged-enum-constructor
  • {destructor,copy-constructor,copy-assignment}-attributes: See the description of the struct attributes, these do the same for the respective generated code.

Enum variant annotations

These apply to both tagged and untagged enum variants.

  • variant-{constructor,const-cast,mut-cast,is}-attributes: See the description of the struct attributes. These do the same for the respective functions.

TODO: We should allow to override the derive-{const,mut}-casts, helper methods et al. with per-variant annotations, probably.

Union Annotations

  • field-names=[field1, field2, …] – sets the names of all the fields in the output union. These names will be output verbatim, and are not eligible for renaming.

The rest are just local overrides for the same options found in the cbindgen.toml:

  • rename-all=RenameRule

Function Annotations

All function attributes are just local overrides for the same options found in the cbindgen.toml:

  • rename-all=RenameRule
  • prefix
  • postfix
  • ptrs-as-arrays=[[ptr_name1; array_length1], [ptr_name2; array_length2], …] – represents the pointer arguments of a function as arrays. Below how the mappings are performed:
arg: *const T --> const T arg[array_length]
arg: *mut T ---> T arg[array_length]

If array_length is not specified:

arg: *const T --> const T arg[]
arg: *mut T --> T arg[]

Generating Swift Bindings

In addition to parsing function names in C/C++ header files, the Swift compiler can make use of the swift_name attribute on functions to generate more idiomatic names for imported functions and methods.

This attribute is commonly used in Objective-C/C/C++ via the NS_SWIFT_NAME and CF_SWIFT_NAME macros.

Given configuration in the cbindgen.toml, cbindgen can generate these attributes for you by guessing an appropriate method signature based on the existing function name (and type, if it is a method in an impl block).

This is controlled by the swift_name_macro option in the cbindgen.toml.

cbindgen.toml

Most configuration happens through your cbindgen.toml file. Every value has a default (that is usually reasonable), so you can start with an empty cbindgen.toml and tweak it until you like the output you’re getting.

Note that many options defined here only apply for one of C or C++. Usually it’s an option specifying whether we should try to make use of a feature in C++’s type system or generate a helper method.

# The language to output bindings in
#
# possible values: "C", "C++", "Cython"
#
# default: "C++"
language = "C"




# Options for wrapping the contents of the header:

# An optional string of text to output at the beginning of the generated file
# default: doesn't emit anything
header = "/* Text to put at the beginning of the generated file. Probably a license. */"

# An optional string of text to output at the end of the generated file
# default: doesn't emit anything
trailer = "/* Text to put at the end of the generated file */"

# An optional name to use as an include guard
# default: doesn't emit an include guard
include_guard = "mozilla_wr_bindings_h"

# Whether to add a `#pragma once` guard
# default: doesn't emit a `#pragma once`
pragma_once = true

# An optional string of text to output between major sections of the generated
# file as a warning against manual editing
#
# default: doesn't emit anything
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"

# Whether to include a comment with the version of cbindgen used to generate the file
# default: false
include_version = true

# An optional namespace to output around the generated bindings
# default: doesn't emit a namespace
namespace = "ffi"

# An optional list of namespaces to output around the generated bindings
# default: []
namespaces = ["mozilla", "wr"]

# An optional list of namespaces to declare as using with "using namespace"
# default: []
using_namespaces = ["mozilla", "wr"]

# A list of sys headers to #include (with angle brackets)
# default: []
sys_includes = ["stdio", "string"]

# A list of headers to #include (with quotes)
# default: []
includes = ["my_great_lib.h"]

# Whether cbindgen's default C/C++ standard imports should be suppressed. These
# imports are included by default because our generated headers tend to require
# them (e.g. for uint32_t). Currently, the generated imports are:
#
# * for C: `stdarg.h`, `stdbool.h`, `stdint.h`, `stdlib.h`, `uchar.h`
#
# * for C++: `cstdarg`, `cstdint`, `cstdlib`, `new`, `cassert` (depending on config)
#
# default: false
no_includes = false

# Whether to make a C header C++ compatible.
# These will wrap generated functions into a `extern "C"` block, e.g.
#
# #ifdef __cplusplus
# extern "C" {
# #endif // __cplusplus
#
# // Generated functions.
#
# #ifdef __cplusplus
# } // extern "C"
# #endif // __cplusplus
#
# If the language is not C this option won't have any effect.
#
# default: false
cpp_compat = false

# A list of lines to add verbatim after the includes block
after_includes = "#define VERSION 1"



# Code Style Options

# The style to use for curly braces
#
# possible values: "SameLine", "NextLine"
#
# default: "SameLine"
braces = "SameLine"

# The desired length of a line to use when formatting lines
# default: 100
line_length = 80

# The amount of spaces to indent by
# default: 2
tab_width = 3

# Include doc comments from Rust as documentation
documentation = true

# How the generated documentation should be commented.
#
# possible values:
# * "c": /* like this */
# * "c99": // like this
# * "c++": /// like this
# * "doxy": like C, but with leading *'s on each line
# * "auto": "c++" if that's the language, "doxy" otherwise
#
# default: "auto"
documentation_style = "doxy"

# How much of the documentation for each item is output.
#
# possible values:
# * "short": Only the first line.
# * "full": The full documentation.
#
# default: "full"
documentation_length = "short"




# Codegen Options

# When generating a C header, the kind of declaration style to use for structs
# or enums.
#
# possible values:
# * "type": typedef struct { ... } MyType;
# * "tag": struct MyType { ... };
# * "both": typedef struct MyType { ... } MyType;
#
# default: "both"
style = "both"

# If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t`
# instead of `uintptr_t` and `intptr_t` respectively.
usize_is_size_t = true

# A list of substitutions for converting cfg's to ifdefs. cfgs which aren't
# defined here will just be discarded.
#
# e.g.
# `#[cfg(target = "freebsd")] ...`
# becomes
# `#if defined(DEFINE_FREEBSD) ... #endif`
[defines]
"target_os = freebsd" = "DEFINE_FREEBSD"
"feature = serde" = "DEFINE_SERDE"





[export]
# A list of additional items to always include in the generated bindings if they're
# found but otherwise don't appear to be used by the public API.
#
# default: []
include = ["MyOrphanStruct", "MyGreatTypeRename"]

# A list of items to not include in the generated bindings
# default: []
exclude = ["Bad"]

# A prefix to add before the name of every item
# default: no prefix is added
prefix = "CAPI_"

# Types of items that we'll generate. If empty, then all types of item are emitted.
#
# possible items: (TODO: explain these in detail)
# * "constants":
# * "globals":
# * "enums":
# * "structs":
# * "unions":
# * "typedefs":
# * "opaque":
# * "functions":
#
# default: []
item_types = ["enums", "structs", "opaque", "functions"]

# Whether applying rules in export.rename prevents export.prefix from applying.
#
# e.g. given this toml:
#
# [export]
# prefix = "capi_"
# [export.rename]
# "MyType" = "my_cool_type"
#
# You get the following results:
#
# renaming_overrides_prefixing = true:
# "MyType" => "my_cool_type"
#
# renaming_overrides_prefixing = false:
# "MyType => capi_my_cool_type"
#
# default: false
renaming_overrides_prefixing = true

# Table of name conversions to apply to item names (lhs becomes rhs)
[export.rename]
"MyType" = "my_cool_type"
"my_function" = "BetterFunctionName"

# Table of things to prepend to the body of any struct, union, or enum that has the
# given name. This can be used to add things like methods which don't change ABI,
# mark fields private, etc
[export.pre_body]
"MyType" = """
  MyType() = delete;
private:
"""

# Table of things to append to the body of any struct, union, or enum that has the
# given name. This can be used to add things like methods which don't change ABI.
[export.body]
"MyType" = """
  void cppMethod() const;
"""

# Configuration for name mangling
[export.mangle]
# Whether the types should be renamed during mangling, for example
# c_char -> CChar, etc.
rename_types = "PascalCase"
# Whether the underscores from the mangled name should be omitted.
remove_underscores = false

[layout]
# A string that should come before the name of any type which has been marked
# as `#[repr(packed)]`. For instance, "__attribute__((packed))" would be a
# reasonable value if targeting gcc/clang. A more portable solution would
# involve emitting the name of a macro which you define in a platform-specific
# way. e.g. "PACKED"
#
# default: `#[repr(packed)]` types will be treated as opaque, since it would
# be unsafe for C callers to use a incorrectly laid-out union.
packed = "PACKED"

# A string that should come before the name of any type which has been marked
# as `#[repr(align(n))]`. This string must be a function-like macro which takes
# a single argument (the requested alignment, `n`). For instance, a macro
# `#define`d as `ALIGNED(n)` in `header` which translates to
# `__attribute__((aligned(n)))` would be a reasonable value if targeting
# gcc/clang.
#
# default: `#[repr(align(n))]` types will be treated as opaque, since it
# could be unsafe for C callers to use a incorrectly-aligned union.
aligned_n = "ALIGNED"


[fn]
# An optional prefix to put before every function declaration
# default: no prefix added
prefix = "WR_START_FUNC"

# An optional postfix to put after any function declaration
# default: no postix added
postfix = "WR_END_FUNC"

# How to format function arguments
#
# possible values:
# * "horizontal": place all arguments on the same line
# * "vertical": place each argument on its own line
# * "auto": only use vertical if horizontal would exceed line_length
#
# default: "auto"
args = "horizontal"

# An optional string that should prefix function declarations which have been
# marked as `#[must_use]`. For instance, "__attribute__((warn_unused_result))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "MUST_USE_FUNC"
# default: nothing is emitted for must_use functions
must_use = "MUST_USE_FUNC"

# An optional string that should prefix function declarations which have been
# marked as `#[deprecated]` without note. For instance, "__attribute__((deprecated))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_FUNC"
# default: nothing is emitted for deprecated functions
deprecated = "DEPRECATED_FUNC"

# An optional string that should prefix function declarations which have been
# marked as `#[deprecated(note = "reason")]`. `{}` will be replaced with the
# double-quoted string. For instance, "__attribute__((deprecated({})))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_FUNC_WITH_NOTE(note)"
# default: nothing is emitted for deprecated functions
deprecated_with_notes = "DEPRECATED_FUNC_WITH_NOTE"

# An optional string that will be used in the attribute position for functions
# that don't return (that return `!` in Rust).
#
# For instance, `__attribute__((noreturn))` would be a reasonable value if
# targeting gcc/clang.
no_return = "NO_RETURN"

# An optional string that, if present, will be used to generate Swift function
# and method signatures for generated functions, for example "CF_SWIFT_NAME".
# If no such macro is available in your toolchain, you can define one using the
# `header` option in cbindgen.toml
# default: no swift_name function attributes are generated
swift_name_macro = "CF_SWIFT_NAME"

# A rule to use to rename function argument names. The renaming assumes the input
# is the Rust standard snake_case, however it accepts all the different rename_args
# inputs. This means many options here are no-ops or redundant.
#
# possible values (that actually do something):
# * "CamelCase": my_arg => myArg
# * "PascalCase": my_arg => MyArg
# * "GeckoCase": my_arg => aMyArg
# * "ScreamingSnakeCase": my_arg => MY_ARG
# * "None": apply no renaming
#
# technically possible values (that shouldn't have a purpose here):
# * "SnakeCase": apply no renaming
# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?)
# * "UpperCase": same as ScreamingSnakeCase in this context
# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context
#
# default: "None"
rename_args = "PascalCase"

# This rule specifies the order in which functions will be sorted.
#
# "Name": sort by the name of the function
# "None": keep order in which the functions have been parsed
#
# default: "None"
sort_by = "Name"

[struct]
# A rule to use to rename struct field names. The renaming assumes the input is
# the Rust standard snake_case, however it acccepts all the different rename_args
# inputs. This means many options here are no-ops or redundant.
#
# possible values (that actually do something):
# * "CamelCase": my_arg => myArg
# * "PascalCase": my_arg => MyArg
# * "GeckoCase": my_arg => mMyArg
# * "ScreamingSnakeCase": my_arg => MY_ARG
# * "None": apply no renaming
#
# technically possible values (that shouldn't have a purpose here):
# * "SnakeCase": apply no renaming
# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?)
# * "UpperCase": same as ScreamingSnakeCase in this context
# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context
#
# default: "None"
rename_fields = "PascalCase"

# An optional string that should come before the name of any struct which has been
# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "MUST_USE_STRUCT"
#
# default: nothing is emitted for must_use structs
must_use = "MUST_USE_STRUCT"

# An optional string that should come before the name of any struct which has been
# marked as `#[deprecated]` without note. For instance, "__attribute__((deprecated))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_STRUCT"
# default: nothing is emitted for deprecated structs
deprecated = "DEPRECATED_STRUCT"

# An optional string that should come before the name of any struct which has been
# marked as `#[deprecated(note = "reason")]`. `{}` will be replaced with the
# double-quoted string. For instance, "__attribute__((deprecated({})))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_STRUCT_WITH_NOTE(note)"
# default: nothing is emitted for deprecated structs
deprecated_with_notes = "DEPRECATED_STRUCT_WITH_NOTE"

# Whether a Rust type with associated consts should emit those consts inside the
# type's body. Otherwise they will be emitted trailing and with the type's name
# prefixed. This does nothing if the target is C, or if
# [const]allow_static_const = false
#
# default: false
# associated_constants_in_body: false

# Whether to derive a simple constructor that takes a value for every field.
# default: false
derive_constructor = true

# Whether to derive an operator== for all structs
# default: false
derive_eq = false

# Whether to derive an operator!= for all structs
# default: false
derive_neq = false

# Whether to derive an `operator<` for all structs
# default: false
derive_lt = false

# Whether to derive an `operator<=` for all structs
# default: false
derive_lte = false

# Whether to derive an operator> for all structs
# default: false
derive_gt = false

# Whether to derive an operator>= for all structs
# default: false
derive_gte = false





[enum]
# A rule to use to rename enum variants, and the names of any fields those
# variants have. This should probably be split up into two separate options, but
# for now, they're the same! See the documentation for `[struct]rename_fields`
# for how this applies to fields. Renaming of the variant assumes that the input
# is the Rust standard PascalCase. In the case of QualifiedScreamingSnakeCase,
# it also assumed that the enum's name is PascalCase.
#
# possible values (that actually do something):
# * "CamelCase": MyVariant => myVariant
# * "SnakeCase": MyVariant => my_variant
# * "ScreamingSnakeCase": MyVariant => MY_VARIANT
# * "QualifiedScreamingSnakeCase": MyVariant => ENUM_NAME_MY_VARIANT
# * "LowerCase": MyVariant => myvariant
# * "UpperCase": MyVariant => MYVARIANT
# * "None": apply no renaming
#
# technically possible values (that shouldn't have a purpose for the variants):
# * "PascalCase": apply no renaming
# * "GeckoCase": apply no renaming
#
# default: "None"
rename_variants = "None"

# Whether an extra "sentinel" enum variant should be added to all generated enums.
# Firefox uses this for their IPC serialization library.
#
# WARNING: if the sentinel is ever passed into Rust, behaviour will be Undefined.
# Rust does not know about this value, and will assume it cannot happen.
#
# default: false
add_sentinel = false

# Whether enum variant names should be prefixed with the name of the enum.
# default: false
prefix_with_name = false

# Whether to emit enums using "enum class" when targeting C++.
# default: true
enum_class = true

# Whether to generate static `::MyVariant(..)` constructors and `bool IsMyVariant()`
# methods for enums with fields.
#
# default: false
derive_helper_methods = false

# Whether to generate `const MyVariant& AsMyVariant() const` methods for enums with fields.
# default: false
derive_const_casts = false

# Whether to generate `MyVariant& AsMyVariant()` methods for enums with fields
# default: false
derive_mut_casts = false

# The name of the macro/function to use for asserting `IsMyVariant()` in the body of
# derived `AsMyVariant()` cast methods.
#
# default: "assert" (but also causes ``<cassert>`` to be included by default)
cast_assert_name = "MOZ_RELEASE_ASSERT"

# An optional string that should come before the name of any enum which has been
# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "MUST_USE_ENUM"
#
# Note that this refers to the *output* type. That means this will not apply to an enum
# with fields, as it will be emitted as a struct. `[struct]must_use` will apply there.
#
# default: nothing is emitted for must_use enums
must_use = "MUST_USE_ENUM"

# An optional string that should come before the name of any enum which has been
# marked as `#[deprecated]` without note. For instance, "__attribute__((deprecated))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_ENUM"
# default: nothing is emitted for deprecated enums
deprecated = "DEPRECATED_ENUM"

# An optional string that should come before the name of any enum which has been
# marked as `#[deprecated(note = "reason")]`. `{}` will be replaced with the
# double-quoted string. For instance, "__attribute__((deprecated({})))"
# would be a reasonable value if targeting gcc/clang. A more portable solution
# would involve emitting the name of a macro which you define in a
# platform-specific way. e.g. "DEPRECATED_ENUM_WITH_NOTE(note)"
# default: nothing is emitted for deprecated enums
deprecated_with_notes = "DEPRECATED_ENUM_WITH_NOTE"

# An optional string that should come after the name of any enum variant which has been
# marked as `#[deprecated]` without note. For instance, "__attribute__((deprecated))"
# would be a reasonable value if targeting gcc/clang. A more portable solution would
# involve emitting the name of a macro which you define in a platform-specific
# way. e.g. "DEPRECATED_ENUM_VARIANT"
# default: nothing is emitted for deprecated enum variants
deprecated_variant = "DEPRECATED_ENUM_VARIANT"

# An optional string that should come after the name of any enum variant which has been
# marked as `#[deprecated(note = "reason")]`. `{}` will be replaced with the
# double-quoted string. For instance, "__attribute__((deprecated({})))" would be a
# reasonable value if targeting gcc/clang. A more portable solution would involve
# emitting the name of a macro which you define in a platform-specific
# way. e.g. "DEPRECATED_ENUM_WITH_NOTE(note)"
# default: nothing is emitted for deprecated enum variants
deprecated_variant_with_notes = "DEPRECATED_ENUM_VARIANT_WITH_NOTE({})"

# Whether enums with fields should generate destructors. This exists so that generic
# enums can be properly instantiated with payloads that are C++ types with
# destructors. This isn't necessary for structs because C++ has rules to
# automatically derive the correct constructors and destructors for those types.
#
# Care should be taken with this option, as Rust and C++ cannot
# properly interoperate with eachother's notions of destructors. Also, this may
# change the ABI for the type. Either your destructor-full enums must live
# exclusively within C++, or they must only be passed by-reference between
# C++ and Rust.
#
# default: false
derive_tagged_enum_destructor = false

# Whether enums with fields should generate copy-constructor. See the discussion on
# derive_tagged_enum_destructor for why this is both useful and very dangerous.
#
# default: false
derive_tagged_enum_copy_constructor = false
# Whether enums with fields should generate copy-assignment operators.
#
# This depends on also deriving copy-constructors, and it is highly encouraged
# for this to be set to true.
#
# default: false
derive_tagged_enum_copy_assignment = false

# Whether enums with fields should generate an empty, private destructor.
# This allows the auto-generated constructor functions to compile, if there are
# non-trivially constructible members. This falls in the same family of
# dangerousness as `derive_tagged_enum_copy_constructor` and co.
#
# default: false
private_default_tagged_enum_constructor = false





[const]
# Whether a generated constant can be a static const in C++ mode. I have no
# idea why you would turn this off.
#
# default: true
allow_static_const = true

# Whether a generated constant can be constexpr in C++ mode.
#
# default: true
allow_constexpr = false

# This rule specifies the order in which constants will be sorted.
#
# "Name": sort by the name of the constant
# "None": keep order in which the constants have been parsed
#
# default: "None"
sort_by = "Name"




[macro_expansion]
# Whether bindings should be generated for instances of the bitflags! macro.
# default: false
bitflags = true






# Options for how your Rust library should be parsed

[parse]
# Whether to parse dependent crates and include their types in the output
# default: false
parse_deps = true

# A white list of crate names that are allowed to be parsed. If this is defined,
# only crates found in this list will ever be parsed.
#
# default: there is no whitelist (NOTE: this is the opposite of [])
include = ["webrender", "webrender_traits"]

# A black list of crate names that are not allowed to be parsed.
# default: []
exclude = ["libc"]

# Whether to use a new temporary target directory when running `rustc -Zunpretty=expanded`.
# This may be required for some build processes.
#
# default: false
clean = false

# Which crates other than the top-level binding crate we should generate
# bindings for.
#
# default: []
extra_bindings = ["my_awesome_dep"]

[parse.expand]
# A list of crate names that should be run through `cargo expand` before
# parsing to expand any macros. Note that if a crate is named here, it
# will always be parsed, even if the blacklist/whitelist says it shouldn't be.
#
# default: []
crates = ["euclid"]

# If enabled,  use the `--all-features` option when expanding. Ignored when
# `features` is set. For backwards-compatibility, this is forced on if
# `expand = ["euclid"]` shorthand is used.
#
# default: false
all_features = false

# When `all_features` is disabled and this is also disabled, use the
# `--no-default-features` option when expanding.
#
# default: true
default_features = true

# A list of feature names that should be used when running `cargo expand`. This
# combines with `default_features` like in your `Cargo.toml`. Note that the features
# listed here are features for the current crate being built, *not* the crates
# being expanded. The crate's `Cargo.toml` must take care of enabling the
# appropriate features in its dependencies
#
# default: []
features = ["cbindgen"]

[ptr]
# An optional string to decorate all pointers that are
# required to be non null. Nullability is inferred from the Rust type: `&T`,
# `&mut T` and `NonNull<T>` all require a valid pointer value.
non_null_attribute = "_Nonnull"

# Options specific to Cython bindings.

[cython]

# Header specified in the top level `cdef extern from header:` declaration.
#
# default: *
header = '"my_header.h"'

# `from module cimport name1, name2` declarations added in the same place
# where you'd get includes in C.
[cython.cimports]
module = ["name1", "name2"]