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
| SDK | Package | Backend |
|---|---|---|
| C# | NuGet | DllImport (P/Invoke) |
| Python | PyPI | ctypes |
| TypeScript | npm | napi-rs (Node.js) + wasm-bindgen (Web) |
| Rust | crates.io | Direct linking (no FFI) |
| C/C++ | local | C header via cbindgen |
| Go | local | cgo |
| Kotlin | local | JNI |
| Swift | local | C interop |
| Lua | local | C 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.
Quick Links
- New to GoudEngine? Start with a getting-started guide for your language in the sidebar.
- Building from source? See the Building and Development Guide.
- Understanding the internals? Read the SDK-First Architecture document.
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.
| Language | Best for | Install |
|---|---|---|
| Rust | Maximum performance, engine contributions | cargo add goud-engine |
| C# | Unity-like workflow, .NET ecosystem | dotnet add package GoudEngine |
| Python | Rapid prototyping, scripting | pip install goudengine |
| TypeScript | Web games (WASM), desktop via Node.js | npm install goudengine |
| C | Minimal overhead, embedded systems | Header-only |
| C++ | RAII wrappers, existing C++ projects | CMake / vcpkg / Conan |
| Go | Simple concurrency, Go-native projects | go get github.com/aram-devdocs/GoudEngine/sdks/go |
| Kotlin | JVM ecosystem, Android (future) | Gradle: io.github.aram-devdocs:goudengine |
| Swift | Apple platforms, SwiftPM projects | Swift Package Manager |
| Lua | Embedded scripting, mod support | luarocks install goudengine or embedded runner |
What you get
Each guide walks you through the same steps:
- Prerequisites – what to install
- Install – one command to get the SDK
- Hello World – open a window
- Draw a Sprite – load and render an image
- Handle Input – respond to keyboard and mouse
- Run Examples – try the included demo games
- 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:
- start
cargo run -p goudengine-mcp - call
goudengine.list_contexts - 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
- C# SDK README — full API reference
- C# examples source — complete game source code
- Build Your First Game — end-to-end minimal game walkthrough
- Debugger Runtime — local attach, capture, replay, and metrics workflow
- Example Showcase — current cross-language parity matrix
- Cross-Platform Deployment — packaging and release workflow
- FAQ and Troubleshooting — common runtime and build issues
- SDK-first architecture — how the engine layers fit together
- Development guide — building from source, version management, git hooks
- Other getting started guides: Python · TypeScript · Rust · Go · Kotlin · Lua
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:
- start
cargo run -p goudengine-mcp - call
goudengine.list_contexts - 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
| Import | Description |
|---|---|
GoudGame | Window, game loop, rendering, input |
Key | Keyboard key constants (GLFW values) |
MouseButton | Mouse button constants |
Vec2 | 2D vector with arithmetic methods |
Color | RGBA color (Color.red(), Color.from_hex(0xFF0000)) |
Transform2D | 2D position, rotation, scale |
Sprite | Sprite rendering component |
Entity | ECS entity handle |
Next Steps
- Python examples — source code for
main.py,flappy_bird.py, andsandbox.py - Python SDK README — full API reference
- Build Your First Game — end-to-end minimal game walkthrough
- Debugger Runtime — local attach, capture, replay, and metrics workflow
- Example Showcase — current cross-language parity matrix
- Cross-Platform Deployment — packaging and release workflow
- FAQ and Troubleshooting — common runtime and build issues
- Architecture overview — how the Rust core and Python SDK connect
- Development guide — building from source, running tests
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:
- Start the app with debugger mode enabled.
- In another terminal, run
cargo run -p goudengine-mcp. - Call
goudengine.list_contexts, thengoudengine.attach_context. - 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
- Rust SDK README — crate design and re-export structure
- Debugger Runtime — local attach, capture, replay, and metrics workflow
- Rust examples — flappy_bird source code
- Build Your First Game — end-to-end minimal game walkthrough
- Example Showcase — current cross-language parity matrix
- Cross-Platform Deployment — packaging and release workflow
- FAQ and Troubleshooting — common runtime and build issues
- Architecture — layer design and engine internals
- Development guide — building from source, running tests
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.js | Web | |
|---|---|---|
| Constructor | new GoudGame({...}) | await GoudGame.create({...}) |
| Extra parameters | — | canvas, wasmUrl |
| Game loop | while (!game.shouldClose()) | game.run((dt) => { ... }) |
| Clear color | beginFrame(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.WebSocketand wait untilpeerCount() > 0before sending your first packet. - The debugger runtime is desktop-only in this batch.
goudengine/webdoes 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:
- start
cargo run -p goudengine-mcp - call
goudengine.list_contexts - 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:
| Key | Code |
|---|---|
| Escape | 256 |
| Space | 32 |
| R | 82 |
| Arrow Left | 263 |
| Arrow Right | 262 |
| Arrow Up | 265 |
| Arrow Down | 264 |
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
- TypeScript SDK README — full API reference and build instructions
- TypeScript examples — Flappy Bird source with shared desktop/web logic
- Build Your First Game — end-to-end minimal game walkthrough
- Example Showcase — current cross-language parity matrix
- Cross-Platform Deployment — packaging and release workflow
- Web Platform Gotchas — browser-specific limitations and workarounds
- FAQ and Troubleshooting — common runtime and build issues
- Architecture overview — layer design and codegen pipeline
- Development guide — build system, git hooks, version management
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# guide – Python guide – TypeScript guide – Rust guide – Swift guide – Kotlin 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
| Import | Description |
|---|---|
goud.NewGame | Create a windowed game instance |
goud.Color | RGBA color (goud.ColorWhite(), goud.ColorRGB(r, g, b)) |
goud.Vec2 | 2D vector with arithmetic methods |
goud.Vec3 | 3D vector |
goud.Rect | Rectangle (x, y, width, height) |
goud.Transform2D | 2D position, rotation, scale |
goud.EntityID | ECS 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
- Go SDK README – full architecture and API overview
- Go examples source – complete game source code
- Build Your First Game – end-to-end minimal game walkthrough
- Example Showcase – current cross-language parity matrix
- SDK-first architecture – how the engine layers fit together
- Development guide – building from source, version management, git hooks
- Other getting started guides: C# – Python – TypeScript – Rust – Swift – Kotlin – Lua
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# guide – Python guide – TypeScript guide – Rust guide – Swift guide – Go 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
- Kotlin SDK docs – SDK-specific documentation
- Kotlin examples source – complete game source code
- Build Your First Game – end-to-end minimal game walkthrough
- Example Showcase – current cross-language parity matrix
- SDK-first architecture – how the engine layers fit together
- Development guide – building from source, version management, git hooks
- Other getting started guides: C# – Python – TypeScript – Rust – Swift – Go – Lua
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
- Swift SDK source — package layout and generated code
- Swift examples source — complete game source code
- Build Your First Game — end-to-end minimal game walkthrough
- Example Showcase — current cross-language parity matrix
- SDK-first architecture — how the engine layers fit together
- Development guide — building from source, version management, git hooks
- Other getting started guides: C# · Python · TypeScript · Rust · Go · Kotlin · Lua
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# guide – Python guide – TypeScript guide – Rust guide – Swift guide – Go guide – Kotlin 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:
- Embedded runner (recommended for games) – The
lua-runnerbinary loads your Lua scripts and registers all engine bindings as globals automatically. - 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
Embedded Runner (recommended)
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
| Platform | Library | Status |
|---|---|---|
| macOS | libgoud_engine.dylib | Supported |
| Linux | libgoud_engine.so | Supported |
| Windows | goud_engine.dll | Experimental |
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
- Lua SDK README – Lua SDK documentation
- Lua examples source – complete game source code
- Build Your First Game – end-to-end minimal game walkthrough
- Example Showcase – current cross-language parity matrix
- SDK-first architecture – how the engine layers fit together
- Development guide – building from source, version management, git hooks
- Other getting started guides: C# – Python – TypeScript – Rust – Swift – Go – Kotlin
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
| Tool | Version | Notes |
|---|---|---|
| Rust | stable (edition 2021) | Installed via rustup |
| .NET SDK | 8.0 | For C# SDK and examples |
| Python | 3.9+ (3.11 recommended) | For codegen scripts and Python SDK |
| Node.js | 16+ (20 recommended) | For TypeScript SDK |
| cbindgen | 0.29 | cargo install cbindgen |
| wasm-pack | latest | Only needed for TypeScript Web/WASM builds |
| cargo-deny | latest | cargo 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-devandlibxxf86vm-devare required by CI but are not installed byinstall.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:
- Detects the OS (Linux distro or macOS)
- Installs system libraries (OpenGL, X11, ALSA) via the native package manager
- Installs .NET SDK 8.0 on Ubuntu/Debian if not already present
- Installs Rust via rustup if
cargois not found - 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-devorlibxxf86vm-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
-
Clone the repo
git clone https://github.com/aram-devdocs/GoudEngine.git cd GoudEngine -
Install system dependencies — run
./install.shor use the manual commands in the System Dependencies section above. -
Install Rust (skip if
install.shalready installed it — runcargo --versionto check)curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" -
Install Rust tools
cargo install cbindgen cargo install cargo-deny -
Verify the build
cargo check cargo build cargo test -
Install .NET SDK 8.0 (required for C# SDK work) — download from dotnet.microsoft.com/download/dotnet/8.0.
-
Install Python 3.11+ (for codegen and Python SDK) — verify with
python3 --version. -
Install Node.js 20 (for TypeScript SDK) — use nvm or the official installer.
-
(Optional) Install wasm-pack (for TypeScript Web/WASM builds)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -
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:
Option 1: NullBackend (recommended for unit/integration tests)
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
| Tool | Purpose | Install |
|---|---|---|
| mdbook | Build and preview docs locally | cargo install mdbook |
| cargo-tarpaulin | Code coverage reports | cargo install cargo-tarpaulin |
| cargo-audit | Security vulnerability scanning | cargo install cargo-audit |
| GraphViz | Module dependency graph (./graph.sh) | sudo apt install graphviz / brew install graphviz |
| cargo-modules | Module dependency graph (./graph.sh) | cargo install cargo-modules |
Next Steps
- Development Guide for the day-to-day workflow
- Building for build commands and packaging
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.
- Use conventional commit prefixes (
feat:,fix:,chore:) in PR titles. - On merge to main, release-please creates or updates a Release PR.
- When the Release PR merges, it creates a tag and GitHub release.
- 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
| File | Purpose |
|---|---|
AGENTS.md | Root agent instructions (commands, architecture, anti-patterns) |
CLAUDE.md | Symlink to AGENTS.md (Claude Code compatibility) |
GEMINI.md | Symlink to AGENTS.md (Gemini compatibility) |
.cursorignore | Excludes 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:
- start
cargo run -p goudengine-mcp - call
goudengine.list_contexts - 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 patternsgoud_engine/src/ffi/AGENTS.md– FFI boundary rulessdks/AGENTS.md– SDK development rulescodegen/AGENTS.md– codegen pipeline detailsexamples/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:
- Create
.agents/skills/<skill-name>/SKILL.md - 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.shstep 2 (cargo run -p lint-layers)- Pre-commit hook (via
.husky/)
Key Files by Layer
Core (goud_engine/src/core/)
| File | Purpose |
|---|---|
types.rs | Shared FFI-compatible types (FfiVec2, GoudContextId, GoudResult) |
context_registry.rs | Thread-safe registry mapping GoudContextId → engine instances |
component_ops.rs | Generic component CRUD used by FFI component handlers |
math.rs | Vec2, Vec3, Color, Rect, Mat3x3 with #[repr(C)] for FFI |
error.rs | GoudError, GoudErrorCode, GoudResult |
FFI (goud_engine/src/ffi/)
Each domain has its own file. All public functions are #[no_mangle] pub extern "C".
| File | Domain |
|---|---|
context.rs | Engine context create/destroy |
entity.rs | Entity spawn/despawn |
component.rs | Generic component add/remove/query |
component_transform2d.rs | Transform2D component operations |
component_sprite.rs | Sprite component operations |
window.rs | Window management, frame lifecycle |
renderer.rs | 2D rendering |
renderer3d.rs | 3D rendering |
input.rs | Input state queries |
collision.rs | Collision detection |
types.rs | Re-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 methodsenums— enumeration types (e.g.,Key,MouseButton) with values and optional platform mapstools— high-level objects likeGoudGamewith 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 fieldsffi_handles— opaque handle types (GoudContextId,GoudTextureHandle)ctypes_mappings— Python ctypes annotations for pointer typestools— 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 mechanic | goud_engine/src/ (core, ecs, or assets) |
| Expose a mechanic to SDKs | Add #[no_mangle] extern "C" function in goud_engine/src/ffi/ |
| Add a new type to all SDKs | Add to codegen/goud_sdk.schema.json, run ./codegen.sh |
| Change method naming in one SDK | Edit the relevant generator in codegen/gen_<lang>.py |
| Add a new SDK language | See 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)
| Layer | Modules (under goud_engine/src/) | Responsibility |
|---|---|---|
| 1 — Foundation | core/ | Error types, math primitives, Handle<T>, provider trait definitions |
| 2 — Libs | libs/ | Graphics backend, platform windowing, native provider impls |
| 3 — Services | ecs/, assets/ | ECS (World, Entity, Component, Systems), asset loading |
| 4 — Engine | sdk/, rendering/, component_ops/, context_registry/ | Game API, render orchestration, context registry |
| 5 — FFI | ffi/, wasm/ | C-ABI exports consumed by external SDKs |
Beyond the engine crate, two additional layers complete the picture:
| Layer | Path | Responsibility |
|---|---|---|
| SDKs | sdks/ | Language-specific wrappers over FFI (all SDK languages under sdks/) |
| Apps | examples/ | 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
unsafeblock 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
| Component | Purpose |
|---|---|
Transform2D | Local 2D spatial transform: position, rotation, scale |
GlobalTransform2D | World-space 2D transform; written by the propagation system |
Sprite | Texture handle, color tint, flip flags |
RigidBody | Physics body: type (dynamic/kinematic/static), velocity, mass, gravity scale, damping |
Collider | Collision shape (circle, box, capsule, polygon), friction, restitution, sensor flag, layer/mask filtering |
AudioSource | Audio clip handle, playback state, volume, pitch, channel (Music/SFX/Ambience/UI/Voice), spatial positioning |
Text | Text content, font handle, font size, color, alignment, word-wrapping |
SpriteAnimator | Sprite sheet animation clip, frame timing, playback mode (loop/one-shot), frame events |
AnimationController | State machine with parametric transitions, blend duration, bool/float parameters |
AnimationLayerStack | Multi-layer animation blending with per-layer weight and blend mode (override/additive) |
Parent | Reference to this entity’s parent |
Children | List 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 IDChildren— 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’slayeris compared against the other body’smask. 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 whenmax_widthis 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:
| Range | Category |
|---|---|
| 0–99 | Context lifecycle |
| 100–199 | Resource management |
| 200–299 | Rendering |
| 300–399 | Physics |
| 400–499 | Audio |
| 500–599 | Animation |
| 600–699 | UI |
| 700–799 | Scene 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.mdin 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
initis called once duringGoudGame::new(). Failure is fatal unless a fallback is configured.updateis called once per frame. Providers that do not need per-frame work implement it as a no-op.shutdownis called duringGoudGame::drop(). Must not fail; all GPU and OS resources must be released beforeDrop.
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
| Implementation | Feature | Notes |
|---|---|---|
OpenGLRenderProvider | native | Wraps OpenGLBackend |
NullRenderProvider | always | No-op, returns zero handles |
Key methods:
begin_frame() -> GoudResult<FrameContext>— starts a frame, returns an opaque tokenend_frame(frame: FrameContext) -> GoudResult<()>— finalizes and presents; consumes the tokencreate_texture(desc) / destroy_texture(handle)— GPU texture managementcreate_buffer / create_shader / create_pipeline / create_render_target— resource creation with paired destroy methodsdraw(cmd) / draw_batch(cmds)— 2D sprite drawsdraw_mesh / draw_text / draw_particles— specialized draw pathsset_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
| Implementation | Feature | Notes |
|---|---|---|
RodioAudioProvider | audio | Uses the rodio crate directly |
NullAudioProvider | always | Silent no-op |
Key methods:
play(handle, config) -> GoudResult<PlaybackId>— starts playback, returns a handle for the active instancestop / pause / resume(id)— control a specific instanceis_playing(id) -> boolset_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
| Implementation | Feature | Notes |
|---|---|---|
GlfwInputProvider | native | State synced from InputManager each frame |
NullInputProvider | always | All buttons unpressed, all axes zero |
Key methods:
update_input()— process queued events and update statekey_pressed / key_just_pressed / key_just_released(key: KeyCode) -> boolmouse_position() -> [f32; 2]— window coordinatesmouse_delta() -> [f32; 2]— movement since last framemouse_button_pressed(button: MouseButton) -> boolscroll_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
| Implementation | Feature | Notes |
|---|---|---|
Rapier2DPhysicsProvider | physics | Wraps rapier2d for 2D rigid body simulation |
Rapier3DPhysicsProvider | physics | Wraps rapier3d for 3D rigid body simulation |
NullPhysicsProvider | always | No simulation; all queries return defaults |
Key methods:
step(delta)— advance the simulationset_gravity / gravity— global gravity as[f32; 2]create_body(desc) / destroy_body(handle)body_position / set_body_position / body_velocity / set_body_velocityapply_force / apply_impulsecreate_collider / destroy_colliderraycast(origin, dir, max_dist) -> Option<RaycastHit>overlap_circle(center, radius) -> Vec<BodyHandle>drain_collision_events() -> Vec<CollisionEvent>— returns ownedVecto avoid lifetime coupling with the provider borrowcreate_joint / destroy_jointdebug_shapes() -> Vec<DebugShape>
All Vec2-like parameters use [f32; 2] arrays to avoid depending on external math types in the trait definition.
WindowProvider
| Implementation | Feature | Notes |
|---|---|---|
GlfwWindowProvider | native | Wraps GlfwPlatform |
NullWindowProvider | always | No-op, should_close always false |
Key methods:
init() / shutdown()should_close() -> bool/set_should_close(value)poll_events()— pump the OS event queueswap_buffers()— present the frameget_size() -> (u32, u32)— screen coordinatesget_framebuffer_size() -> (u32, u32)— pixel coordinates (differs fromget_sizeon 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:
RenderProviderdefines rendering operations.Provideridentifies the backend and exposes capabilities.ProviderLifecyclecovers startup, per-frame maintenance, and shutdown.
The engine talks only through these traits. It does not call console graphics APIs directly.
Integration Points
| Concern | Public hook | Backend responsibility |
|---|---|---|
| Window or surface bootstrap | provider constructor plus init() | Bind the renderer to the partner window, surface, or swap-chain object |
| Back-buffer acquire and present | begin_frame() / end_frame() | Acquire the current target, record commands, submit, and present |
| Resize or mode change | resize(width, height) | Rebuild the swap chain, cached render targets, and dependent state |
| GPU work submission | draw and resource methods | Translate engine descriptors into private API calls |
| Diagnostics | render_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 asrender_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_nameto 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
FrameContextyou returned frombegin_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, andcreate_render_targetshould 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()orend_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 ofend_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
Related Guides
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 exposecodegen/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:
- FFI declarations — how to call the C functions from your language (e.g.,
DllImportin C#,ctypes.CDLLin Python, napi-rs bindings in TypeScript/Node) - Value type wrappers — classes or structs for
Color,Vec2,Vec3,Rect,Transform2D,Sprite - Enum definitions —
KeyandMouseButtonwith their numeric values - Tool wrappers — the
GoudGameclass with all methods fromschema["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
GoudGameconstructor (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.pyhas a type map for the new language -
codegen/gen_<lang>.pyexists and runs without error - All generated files begin with
HEADER_COMMENT -
codegen.shincludes the generator step -
sdks/<lang>/has a package manifest -
./codegen.shruns end-to-end cleanly - Test file exists and passes
- At least one example game exists under
examples/<lang>/ -
AGENTS.mdEssential Commands lists how to run the new SDK
Reference: Existing Generators
| File | Target | Notes |
|---|---|---|
codegen/gen_csharp.py | .NET 8 | Most complete; handles struct marshaling, builder pattern, DllImport |
codegen/gen_python.py | Python 3 | Uses ctypes; reference for dynamic-type languages |
codegen/gen_ts_node.py | TypeScript (Node) | Uses napi-rs; generates both Rust glue and TypeScript wrapper |
codegen/gen_ts_web.py | TypeScript (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
| Provider | Backend | Dimensions |
|---|---|---|
Rapier2DPhysicsProvider | rapier2d | 2D |
Rapier3DPhysicsProvider | rapier3d | 3D |
NullPhysicsProvider | none | fallback |
PhysicsWorld
The PhysicsWorld resource controls simulation parameters:
- Timestep: fixed at 1/60s by default
- Iterations: 8 velocity, 3 position (configurable)
- Gravity:
Vec2for 2D,Vec3for 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.
| Field | Type | Description |
|---|---|---|
body_type | RigidBodyType | Dynamic, Kinematic, or Static |
velocity | Vec2 | Linear velocity |
mass | f32 | Body mass |
gravity_scale | f32 | Per-body gravity multiplier |
angular_velocity | f32 | Rotational speed |
linear_damping | f32 | Velocity decay per frame |
angular_damping | f32 | Angular velocity decay |
can_sleep | bool | Whether the body can enter sleep state |
Collider
Defines collision geometry attached to a body.
| Field | Type | Description |
|---|---|---|
shape | ColliderShape | Circle, Box (AABB), Capsule, or Polygon |
friction | f32 | Surface friction coefficient |
restitution | f32 | Bounciness (0 = no bounce, 1 = full) |
is_sensor | bool | Trigger-only (no physical response) |
layer | u32 | Collision layer bitmask |
mask | u32 | Which layers this collider interacts with |
Collision Shapes
ColliderShape::Circle(radius)— circle with given radiusColliderShape::Aabb(half_extents)— axis-aligned boxColliderShape::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
| Provider | Backend | Notes |
|---|---|---|
RodioAudioProvider | rodio | Full playback, spatial audio |
NullAudioProvider | none | Silent fallback for headless testing |
AudioSource Component
Attach AudioSource to an entity for audio playback.
| Field | Type | Default | Description |
|---|---|---|---|
volume | f32 | 1.0 | Playback volume (0.0–1.0) |
pitch | f32 | 1.0 | Speed multiplier |
looping | bool | false | Loop playback |
channel | AudioChannel | SFX | Mixing channel |
auto_play | bool | false | Start playing on spawn |
spatial | bool | false | Enable spatial positioning |
max_distance | f32 | 100.0 | Maximum audible distance |
attenuation | AttenuationModel | InverseDistance | Distance falloff model |
Channels
Audio is routed through named channels with independent volume control:
| Channel | Use case |
|---|---|
Music | Background music |
SFX | Sound effects |
Ambience | Environmental audio |
UI | Interface sounds |
Voice | Dialogue and narration |
Volume Control
Three levels of volume control, applied multiplicatively:
- Master volume — global scaling for all audio
- Channel volume — per-channel scaling (Music, SFX, etc.)
- 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:
InverseDistanceandLinearDistance max_distancecontrols 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()andgoud_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 blendgoud_audio_crossfade_to(from, asset, duration, channel)for timed transition to new contentgoud_audio_mix_with(primary, asset, secondary_volume, secondary_channel)for layered playbackgoud_audio_update_crossfades(delta_sec)to advance active timed transitionsgoud_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.
| Field | Type | Default | Description |
|---|---|---|---|
content | String | "" | Text to display |
font_handle | AssetHandle<FontAsset> | — | TrueType font asset |
bitmap_font_handle | Option<AssetHandle<BitmapFontAsset>> | None | Bitmap font (.fnt) |
font_size | f32 | 16.0 | Font size in pixels |
color | Color | white | Text color (RGBA) |
alignment | TextAlignment | Left | Horizontal alignment |
max_width | Option<f32> | None | Word-wrap width |
line_spacing | f32 | 1.0 | Line 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 positionCenter— centered on entity positionRight— 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.
| Field | Type | Description |
|---|---|---|
frames | Vec<Rect> | Source rectangles in the sprite sheet |
frame_duration | f32 | Seconds per frame |
mode | PlaybackMode | Loop or OneShot |
events | Vec<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.
| Field | Type | Description |
|---|---|---|
clip | AnimationClip | Active animation clip |
current_frame | usize | Current frame index |
elapsed | f32 | Time since last frame change |
playing | bool | Whether animation is advancing |
finished | bool | True 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 matchesFloatGreaterThan { param, threshold }— transition when a float exceeds a thresholdFloatLessThan { 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.
| Field | Type | Description |
|---|---|---|
name | String | Layer identifier |
weight | f32 | Blend weight (0.0–1.0) |
blend_mode | BlendMode | Override 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:
| Easing | Description |
|---|---|
Linear | Constant speed |
EaseIn | Starts slow, accelerates |
EaseOut | Starts fast, decelerates |
EaseInOut | Slow start and end |
EaseInBack | Overshoots before settling |
EaseOutBounce | Bounces at the end |
FFI
Animation FFI is in goud_engine/src/ffi/animation/:
goud_animation_controller_*()— state machine operationsgoud_animation_layer_*()— layer stack managementgoud_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:
| Transition | Behavior |
|---|---|
Instant | Immediate switch, no animation |
Fade | Fade out current scene, fade in next |
Custom | SDK-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.
NetworkManagercreates endpoints from a game or context:- C#:
new NetworkManager(gameOrContext) - Python:
NetworkManager(game_or_context) - TypeScript:
new NetworkManager(gameOrContext)
- C#:
NetworkEndpointis returned byhost()/Host()andconnect()/Connect(). It exposesreceive,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.WebSocketon 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 untilpeerCount() > 0before 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:
| Range | Category | Examples |
|---|---|---|
| 0 | Success | SUCCESS |
| 1–99 | Context | ERR_NOT_INITIALIZED, ERR_ALREADY_INITIALIZED |
| 100–199 | Resource | ERR_RESOURCE_NOT_FOUND, ERR_HANDLE_EXPIRED |
| 200–299 | Graphics | ERR_SHADER_COMPILATION_FAILED |
| 300–399 | Entity/ECS | ERR_ENTITY_NOT_FOUND, ERR_COMPONENT_NOT_FOUND |
| 400–499 | Input | Input-related errors |
| 500–599 | System | Platform and window errors |
| 600–699 | Provider | Provider initialization and operation errors |
| 900–999 | Internal | Unexpected failures |
GoudError
The GoudError enum covers all error conditions. Key variants:
NotInitialized/AlreadyInitialized— context lifecycle errorsResourceNotFound(String)/ResourceLoadFailed(String)— asset system errorsEntityNotFound/ComponentNotFound— ECS errorsPhysicsInitFailed(String)/AudioInitFailed(String)— provider errorsInternalError(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 thei32error codegoud_get_last_error_message()— returns a human-readable messagegoud_last_error_subsystem()— which subsystem produced the errorgoud_last_error_operation()— which operation failedgoud_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, orWarning
Recovery
Errors include a RecoveryClass:
Fatal— unrecoverable; the engine must shut downRecoverable— caller can handle and continueRetry— 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.rs—GoudErrorenumcodes.rs— error code constantsdiagnostic.rs— backtrace capturerecovery.rs— recovery strategiesffi_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
- Create a pool with a fixed capacity. All slots are pre-allocated upfront.
- Acquire a slot – the pool returns a
(slot_index, entity_id)pair from its LIFO free-list. - 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
| Function | Description |
|---|---|
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 slotsactive– slots currently in useavailable– slots ready for acquisitionhigh_water_mark– peak simultaneous active slotstotal_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
- Allocate values with
alloc(val)– returns a mutable reference. Each allocation is an O(1) pointer bump. - 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
| Function | Description |
|---|---|
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 usebytes_capacity– total backing storagereset_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
| Operation | EntityPool | FrameArena |
|---|---|---|
| Allocate | O(1) free-list pop | O(1) pointer bump |
| Free | O(1) free-list push | O(1) bulk reset (all at once) |
| Memory | Pre-allocated at creation | Grows as needed, reuses on reset |
| Thread safety | Per-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 Function | Description |
|---|---|
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 Function | Description |
|---|---|
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
| Scenario | Suggested Cell Size |
|---|---|
| Small query radius (< 50 units) | 1x to 2x the query radius |
| Large open world | Larger cells (100-500 units) to reduce memory |
| Dense clusters | Smaller 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
updateonly)
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, clearoperations.rs– insert, remove, updatequeries.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
| Parameter | Description |
|---|---|
context_id | Engine context handle |
category | Null-terminated C string naming the atlas (e.g., “sprites”, “ui”) |
max_width | Maximum atlas width in pixels (0 = default 2048, max 8192) |
max_height | Maximum 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 Function | Description |
|---|---|
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:
| Field | Type | Description |
|---|---|---|
u_min, v_min | f32 | Top-left UV coordinates |
u_max, v_max | f32 | Bottom-right UV coordinates |
pixel_x, pixel_y | u32 | Pixel offset within the atlas |
pixel_w, pixel_h | u32 | Pixel 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:
| Field | Type | Description |
|---|---|---|
texture_count | u32 | Number of packed textures |
width, height | u32 | Atlas dimensions |
used_pixels | u32 | Pixels occupied by packed textures |
total_pixels | u32 | Total atlas area |
efficiency | f32 | Packing efficiency (0.0 to 1.0) |
wasted_pixels | u32 | Unused 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:
- Sorting sprites by z-layer then by texture
- Building a single vertex buffer for all sprites
- 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):
| Field | Type | Description |
|---|---|---|
texture | u64 | Texture handle from goud_texture_load |
x, y | f32 | Position in screen-space pixels |
width, height | f32 | Sprite dimensions on screen |
rotation | f32 | Rotation in radians |
src_x, src_y | f32 | Source rectangle offset in pixel coordinates |
src_w, src_h | f32 | Source rectangle size (0,0 = full texture) |
r, g, b, a | f32 | Color tint and opacity |
z_layer | i32 | Depth sorting (lower values drawn first) |
Drawing
goud_renderer_draw_sprite_batch(context_id, cmds, count) -> u32
| Parameter | Description |
|---|---|
context_id | Engine context handle |
cmds | Pointer to an array of FfiSpriteCmd |
count | Number of commands in the array |
Returns the number of sprites drawn (0 on error).
The renderer:
- Sorts commands by
z_layer, then bytexture - Builds rotated quad vertices with UV mapping
- Groups consecutive sprites with the same texture into GPU batches
- 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):
| Field | Type | Description |
|---|---|---|
font_handle | u64 | Font handle from goud_font_load |
text | *const c_char | Null-terminated UTF-8 string |
x, y | f32 | Position in screen-space pixels |
font_size | f32 | Font size in pixels |
alignment | u8 | 0=Left, 1=Center, 2=Right |
direction | u8 | 0=Auto, 1=LTR, 2=RTL |
max_width | f32 | Maximum line width (0 = no wrap) |
line_spacing | f32 | Line spacing multiplier (default 1.0) |
r, g, b, a | f32 | Text 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
| Scenario | Without Batching | With Batching |
|---|---|---|
| 100 sprites, 5 textures | 100 draw calls | 5 draw calls |
| 500 sprites, 10 textures | 500 draw calls | 10 draw calls |
| 1000 sprites, 1 texture | 1000 draw calls | 1 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:
- Open a window and run a frame loop.
- Draw a player sprite.
- Handle jump input.
- Move one obstacle lane.
- 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 * dty += 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
vyonce 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_ygap_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:
- Getting Started — C#
- Build the five steps above.
- Compare to
examples/csharp/flappy_goud/only when stuck.
Run command:
./dev.sh --game flappy_goud
Python beginner variant
Use this order:
- Getting Started — Python
- Build the five steps above.
- Compare to
examples/python/flappy_bird.pyonly 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:
- Download C# Flappy Goud
- Download Python Flappy Bird
- Download TypeScript Flappy Bird
- Download Rust Flappy Bird
Canonical source locations in this repository:
examples/csharp/flappy_goud/examples/python/flappy_bird.pyexamples/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-mcpbridge
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_labas 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:
imagePngmetadataJsonsnapshotJsonmetricsTraceJson
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:
manifestJsondata
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.
| SDK | Target | Run | Stable route label |
|---|---|---|---|
| C# | Headless | ./dev.sh --game feature_lab | feature-lab-csharp-headless |
| Python | Headless | python3 examples/python/feature_lab.py | feature-lab-python-headless |
| Rust | Headless | cargo run -p feature-lab | feature-lab-rust-headless |
| TypeScript | Desktop | ./dev.sh --sdk typescript --game feature_lab | feature-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:
- Call
goudengine.list_contexts. - Call
goudengine.attach_context. - Use snapshot, control, capture, replay, and metrics tools against that route.
- Use prompt bundles such as:
goudengine.safe_attachgoudengine.inspect_runtimegoudengine.troubleshoot_attach
- Read knowledge resources such as:
goudengine://knowledge/sdk-contractgoudengine://knowledge/mcp-workflowgoudengine://knowledge/sdk-rustgoudengine://knowledge/sdk-csharpgoudengine://knowledge/sdk-pythongoudengine://knowledge/sdk-typescript-desktop
- 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, or3to switch between2D,3D, andHybrid - Move the local bird with
WASDor the arrow keys - See the mouse marker update live
- Press
SPACEto 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-x64andosx-arm64runtime 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
.wasmtogether.
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.wasmand 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 checkcargo test --workspace --quietpython3 sdks/python/test_bindings.pydotnet test sdks/csharp.tests/GoudEngine.Tests.csproj -v minimal(cd sdks/typescript && npm test)bash scripts/check-generated-artifacts.shPATH="$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.gzgoud-engine-console-v<version>-osx-x64.tar.gzgoud-engine-console-v<version>-osx-arm64.tar.gzgoud-engine-console-v<version>-win-x64.tar.gz
Extract the archive for the host machine you build on. The layout is fixed:
| Path | Linux/macOS | Windows |
|---|---|---|
lib/ | lib/libgoud_engine.a | lib/goud_engine.lib |
include/ | include/goud_engine.h | include/goud_engine.h |
The archive is minimal on purpose. It contains the static engine library and the generated public header, nothing else.
Link the Static Library
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_DIRto the compiler include path. - Add the library file from
LIB_DIRto 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:
- The generated C ABI in
goud_engine.h - The provider traits in Rust, especially
RenderProvider - 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
RenderProviderin 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
- Link the static library and confirm the title boots.
- Stand up a
RenderProviderthat can clear the screen and present. - Add buffer, texture, and shader creation.
- Add sprite draws, then text, then mesh and particle paths.
- Fill in diagnostics after the frame loop is stable.
Related Docs
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:
- Create the game.
- Call
await game.preload([...])for the path-based textures/fonts you know you need. - 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 testsgoudengine/web: browser WebSocket client connections
Current limitation:
- Browser hosting is not supported.
- Use
NetworkProtocol.WebSocketon the web target. connect()returns before the socket is fully open, so wait forpeerCount() > 0before 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.
Recommended smoke path
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:
- Run
./codegen.sh - Run
bash scripts/check-generated-artifacts.sh
Verification:
cargo checkcompletes 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:
- Run
./codegen.sh - Run
python3 scripts/generate-doc-snippets.py - Run
cd sdks/typescript && node scripts/generate-doc-media.mjs - Run
python3 scripts/generate-showcase-docs.py - 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:
- Run
cd sdks/typescript && node scripts/generate-doc-media.mjs - Run
python3 scripts/generate-doc-snippets.py - Run
python3 scripts/generate-showcase-docs.py - 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-mcpstarts, butgoudengine.list_contextsreturns 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:
- Start a desktop or headless process with debugger mode enabled before creation.
- Use one of the shipped Feature Lab examples if you want a known-good route:
./dev.sh --game feature_labpython3 examples/python/feature_lab.pycargo run -p feature-lab./dev.sh --sdk typescript --game feature_lab
- Re-run
cargo run -p goudengine-mcp, then callgoudengine.list_contextsandgoudengine.attach_context.
Verification:
goudengine.list_contextsshows one of the stable Feature Lab labels such asfeature-lab-csharp-headless,feature-lab-python-headless,feature-lab-rust-headless, orfeature-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:
cargo build --releaseGOUD_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:
- Verify binaries:
file sdks/csharp/runtimes/osx-x64/native/libgoud_engine.dylibfile sdks/csharp/runtimes/osx-arm64/native/libgoud_engine.dylib
- 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:
- Use repo command:
./dev.sh --sdk typescript --game flappy_bird_web - 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:
- Build once to refresh manifest.
- Run
python3 codegen/validate.py - Update
codegen/goud_sdk.schema.jsonand/orcodegen/ffi_mapping.json - 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:
- Run
./codegen.sh - Run
cd sdks/typescript && npm test
Verification:
- Test suite is green with regenerated outputs.
[SDK] Clean-room regenerate fails
Symptoms:
scripts/clean-room-regenerate.shfails after deleting generated files.
Cause:
- Regeneration pipeline no longer rebuilds from source truth.
Fix:
- Run
bash scripts/clean-room-regenerate.sh - If docs are required, run:
PATH="$HOME/.cargo/bin:$HOME/.dotnet/tools:$PATH" bash scripts/clean-room-regenerate.sh --docs
- 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:
- Prefer headless-safe smoke paths for CI.
- 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:
- Install required OpenGL/GLFW runtime packages.
- 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:
- Review Web Platform Gotchas
- 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:
- Use
NetworkProtocol.WebSocket - Point it at a browser-reachable
ws://orwss://URL - Wait for
peerCount() > 0before first send - Validate with:
./dev.sh --sdk typescript --game sandbox_webcd examples/typescript/feature_labnpm run smoke:web-network
Verification:
- Browser smoke passes with a real
ping/pongroundtrip.
Example and tutorial issues
[Examples] Not sure which example to run first
Use this order:
- Flappy Bird for baseline behavior parity.
- Sandbox for the full cross-language feature tour.
- Feature Lab for supplemental smoke coverage.
- C# specialization demos for renderer/gameplay patterns.
Reference:
examples/README.md- Sandbox Guide
- Example Showcase
[Tutorial] Need the final code for Build Your First Game
Use shipped final projects:
examples/csharp/flappy_goud/examples/python/flappy_bird.pyexamples/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_webcd 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:
Related guides
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.
| Example | SDK | Target | Path | Run command | Description | Media | Source |
|---|---|---|---|---|---|---|---|
| Flappy Goud | C# | Desktop | examples/csharp/flappy_goud | ./dev.sh --game flappy_goud | Flappy Bird parity baseline. | ![]() | GitHub |
| Flappy Bird | Python | Desktop | examples/python/flappy_bird.py | ./dev.sh --sdk python --game flappy_bird | Python parity baseline. | ![]() | GitHub |
| Flappy Bird | TypeScript | Desktop | examples/typescript/flappy_bird/desktop.ts | ./dev.sh --sdk typescript --game flappy_bird | TypeScript desktop parity baseline. | ![]() | GitHub |
| Flappy Bird | TypeScript | Web | examples/typescript/flappy_bird/web | ./dev.sh --sdk typescript --game flappy_bird_web | TypeScript web parity baseline. | ![]() | GitHub |
| Flappy Bird | Rust | Desktop | examples/rust/flappy_bird | cargo run -p flappy-bird | Native Rust parity baseline. | ![]() | GitHub |
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.
| Example | SDK | Target | Path | Run command | Description | Media | Source |
|---|---|---|---|---|---|---|---|
| Sandbox | C# | Desktop | examples/csharp/sandbox | ./dev.sh --game sandbox | Manifest-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. | ![]() | GitHub |
| Sandbox | Python | Desktop | examples/python/sandbox.py | ./dev.sh --sdk python --game sandbox | Manifest-driven Python sandbox following the shared asset pack, three-panel HUD, and peer packet contract on this branch. | ![]() | GitHub |
| Sandbox | TypeScript | Desktop | examples/typescript/sandbox/desktop.ts | ./dev.sh --sdk typescript --game sandbox | Manifest-driven TypeScript desktop sandbox following the shared HUD and peer packet contract on this branch. | ![]() | GitHub |
| Sandbox | TypeScript | Web | examples/typescript/sandbox/web | ./dev.sh --sdk typescript --game sandbox_web | Browser/WASM sandbox with the same HUD structure and explicit capability-gated networking and renderer copy. | ![]() | GitHub |
| Sandbox | Rust | Desktop | examples/rust/sandbox | cargo run -p sandbox | Manifest-driven Rust sandbox using the shared asset pack and GoudGame::draw_text(...) for the branch’s recovered native HUD path. | ![]() | GitHub |
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.
| Example | SDK | Target | Path | Run command | Description | Media | Source |
|---|---|---|---|---|---|---|---|
| Feature Lab | C# | Headless | examples/csharp/feature_lab | ./dev.sh --game feature_lab | Headless smoke for ECS and provider wrappers with debugger mode enabled and stable local attach coverage. | ![]() | GitHub |
| Feature Lab | Python | Headless | examples/python/feature_lab.py | python3 examples/python/feature_lab.py | Headless smoke for generated wrappers with debugger mode enabled and stable local attach coverage. | ![]() | GitHub |
| Feature Lab | TypeScript | Desktop | examples/typescript/feature_lab/desktop.ts | ./dev.sh --sdk typescript --game feature_lab | Desktop smoke for SDK capability probes plus debugger manifest, snapshot, and local attach coverage. | ![]() | GitHub |
| Feature Lab | TypeScript | Web | examples/typescript/feature_lab/web | ./dev.sh --sdk typescript --game feature_lab_web | Web smoke with browser/WASM coverage. Debugger attach remains desktop-only in this batch. | ![]() | GitHub |
| Feature Lab | Rust | Headless | examples/rust/feature_lab | cargo run -p feature-lab | Native Rust headless smoke for SDK coverage with debugger enablement and stable local attach coverage. | ![]() | GitHub |
C# specialization demos
C#-specific gameplay and renderer examples.
| Example | SDK | Target | Path | Run command | Description | Media | Source |
|---|---|---|---|---|---|---|---|
| 3D Cube | C# | Desktop | examples/csharp/3d_cube | ./dev.sh --game 3d_cube | 3D renderer demo. | ![]() | GitHub |
| Goud Jumper | C# | Desktop | examples/csharp/goud_jumper | ./dev.sh --game goud_jumper | Platform movement and collision. | ![]() | GitHub |
| Isometric RPG | C# | Desktop | examples/csharp/isometric_rpg | ./dev.sh --game isometric_rpg | Isometric camera and RPG systems. | ![]() | GitHub |
| Hello ECS | C# | Desktop | examples/csharp/hello_ecs | ./dev.sh --game hello_ecs | Minimal ECS starter. | ![]() | GitHub |
| Character Sandbox | C# | Desktop | examples/csharp/character_sandbox | ./dev.sh --game character_sandbox | 3D character sandbox with model loading, skeletal animation, scene management. | ![]() | GitHub |
Starter demos
Single-file SDK demos used for quick setup checks.
| Example | SDK | Target | Path | Run command | Description | Media | Source |
|---|---|---|---|---|---|---|---|
| Python SDK Demo | Python | Desktop | examples/python/main.py | ./dev.sh --sdk python --game python_demo | Minimal Python startup demo. | ![]() | GitHub |
Notes
- This page is generated. Update
examples/showcase.manifest.jsonand 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
| Status | Meaning |
|---|---|
draft | Work in progress, not ready for review |
proposed | PR open, ready for review |
accepted | PR merged with review approval |
implemented | Code is shipped; RFC is complete |
superseded | Replaced by a later RFC (link to successor) |
Acceptance requires at least one PR review approval from a maintainer. The proposed → accepted 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
- Copy the template:
docs/rfcs/RFC-0000-template.md - Assign the next number and rename the file
- Fill in: motivation, detailed design, drawbacks, alternatives considered
- Open a PR; set
status: proposedin the front matter - Address review feedback on the PR
- On merge:
rfc-approve.ymlautomatically setsstatus: accepted; update the index below
Index
| RFC | Title | Status |
|---|---|---|
| RFC-0001 | Provider Trait Pattern | accepted |
| RFC-0002 | NetworkProvider Trait Design | accepted |
| RFC-0003 | UI Layout and Input Behavior | draft |
| RFC-0004 | Debugger Runtime, Snapshot Contract, and Local Attach Model | accepted |
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
wgpubackend requires duplicating every code path that touchesbackendandsprite_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-in | Feature Flag | Notes |
|---|---|---|
OpenGLRenderProvider | native | Wraps existing OpenGLBackend |
WgpuRenderProvider | wgpu | F02-03; wraps existing wgpu_backend/ modules |
NullRenderProvider | always | No-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-in | Feature Flag | Notes |
|---|---|---|
Rapier2DPhysicsProvider | rapier2d | 2D rigid-body physics |
Rapier3DPhysicsProvider | rapier3d | 3D rigid-body physics |
SimplePhysicsProvider | always | AABB collision + gravity only, no rapier dependency |
NullPhysicsProvider | always | No-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-in | Feature Flag | Notes |
|---|---|---|
RodioAudioProvider | audio | Uses existing rodio integration |
WebAudioProvider | web | Browser/WASM via web-sys |
NullAudioProvider | always | No-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-in | Feature Flag | Notes |
|---|---|---|
GlfwWindowProvider | native | Wraps existing GLFW platform layer |
WinitWindowProvider | winit | Future — needed for mobile/web targets |
NullWindowProvider | always | No-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-in | Feature Flag | Notes |
|---|---|---|
GlfwInputProvider | native | Reads from GLFW event queue |
NullInputProvider | always | All buttons unpressed |
3.3 Lifecycle Protocol
Every provider follows a five-phase lifecycle:
- Create — constructed with a config struct (
RenderConfig,PhysicsConfig, etc.) - Init —
init()called duringGoudGame::new(). Failure is fatal unless a fallback is configured (§3.9). - Update — per-frame
update(delta)for providers that need it (physics, audio). - Shutdown —
shutdown()called duringGoudGame::drop(). Must not fail. - 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:
shutdown()on active provider → drop it- Create and
init()replacement provider - Invalidate all resource handles (textures, buffers, shaders) from old provider
- 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.
| Component | Layer | Path |
|---|---|---|
| Provider trait definitions | Layer 1 | goud_engine/src/libs/providers/ (new module) |
| Concrete implementations | Layer 1 | goud_engine/src/libs/providers/impls/ |
ProviderRegistry | Layer 2 | goud_engine/src/core/providers/registry.rs |
Builder (GoudEngine::builder()) | Layer 2 | goud_engine/src/core/providers/builder.rs |
| FFI enum selection | Layer 3 | goud_engine/src/ffi/providers.rs |
| SDK enum wrappers | Layer 4 | generated 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
GoudGamestruct changes:Option<OpenGLBackend>andSpriteBatch<OpenGLBackend>fields are replaced byProviderRegistryandOption<Box<dyn WindowProvider>>.SpriteBatch<B: RenderBackend>generic parameter changes:SpriteBatchwill receive a&mut dyn RenderProvideror a concrete backend reference via downcast. The public API surface ofSpriteBatchmay change.GoudGame::new(width, height, title, renderer_type)signature expands to accept physics and audio provider selections.
FFI Changes
goud_game_creategainsGoudPhysicsTypeandGoudAudioTypeparameters.- 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::Nullto 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+Nullphysics +Nullaudio 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
OpenGLRenderProviderwrapping the existing backend. - F02-04: Implement
Rapier2DPhysicsProvider. - F02-05: Implement
RodioAudioProvider. - F02-06: Split
PlatformBackendintoWindowProvider+GlfwInputProvider. - F02-07: Wire
ProviderRegistryintoGoudGame, remove hardcoded fields. - F02-08: Hot-swap mechanism (dev-tools feature only).
- F02-09: FFI enum selection + SDK codegen updates.
6. Resolved Decisions
-
Window+Input: Keep Separate.
WindowProviderandInputProviderremain separate traits.glfw_platform.rspumps events viapoll_events(&mut self, input: &mut InputManager)— tight event dispatch but loose storage.InputManageris an independent resource, andGamepadStateinfrastructure incore/input_manager/types.rsexists 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.WindowProvideris!Send + !Sync(GLFW main-thread) whileInputProvidercan beSend + Sync. AWindowInputBridgehelper wires GLFW events →InputProviderfor the common case. -
Resource Ownership: Provider-Owned. Providers own their resources. A shared
ResourcePoolis deferred to post-stabilization.RenderBackendalready uses handle-based resource management (create_buffer(),create_texture(),destroy_*()), andGoudGameis the single root owner. No cross-subsystem resource sharing exists today. Extension point: providers MAY exposefn shared_resources(&self) -> Option<&dyn Any>for future cross-provider sharing. -
NullProvider: Explicit Structs. Use explicit
Null*Providerstructs, not default method implementations. The codebase usesOption<T>for conditional features andAssetStateenum 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. -
Async & Main-Thread: Document Constraint, Defer. The
!Sendconstraint onGoudGamewhen a nativeWindowProvideris 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.rslines 7–10) andGoudContextis already!Send + !Sync. If an async executor is adopted later,WindowProvidercalls must be scheduled on the main thread via aMainThreadSchedulerthat 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
'staticclosures that complicate borrow lifetimes.drain_eventsmatchesPhysicsProvider::drain_collision_eventsfrom RFC-0001. ConnectionIdnotPeerId.ConnectionIdis 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_eventscollects that work synchronously (see §3.5). - Owned
Vecreturn accepted.drain_eventsreturnsVec<NetworkEvent>whereReceivedvariants containdata: Vec<u8>, causing per-message heap allocations. This mirrorsPhysicsProvider::drain_collision_eventsfrom RFC-0001, which justified the pattern to avoid lifetime coupling. Network payloads are larger than collision events, butdrain_eventsruns once per frame (not per-packet), and theVecis 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
| Implementation | Feature Flag | Notes |
|---|---|---|
UdpNetProvider | net-udp | UDP transport; desktop targets |
WebSocketNetProvider | net-ws | WebSocket transport; web/WASM targets |
NullNetProvider | always | No-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
| Component | Layer | Path |
|---|---|---|
NetworkProvider trait + supporting types | Layer 1 | goud_engine/src/libs/providers/network.rs |
| Concrete implementations | Layer 1 | goud_engine/src/libs/providers/impls/network/ |
ProviderRegistry (gains network field) | Layer 2 | goud_engine/src/core/providers/registry.rs |
| FFI functions | Layer 3 | goud_engine/src/ffi/network.rs |
| SDK wrappers | Layer 4 | generated 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: gainsnetwork: Option<Box<dyn NetworkProvider>>. Existing construction code continues to compile; the field defaults toNone.- Error types: network errors use
ProviderError { subsystem: "network", message }from RFC-0001; error codes 700–709 reserved. No newGoudErrorvariant needed. - FFI: new
goud_network_*functions ingoud_engine/src/ffi/network.rs. No existing FFI function signatures change. C# bindings regenerate automatically oncargo build. Pythongenerated/_ffi.pyrequires manual update. - SDK wrappers: generated from
goud_sdk.schema.jsonfor 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
-
Optional vs mandatory in
ProviderRegistry. UsingOption<Box<dyn NetworkProvider>>requires every network call site to unwrap. An alternative is always storingNullNetProviderand removing theOption. The choice affects how the engine signals “networking not configured” vs “networking failed.” -
Async I/O strategy. Background
std::threadper 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. -
ConnectionIdreuse policy. After a connection closes, can itsConnectionIdvalue 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. -
Max message size enforcement.
NetworkCapabilities::max_message_sizedeclares the limit but the trait does not specify whethersendsilently truncates, returns an error, or panics on oversized messages. The current leaning is that implementations MUST returnErr(ProviderError)on oversized data (no silent truncation, no panics), making a caller-side bounds check optional but safe. This must be confirmed before implementation. -
Encryption and TLS surface.
HostConfigandconnecthave no TLS parameters. If encryption is required,HostConfigneeds 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 (
UiNodelayout + 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 | Stretchmargin:{ 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_rectinset bypadding; child layout is computed incontent_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.lefty = parent.y + margin.top
Center:- Node is centered in parent, then offset by margins:
x = parent.center_x - node.width/2 + margin.left - margin.righty = parent.center_y - node.height/2 + margin.top - margin.bottom
BottomRight:x = parent.max_x - margin.right - node.widthy = parent.max_y - margin.bottom - node.height
Stretch:x = parent.x + margin.lefty = parent.y + margin.topwidth = 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 justcontent_rect). - Child layout never uses parent
node_rectdirectly; it always uses parentcontent_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 = YColumn: 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.
Stretchsets child cross size to remaining cross-axis space after margins.
spacing:- Fixed gap inserted between adjacent children.
- Total gap =
spacing * (child_count - 1)whenchild_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
HoverLeavefor previous node. - Dispatch
HoverEnterfor new node.
- Dispatch
- Order is always leave then enter in the same frame.
Focus traversal (Tab)
- Focusable set: visible + enabled + focusable nodes.
- Traversal order: deterministic tree order.
Tabmoves focus forward;Shift+Tabmoves backward.- Traversal wraps at ends (last -> first, first -> last).
Keyboard activation for focused button
- If focused node is a button:
Entertriggers activation.Spacetriggers 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
-
Immediate-mode UI only (no retained
UiNodetree). Rejected because current engine direction already uses retained node/component state and requires persistent focus/hover behavior. -
Single-pass input without consumption. Rejected because UI and gameplay would both react to the same input, causing double-activation bugs.
-
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
- Should focus traversal order be configurable (tree order vs explicit tab index) in a follow-up?
- Should button activation differentiate keydown/keyup timing for
Spacefor stricter accessibility parity? - Should clipping and scroll containers alter hit testing rules in a follow-up RFC?
RFC-0004: Debugger Runtime
rfc: “0004” title: “Debugger Runtime, Snapshot Contract, and Local Attach Model” status: accepted created: 2026-03-12 authors: [“aram-devdocs”] tracking-issue: “#511” related-issues: [“#513”, “#517”, “#520”]
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.rscarriesshow_fps_overlay,physics_debug, anddiagnostic_mode.goud_engine/src/sdk/engine_config.rsforwards part of that config model for windowed game creation.goud_engine/src/sdk/context.rsexposes a bareGoudContextlifecycle API with no config object.goud_engine/src/ffi/debug.rsandgoud_engine/src/ffi/window/state.rsexpose 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:
#511debugger singleton architecture and MCP contract,#513snapshot schema and service-health model,#517local transport, attach protocol, and local-only security policy,#520shared debugger enablement contract.
3. Design
3.1 Scope and Non-Goals
Scope:
- Process model for the debugger runtime and the
goudengine-mcpbridge. - Shared type contract for runtime identity, local discovery, attach, snapshots, and service health.
- Enablement contract for
GameConfig,EngineConfig, and a future config-basedGoudContextflow. - 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:
| Component | Process | Owner | Responsibility |
|---|---|---|---|
DebuggerRuntime | Game process | Rust engine | Produces snapshots, service health, stats, control hooks, capture/replay hooks, and route registration |
goudengine-mcp | Separate local process | Local developer tool | Speaks MCP over stdio and forwards requests over local attach transport |
Topology rules:
- One
DebuggerRuntimeexists 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-mcpis 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_nonceis generated once at runtime-service startup and changes on each process start.context_idmaps to the existing engine context identity used by runtime code and FFI.surface_kinddisambiguates windowed and headless flows that may share engine infrastructure. Wire serialization ofRuntimeSurfaceKindmust use"windowed_game","headless_context", and"tool_context", and parsers must reject other spellings such asWindowedGame.- 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:
- Debugger mode is enabled during engine/context creation.
- The process creates or reuses the process-wide
DebuggerRuntime. - Each eligible context registers a
RuntimeRouteIdwith the runtime. - The runtime publishes discovery metadata for the process and its routes only when
publish_local_attach = trueand at least one attachable route exists. - Local tools attach to one route at a time.
- Context shutdown removes that route.
- 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:
| Capability | Consumer examples | Producer/owner |
|---|---|---|
| Snapshots | overlays, MCP, SDK helpers | debugger runtime core |
| Profiling | overlays, MCP, exports | profiler subsystem through debugger runtime |
| Render stats | overlays, MCP | renderer adapter through debugger runtime |
| Memory stats | overlays, MCP | memory/statistics adapter through debugger runtime |
| Entity inspection | overlays, MCP, SDK helpers | scene/entity inspector through debugger runtime |
| Debug draw | overlays, SDK helpers | debug-draw/control layer |
| Control plane | overlays, MCP, SDK helpers | debugger runtime control layer |
| Replay | MCP, SDK helpers | replay subsystem through debugger runtime |
| Capture | MCP, SDK helpers | capture subsystem through debugger runtime |
| SDK knowledge | MCP resources/prompts | goudengine-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:
DebuggerRuntimeis the source of truth.- FFI exports expose runtime-owned operations and snapshots to non-Rust SDKs.
- SDK convenience APIs wrap the FFI or Rust runtime surface. They do not invent SDK-local debugger behavior.
goudengine-mcpattaches 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:
DebuggerConfigis the canonical debugger-mode value object.enabled = falsemeans no debugger runtime startup, no route registration, and near-zero overhead outside existing debug features.publish_local_attach = trueallows the runtime to publish local discovery metadata and accept local attachments.route_labelis optional display metadata for tools. It is not the stable route identity.
Init-surface mapping:
| Surface | Contract |
|---|---|
GameConfig | Gains debugger: DebuggerConfig as the source of debugger-mode settings for windowed Rust flows |
EngineConfig | Wraps the same GameConfig.debugger contract and exposes builder helpers without redefining it |
GoudContext | Gains 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/ContextConfigmodel 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 encodingpidandprocess_noncein the manifestlocation. Operational rules: - One manifest per process, not per context, and no manifest when
publish_local_attach = falseor 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
pidandprocess_nonce, the highestpublished_at_unix_msis 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.locationmust include bothpidandprocess_nonce. - Readers must treat manifests as stale when the
pidis 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-
pidcases. - 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_errorand immediately close the session without draining the frame. - Versioning:
AttachHelloV1.protocol_versionandAttachAcceptedV1.protocol_versionare fixed to1in this phase. - Heartbeat sender:
goudengine-mcpsends{"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_msis0, 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 exactly2 * 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_errorplus immediate session close. Failures use{"type":"error","code":"...","message":"..."}with codes fromprotocol_error,version_mismatch,route_not_found,route_not_attachable, andattach_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_idis 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-mcpis 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:
0700directories and0600manifest 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:
| State | Meaning |
|---|---|
ready | Data or control path is available and functioning |
disabled | Feature exists but is turned off for this runtime |
unavailable | Feature does not exist for this platform/runtime/provider combination |
faulted | Feature 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 section | Produced by |
|---|---|
route_id, debugger, services.debugger | debugger runtime |
frame | debugger runtime frame coordinator |
selection, scene, entities | scene/entity inspector |
services.renderer, stats.render | renderer adapter |
services.memory, stats.memory | memory/statistics adapter |
services.profiling | profiler subsystem through debugger runtime |
services.physics | physics adapter |
services.audio | audio adapter |
services.network, stats.network | network adapter |
services.window | window/platform adapter |
services.assets | asset manager |
services.capture | capture subsystem |
services.replay | replay subsystem |
diagnostics | error/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:
| Batch | Must preserve |
|---|---|
2.5.2 engine substrate | one process-wide runtime, route registration, runtime-owned capabilities |
2.5.3 control/protocol/replay | local-only attach model, route-scoped sessions, snapshot/service-health names |
2.5.4 public surface rollout | one Rust-owned enablement model and no SDK-local debugger runtimes |
2.5.5 DX/docs/Feature Lab | bridge-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
-
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.
-
Separate debugger enablement models per SDK. Rejected because the later FFI and codegen rollout would fragment the contract and create SDK-only behavior.
-
Post-create debugger enablement as the canonical
GoudContextpath. Rejected becauseGameConfigandEngineConfigare already pre-init configuration surfaces. A future config-basedContextConfigkeeps the model aligned. -
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.
-
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.
GameConfigandEngineConfigwill converge onDebuggerConfig.- Standalone
GoudContextwill 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
- 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
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
-
Adding
husky-rsto 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 -
Create hooks directory:
mkdir -p .husky/hooks -
Add a hook (e.g.,
pre-commit):echo '#!/bin/sh\necho "Running pre-commit hook"' > .husky/hooks/pre-commit -
Install hooks:
Note: Due to the execution mechanism of
build.rs, runningcargo cleanis 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-commitprepare-commit-msgcommit-msgpost-commitpre-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
MyTypeand there exists two things in your project with the type nameMyType, 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>, andPin<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"]




















