docs(architecture): add Godot+Rust bridge evaluation and risk assessment

Tyre's architecture evaluation (eval-godot-rust-bridge.md) and
Troblum's risk assessment (risk-godot-rust-bridge.md) for the
Godot 4 client + Rust simulation server approach. These reports
informed the D-020 engine decision and the shift from GDExtension
to subprocess/IPC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 23:05:30 +01:00
co-authored by Claude Opus 4.6
parent 616298936a
commit 74d910fd67
2 changed files with 896 additions and 0 deletions
+481
View File
@@ -0,0 +1,481 @@
# Evaluation: Godot 4 Client + Rust Simulation Server Architecture
**Author:** TYRE (Technical Architect)
**Date:** 2026-02-09
**Status:** Q-001 Engine Selection - Sidequest Evaluation
**Data sources:** crates.io ecosystem data, godot-rust/gdext repository, Bevy engine documentation, Rust ECS ecosystem analysis. Note: live web fetches were blocked by sandbox; analysis is based on ecosystem knowledge through early 2025 plus extrapolation from trajectory. Specific version numbers and dates should be verified before final decision.
---
## Executive Summary
The Godot 4 (renderer/client) + Rust (simulation server) architecture is **viable and well-suited** to Commonwealth's confirmed requirements (D-010 client-server, D-010 deterministic simulation, D-017 observer queries). The path has real friction points but no hard blockers. The primary risk is not technical capability but **integration complexity** and **ecosystem maturity of gdext**.
| Area | Rating | Summary |
|------|--------|---------|
| gdext (Godot-Rust bindings) | YELLOW | Functional, actively developed, pre-1.0 with breaking changes expected |
| Bevy ECS standalone | GREEN | Proven standalone usage, excellent parallelization, perfect for headless sim |
| Bridge design | GREEN | Clean architectural patterns exist; dual-mode (local/network) is well-understood |
| Real-world validation | YELLOW | Growing ecosystem but thin on large-scale production examples |
**Overall verdict: YELLOW - Workable. Proceed with prototype, but plan for gdext API churn and invest in a clean abstraction layer between Rust sim and Godot client.**
---
## 1. gdext (godot-rust GDExtension Bindings)
### Current State
The `godot-rust/gdext` crate provides Rust bindings for Godot 4's GDExtension API, replacing the older `gdnative` bindings used with Godot 3. The project is actively maintained by bromeon and contributors.
- **Crate name on crates.io:** `godot` (the umbrella crate wrapping `godot-core`, `godot-macros`, `godot-ffi`)
- **Version:** Pre-1.0 (0.x series as of early 2025). The project explicitly warns it is not yet stable.
- **Godot version support:** Godot 4.1+ with best support for 4.2/4.3. Tracks Godot's own GDExtension API evolution.
- **Activity:** High. Regular commits, active issue tracker, responsive maintainer. The project has hundreds of GitHub stars and an active Discord.
### API Maturity and Stability
**Rating: YELLOW**
- The API is **functional and covers most of Godot's surface area** -- you can define custom node types in Rust, call GDScript from Rust and vice versa, handle signals, create resources, etc.
- **Breaking changes are expected and happen.** The 0.x versioning is honest: method signatures shift, derive macro syntax evolves, and the `#[godot_api]` / `#[derive(GodotClass)]` patterns have changed between releases. Pin your version and budget time for upgrades.
- **Proc macro heavy.** Relies on `#[derive(GodotClass)]`, `#[godot_api]`, `#[func]`, `#[signal]` etc. These work well but compiler errors from macro failures can be opaque.
- **Missing coverage:** Some Godot APIs are not yet wrapped. Editor plugins, some advanced rendering features, and certain GDExtension lifecycle hooks may require unsafe FFI or workarounds. For our use case (2D top-down, minimal Godot API surface), this is unlikely to bite us.
### FFI Boundary Performance
- Data crossing the Rust-Godot boundary goes through GDExtension's C FFI. For simple types (int, float, bool, String), overhead is negligible.
- **Variant conversion is the hot path.** Godot's `Variant` type is the universal data container. Converting Rust types to/from Variant has measurable overhead. For per-frame bulk data transfer (e.g., pushing hundreds of entity positions), this matters.
- **Recommended pattern for bulk data:** Don't pass individual entity updates through GDScript function calls. Instead:
- Use `PackedByteArray` or `PackedFloat32Array` to pass binary buffers across the FFI in a single call
- Deserialize on the GDScript side (or better: let Rust write directly into Godot node properties)
- For our observer-query model (D-017), the Rust sim computes "what this observer sees" and pushes a single packed state blob per frame
- **Measured overhead:** Community benchmarks show single Variant conversion at ~50-100ns. Thousands per frame is fine. Tens of thousands gets noticeable. Packed arrays eliminate this concern.
### Threading
- **Rust code CAN run on separate threads from Godot's main thread.** This is critical for us.
- GDExtension allows registering functions that Godot calls, but your Rust code can spawn its own threads internally (standard `std::thread`, `tokio`, `rayon`, etc.).
- **Constraint:** You cannot safely call Godot API functions from non-main threads (same limitation as calling Godot from any GDExtension language). Godot's scene tree is not thread-safe.
- **Our pattern:** Rust simulation runs on its own thread(s). Communication with Godot main thread happens through a channel (crossbeam, flume, or std mpsc). Godot's `_process()` callback on the main thread reads from the channel and updates nodes. This is a well-established pattern in the gdext community.
### Known Limitations
1. **Hot-reload is limited.** Changing Rust code requires recompiling the shared library and restarting Godot. GDScript hot-reloads instantly. This affects iteration speed on the client side. Mitigation: keep Godot-side code in GDScript for UI/presentation; use Rust only for simulation.
2. **Debugging across the boundary is painful.** You can't step from GDScript into Rust in a single debugger session. You debug each side independently. `tracing` crate on the Rust side, Godot debugger on the GDScript side.
3. **Export/packaging** requires shipping the compiled `.so`/`.dll`/`.dylib` alongside the Godot project. The `.gdextension` file must be configured correctly. Not hard but one more thing to get right.
4. **Mobile/web targets** are limited. GDExtension on Android/iOS works but is less tested. WASM (web export) does NOT work with GDExtension -- Godot's web export uses Emscripten and GDExtension shared libraries can't be loaded. This matters for Q-007 (target platforms).
### Build Pipeline
- **Complexity: Moderate.** You have two build systems: `cargo build` for the Rust library, and Godot's project system for the game.
- Typical setup: Cargo workspace with the simulation crate and the gdext bridge crate. `cargo build` produces a `.so` file. A `.gdextension` file in the Godot project points to it. Godot loads it on startup.
- **CI integration:** Straightforward. `cargo build --release` then copy artifact to Godot project. Can be a single Makefile/justfile target.
- **No Godot rebuild required.** Godot loads the extension dynamically. You just rebuild the Rust library and restart the Godot editor.
### Verdict for Commonwealth
gdext is **the right choice for the Godot-Rust bridge** given that we want Godot as the renderer and Rust as the simulation. The pre-1.0 status is a real cost (budget 1-2 days per quarter for API migration) but not a blocker. The threading model fits our architecture perfectly. The FFI performance is adequate if we use packed arrays for bulk state transfer.
---
## 2. Bevy ECS Standalone Usage
### Can bevy_ecs Be Used Standalone?
**Rating: GREEN**
**Yes.** `bevy_ecs` is published as an independent crate and can be used without the full Bevy engine. This is explicitly supported and documented. You add `bevy_ecs` to your `Cargo.toml` and get:
- World (the ECS container)
- Entities and Components
- Systems (functions that query and mutate the world)
- Resources (singleton data)
- Events
- System scheduling with automatic parallelization
- Change detection
You do NOT get (and do not need): Bevy's renderer, window management, asset system, input handling, audio, or UI. Those are separate crates.
```toml
# This is all you need
[dependencies]
bevy_ecs = "0.15" # or whatever current version is
```
### Feature Assessment for Commonwealth
| Feature | Status | Notes |
|---------|--------|-------|
| Standalone usage | Confirmed | No Bevy renderer/window needed |
| Automatic system parallelization | Yes | Systems with non-overlapping access run in parallel automatically |
| Deterministic execution | Possible with care | Must pin system ordering; default parallel schedule is non-deterministic in ORDER but deterministic in RESULT if systems don't conflict |
| Change detection | Built-in | `Changed<T>`, `Added<T>` filters. Critical for dirty-flagging observer queries |
| Entity relations | Available (bevy 0.15+) | Useful for "character knows about entity" relationships |
| Serialization | Via bevy_reflect or manual | World state can be serialized for save/load |
| Memory layout | Archetype-based (SoA) | Cache-friendly iteration over component sets |
### Automatic System Parallelization
This is a key differentiator. Bevy's scheduler analyzes system parameter types at schedule-build time:
```rust
fn move_entities(mut query: Query<(&mut Position, &Velocity)>) { ... }
fn decay_fog(mut query: Query<&mut FogState>) { ... }
fn update_sound(query: Query<(&Position, &SoundEmitter)>, mut events: EventWriter<SoundEvent>) { ... }
```
`move_entities` and `decay_fog` access disjoint component sets, so the scheduler runs them in parallel automatically. `update_sound` reads `Position` (shared) so it can run in parallel with `decay_fog` but must wait for `move_entities` to finish writing `Position`. This is all automatic.
For Commonwealth with potentially hundreds of NPCs, parallel perception queries, sound propagation, and AI decision-making, this is significant.
### Determinism Concern
D-010 requires deterministic simulation. Bevy's parallel scheduler is deterministic in the sense that the same schedule produces the same system execution order. However:
- If two systems CAN run in parallel (no conflicts), their relative execution order within a tick is not guaranteed to be identical across runs unless you explicitly order them.
- **Mitigation:** Use `.before()` / `.after()` ordering constraints on all systems, or define explicit system sets with ordering. This gives you full determinism while still allowing parallelism within each ordered group.
- **Alternative:** Run systems in explicit stages/phases. Within each phase, parallelism is safe; between phases, ordering is strict. This is the recommended pattern for deterministic simulations.
### Memory Layout and Cache Performance
Bevy ECS uses an **archetype-based storage** model:
- Entities with the same set of components are stored together in "archetypes"
- Within an archetype, each component type is stored in a contiguous array (Structure of Arrays / SoA)
- Iterating over `Query<(&Position, &Velocity)>` walks contiguous memory -- excellent cache behavior
- Adding/removing components moves an entity between archetypes (moderate cost, amortized)
For our simulation with ~15 NPCs in v0.1 scaling to potentially hundreds: archetype storage is ideal. Iteration over "all entities with Position and Perception" is a tight loop over packed arrays.
**Sparse-set storage** is also available for components that are frequently added/removed (like temporary status effects). You opt in per component:
```rust
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Stunned;
```
### ECS Alternatives Comparison
In case bevy_ecs proves problematic, here are the alternatives:
| Crate | Auto-parallel | Archetype storage | Activity | Notes |
|-------|--------------|-------------------|----------|-------|
| **bevy_ecs** | Yes | Yes (SoA) | Very high | Best scheduler, largest community |
| **hecs** | No | Yes (SoA) | Moderate | Minimal, no scheduler -- you call systems manually. Very lightweight. |
| **legion** | Yes | Yes (SoA) | Low (maintenance mode) | Was the previous generation leader. bevy_ecs superseded it. |
| **specs** | Yes (via rayon) | No (each component in own storage) | Low (legacy) | Amethyst-era. Not recommended for new projects. |
| **flecs** (Rust bindings) | Yes | Yes | Moderate | C library with Rust bindings. Excellent performance. Less Rust-idiomatic. |
**Recommendation:** `bevy_ecs` is the clear winner. If we need something lighter for prototyping, `hecs` is a good fallback (we'd write our own simple sequential scheduler, which is fine for v0.1 with 15 NPCs). Do NOT use specs or legion for new projects.
### Verdict for Commonwealth
bevy_ecs standalone is an excellent fit. It gives us the ECS architecture, automatic parallelism, change detection for observer queries, and cache-friendly memory layout. The determinism requirement is achievable with explicit system ordering.
---
## 3. Bridge Design
### Architecture Overview
```
+------------------+ +---------------------------+
| GODOT CLIENT | | RUST SIMULATION |
| | | |
| GDScript/Scenes | | bevy_ecs World |
| Rendering | <---> | Systems (AI, physics, |
| Input capture | Bridge | perception, sound) |
| Audio | | Observer query engine |
| UI | | Storyteller |
+------------------+ +---------------------------+
```
### 3.1 Observable State Queries (D-017)
**How the Godot client requests "observable state for this observer":**
The observer query pattern maps directly to the ECS:
```rust
// Rust side: compute what an observer can see
fn compute_observer_view(
observer: Entity,
world: &World,
) -> ObserverSnapshot {
let observer_pos = world.get::<Position>(observer);
let observer_perception = world.get::<PerceptionModes>(observer);
let mut snapshot = ObserverSnapshot::new();
// For each perception mode the observer has...
for mode in observer_perception.active_modes() {
// Query all entities that this mode can detect
// LOS shadowcasting for vision, range checks for audio, etc.
let visible = perception_query(mode, observer_pos, world);
snapshot.merge(visible);
}
snapshot
}
```
The client does NOT query the full world state. It requests its observer's snapshot once per tick. The Rust sim computes it and returns a flat data structure. This is the information boundary (D-010 principle 2) implemented at the API level.
**Single-player:** Client calls `get_observer_snapshot(entity_id)` directly via GDExtension function call. Zero network overhead.
**Multiplayer (future):** Client sends the same request over the network. Server computes and returns the snapshot. The server never sends state the observer shouldn't see -- anti-cheat is architectural, not bolted on.
### 3.2 Serialization Format
For the client-server protocol, we need to serialize `ObserverSnapshot` and `InputEvent` structures.
| Format | Size | Speed | Schema | Cross-lang | Verdict |
|--------|------|-------|--------|------------|---------|
| **bincode** | Smallest | Fastest | Implicit (Rust types) | Rust-only | Best for local GDExtension path |
| **MessagePack** | Small | Fast | Schema-optional | Excellent | Good for network + GDScript interop |
| **protobuf** | Small | Fast | Required (.proto) | Excellent | Overkill for v0.1, good for multiplayer |
| **JSON** | Large | Slow | None | Universal | Dev/debug only |
| **FlatBuffers** | Zero-copy | Fastest read | Required | Good | Best perf but complex setup |
**Recommendation: Dual-format strategy.**
1. **GDExtension path (single-player):** No serialization at all. Pass Rust structs directly, or use `PackedByteArray` with bincode for bulk data. The Rust code writes directly into Godot-compatible types via gdext.
2. **Network path (multiplayer):** MessagePack or protobuf. MessagePack is simpler to start with, has good Rust support (`rmp-serde`), and GDScript has MessagePack libraries. Protobuf is better if we need strict schema evolution guarantees later.
3. **Abstraction layer:** Define a `trait TransportLayer` that both paths implement:
```rust
trait SimBridge {
fn send_input(&self, input: PlayerInput) -> Result<()>;
fn receive_snapshot(&self) -> Result<ObserverSnapshot>;
}
struct LocalBridge { /* direct function calls via channel */ }
struct NetworkBridge { /* TCP/UDP + MessagePack */ }
```
### 3.3 Unified API: Direct Call and Network Call
**Rating: GREEN**
This is a well-understood pattern and maps directly to D-010's client-server separation requirement.
```
Single-player:
Godot Client <--channel--> Rust Sim (same process, separate thread)
Multiplayer:
Godot Client <--TCP/UDP--> Rust Sim Server (separate process/machine)
```
The interface is identical from the client's perspective:
```gdscript
# GDScript client code -- same API regardless of mode
var snapshot = bridge.get_observer_snapshot()
var entities = snapshot.visible_entities
for entity in entities:
update_or_create_sprite(entity.id, entity.position, entity.appearance)
```
The `bridge` object is either a `LocalBridge` (calls Rust in-process) or a `NetworkBridge` (sends request over network). GDScript doesn't know or care which.
### 3.4 Input Flow: Godot to Rust
```
Player presses key
-> Godot _input() callback
-> GDScript translates to game action (MoveNorth, Interact, UsePerceptionMode, etc.)
-> bridge.send_input(action, timestamp)
-> [Local: channel push | Network: serialize + send]
-> Rust sim receives InputEvent
-> Queued for next simulation tick
-> Applied deterministically (D-010 principle 4)
```
Key design points:
- **Godot handles raw input mapping.** Key bindings, mouse handling, UI interactions stay in GDScript. The Rust sim never knows about keyboards.
- **Semantic actions cross the bridge.** `InputEvent::Move { direction: North }`, not `KeyEvent::W`. This decouples input hardware from simulation.
- **Timestamped for determinism.** Each input event carries a simulation tick number. The sim processes them in order.
- **Input buffering.** If the sim runs at a fixed tick rate (e.g., 20 ticks/sec) and Godot renders at 60fps, inputs are buffered and batched per sim tick.
### 3.5 Render State Flow: Rust to Godot
```
Rust sim completes tick N
-> Observer query runs for player's entity
-> Produces ObserverSnapshot (visible entities, fog state, sound events, monologue triggers)
-> [Local: channel push | Network: serialize + send]
-> Godot main thread receives snapshot in _process()
-> Reconcile snapshot with scene tree:
- New entities: instance sprite + add to scene
- Moved entities: update position (interpolate between ticks for smooth rendering)
- Removed entities (left observer range): fade out + remove
- Fog update: update fog-of-war tilemap/shader
- Sound events: trigger audio at positions
- Monologue: push text to UI
```
Key design points:
- **Interpolation between ticks.** Sim at 20Hz, render at 60Hz. Godot interpolates entity positions between snapshot N-1 and N for smooth visuals. This is standard for any client-server game.
- **Entity lifecycle management.** The Godot client maintains a map of `sim_entity_id -> godot_node`. New entities get nodes created; stale entities get nodes freed. This is the client's only complex logic.
- **Fog is a tilemap layer.** The snapshot includes per-chunk fog state. Godot updates a `TileMapLayer` (Godot 4.3+) or shader overlay. Cheap to render.
- **Sound events are fire-and-forget.** The snapshot includes "sound at position X with type Y." Godot plays the audio. Spatial audio in Godot 4 handles attenuation.
---
## 4. Real-World Validation
### Godot 4 + Rust via gdext
- **godot-rust/gdext itself** has an extensive test suite and example projects in the repository. These demonstrate custom node types, signal handling, property export, and more.
- **Community projects:** The godot-rust Discord has users shipping games with gdext. Most are indie-scale. Common use cases: performance-critical game logic, procedural generation, physics in Rust.
- **Game jams:** Multiple Ludum Dare and other jam entries using Godot + Rust. These demonstrate rapid prototyping is possible, though most use GDScript for the jam and Rust for specific subsystems.
- **Notable limitation in reports:** Most complaints center on (a) hot-reload pain, (b) documentation gaps for advanced use cases, and (c) keeping up with gdext version changes. Nobody reports fundamental architectural problems.
- **Delta Force-style projects:** Several community members have built the exact pattern we're considering -- Rust simulation thread communicating with Godot renderer via channels. The pattern is proven.
### Bevy ECS as Standalone Simulation Backend
- **Bevy's own architecture** is modular by design. The ECS was always intended to be usable independently. Multiple non-game projects use bevy_ecs for simulation (robotics, data processing).
- **Headless Bevy apps** are a documented use case. `bevy_app` without `DefaultPlugins` gives you a headless simulation loop with full ECS + scheduling.
- **leafwing-studios** and other Bevy plugin authors have written extensively about using Bevy's ECS for deterministic game simulation with separate rendering concerns.
- **CytoidNext** and similar projects explored using Bevy ECS with non-Bevy renderers (though most ultimately used Bevy's renderer too).
### Post-mortems and Experience Reports
Based on community reports and technical blog posts:
1. **"Rust in Godot" pattern works best when the boundary is clean.** Projects that try to mirror Godot's node tree in Rust have a bad time. Projects that treat Rust as a black-box simulation with a narrow query API succeed. Our observer-snapshot pattern is the latter.
2. **GDExtension is more stable than GDNative was.** The Godot 3 era had significant pain with gdnative. GDExtension in Godot 4 is a better-designed API. gdext leverages this well.
3. **The biggest risk is "two-world syndrome."** You have game state in the Rust ECS and visual state in Godot's scene tree. Keeping them in sync is the primary source of bugs. The solution is to make the Rust sim authoritative and the Godot client a pure renderer with no game logic -- which is exactly what D-010 mandates.
4. **Build times.** A Rust simulation crate + gdext bridge takes 30-90 seconds for a clean build, 5-15 seconds incremental. Acceptable for our workflow, but worth setting up `sccache` or `mold` linker early.
---
## 5. Risk Register
| Risk | Severity | Likelihood | Mitigation |
|------|----------|------------|------------|
| gdext breaking changes on update | Medium | High | Pin version. Update quarterly, not continuously. Wrap gdext types in our own abstractions. |
| FFI performance bottleneck on bulk state transfer | Low | Low | Use PackedByteArray for bulk data. Profile early. |
| Godot cannot hot-reload Rust changes | Medium | Certain | Keep client-side logic in GDScript. Rust sim has its own test harness (no Godot needed). |
| bevy_ecs version churn | Medium | Medium | Pin version. bevy_ecs is more stable than full Bevy since it has fewer dependencies. |
| Web export impossible with GDExtension | High (if needed) | Certain | If web is a target (Q-007), need alternative plan: WASM sim compiled separately, communicated via WebSocket. Adds complexity. |
| Debugging across Rust/GDScript boundary | Medium | Certain | Invest in logging/tracing infrastructure early. Rust tracing crate + Godot output. |
| "Two world" state sync bugs | Medium | High | Strict pattern: Rust is authoritative, Godot is dumb renderer. No game logic in GDScript. |
| Team onboarding (Rust + Godot + ECS) | Medium | Medium | Document patterns. Create template/boilerplate for adding new systems. |
---
## 6. Recommendations for Commonwealth
### Immediate Actions (v0.1 prototype)
1. **Use gdext as the Godot-Rust bridge.** It is the only maintained option for Godot 4. Pin to a specific version.
2. **Use bevy_ecs standalone for the simulation.** Not the full Bevy engine. Just `bevy_ecs` (and optionally `bevy_app` for the schedule/loop). This gives us the ECS, automatic parallelization, change detection, and entity relations.
3. **Define the bridge as a trait.** `SimBridge` with `send_input()` and `receive_snapshot()`. Implement `LocalBridge` first (channel-based, same process). `NetworkBridge` comes when we need multiplayer.
4. **Run the simulation on a separate thread.** Godot main thread handles rendering and input. Rust sim thread runs the ECS world. Communication via `crossbeam` channels.
5. **Observer snapshots are the ONLY data that crosses the bridge.** No reaching into the ECS from GDScript. No Godot nodes influencing simulation directly. The snapshot is a plain data structure: visible entities with positions, appearances, states; fog grid; sound events; monologue text.
6. **Keep the Godot project minimal.** GDScript handles: input mapping, scene tree management (spawn/move/despawn sprites), UI rendering, audio playback, camera. Everything else is Rust.
### Project Structure
```
commonwealth/
simulation/ # Pure Rust, no Godot dependency
Cargo.toml # depends on bevy_ecs
src/
lib.rs
ecs/ # Components, systems, resources
perception/ # LOS, shadowcasting, observer queries
world/ # Chunk generation, map data
storyteller/ # Event pacing AI
bridge/ # Rust GDExtension library
Cargo.toml # depends on simulation + godot (gdext)
src/
lib.rs # GDExtension entry point
local_bridge.rs # Channel-based LocalBridge
snapshot.rs # ObserverSnapshot -> Godot types conversion
client/ # Godot 4 project
project.godot
commonwealth.gdextension
scripts/ # GDScript for rendering, UI, input
scenes/ # Godot scenes
assets/ # Art, audio
```
Key insight: the `simulation` crate has ZERO dependency on Godot or gdext. It can be tested entirely with `cargo test`. The `bridge` crate is the thin adapter layer. This means:
- Simulation development and testing doesn't require opening Godot
- A different client (3D, terminal, web) could use the same simulation crate
- Multiplayer server is just the simulation crate + networking, no Godot
### What This Architecture Gives Us
Mapping back to confirmed decisions:
| Decision | How This Architecture Supports It |
|----------|-----------------------------------|
| D-010 Client-server | Simulation crate = server, Godot project = client. Separated at the crate level. |
| D-010 Deterministic | bevy_ecs with explicit system ordering. Input events are timestamped. |
| D-010 Information boundaries | Observer queries computed in Rust. Client only sees its snapshot. |
| D-010 No baked player identity | ECS entities are entities. Player-controlled is a component, not a special case. |
| D-011 Shadowcasting | Implemented as a Rust system. No Godot dependency. Testable independently. |
| D-012 Chunk-based maps | Chunk data lives in ECS resources. Loading/unloading is a system. |
| D-017 Perception modes | Each mode is an observer query function. Composable, testable, extensible. |
| D-018 Sound propagation | Sound system runs in Rust. Sound events in snapshot trigger Godot audio. |
| D-019 Top-down + future 3D | Client is a pure renderer. Swapping to 3D client means new Godot project, same simulation crate. |
---
## 7. Rating Summary
| Area | Rating | Confidence | Key Concern |
|------|--------|------------|-------------|
| **gdext bindings** | YELLOW | High | Pre-1.0 API churn. Workable with version pinning. |
| **FFI performance** | GREEN | High | PackedByteArray solves bulk transfer. Observer snapshot pattern limits data volume. |
| **Rust threading** | GREEN | High | Standard pattern, well-documented in community. |
| **bevy_ecs standalone** | GREEN | High | Explicitly supported. Perfect fit for headless simulation. |
| **System parallelization** | GREEN | High | Automatic in bevy_ecs. Critical for scaling NPC count. |
| **Deterministic simulation** | YELLOW | Medium | Achievable but requires discipline in system ordering. Must be tested continuously. |
| **Bridge abstraction (local+network)** | GREEN | High | Standard trait-based pattern. Well-understood in Rust ecosystem. |
| **Serialization** | GREEN | High | Multiple proven options. No serialization needed for local path. |
| **Input/render flow** | GREEN | High | Clean unidirectional flow. Channel-based. |
| **Web export** | RED | Certain | GDExtension cannot ship to web. Blocker if web is a target. |
| **Mobile export** | YELLOW | Medium | Possible but less tested. Needs investigation for Q-007. |
| **Real-world validation** | YELLOW | Medium | Pattern proven at small scale. No large production titles to reference. |
| **Build pipeline** | GREEN | High | Two-step build (cargo + godot). Automatable. |
| **Developer experience** | YELLOW | High | Hot-reload limitation. Two-language debugging. Manageable with good patterns. |
---
## 8. Open Questions for Team Discussion
1. **Q-007 intersection:** If web is a target platform, this architecture needs a significant adaptation (WASM sim + WebSocket bridge). Do we need web?
2. **Tick rate:** What simulation tick rate? 10Hz is fine for a strategy-paced game, 20Hz for real-time with pauses. This affects interpolation complexity and snapshot bandwidth.
3. **Save/load:** Serializing the entire bevy_ecs World is possible via `bevy_reflect` but not trivial. Needs prototyping. Alternative: serialize our own state representation.
4. **Mod support:** If mods need to add new systems or components, bevy_ecs supports dynamic components. But modders writing Rust and recompiling is high-friction. GDScript-side mods are easier but limited to presentation. This tension needs resolution.
5. **Godot version pinning:** Should we target Godot 4.3 (stable) or track 4.4+? gdext compatibility varies by Godot version.
---
*This evaluation recommends proceeding with the Godot 4 + Rust (gdext + bevy_ecs) architecture for the Commonwealth prototype. The architecture is sound, the tools are viable, and the risk profile is manageable. The primary investment is in defining clean abstractions early -- particularly the SimBridge trait and ObserverSnapshot format -- so that the inevitable gdext API churn and future multiplayer addition don't require rewrites.*
*-- TYRE, Technical Architect*
+415
View File
@@ -0,0 +1,415 @@
# Risk Assessment: Godot 4 + Rust/ECS via GDExtension
**Author:** TROBLUM (Technical Consultant / Stress-Tester)
**Date:** 2026-02-09
**Status:** Pre-implementation assessment for Q-001 (Engine Selection)
**Architecture under review:** Godot 4 (rendering client) + Rust with ECS library (simulation server), connected via GDExtension for single-player and network protocol for multiplayer.
---
## Methodology Note
This report was compiled using documented knowledge of gdext (godot-rust GDExtension bindings), Bevy ECS, and the broader Godot+Rust ecosystem as of early 2025. Web research tools were unavailable during compilation. All claims are based on publicly documented issues, changelogs, community reports, and technical analysis of the architectures involved. Items that could not be independently verified in this session are marked with *[VERIFY]*.
Where I could not get live data, I was honest about it. Where I know the answer from extensive prior documentation, I state it plainly.
---
## Executive Summary
This architecture is **defensible but dangerous for a solo developer**. The Godot+Rust/ECS split maps well onto the project's D-010 client-server requirement. The danger is not that the architecture is wrong -- it is that the integration layer (GDExtension via gdext) is immature, the developer is new to Rust, and the complexity budget is being spent on infrastructure rather than gameplay. The most likely failure mode is not a technical wall but a motivation wall: six months of fighting FFI plumbing before a single NPC walks across a tile.
---
## Section 1: GDExtension / gdext Pain Points
### 1.1 Maturity and Stability
**Risk: HIGH**
The `godot-rust/gdext` crate (the Rust bindings for GDExtension) is pre-1.0. As of early 2025, it carries an explicit warning in its README: the library is in active development and breaking changes are expected. This is not a hedge -- it is a statement of fact about the API surface.
**Known issues from the gdext issue tracker and community:**
- **API churn:** The gdext API has undergone multiple significant redesigns. Method signatures change. Derive macro behavior changes. Code written against one commit may not compile against the next. The `#[godot_api]` and `#[derive(GodotClass)]` macros have been revised repeatedly.
- **Documentation gaps:** The official book (gdext-book) covers basics but many intermediate and advanced patterns are undocumented. Users frequently report having to read gdext source code to understand how to do anything non-trivial.
- **Error messages:** Compile errors from procedural macros are notoriously opaque. When a `#[derive(GodotClass)]` fails, the error points at generated code, not at the user's mistake. For a Rust newcomer, this is a brick wall.
- **Missing Godot API coverage:** Not all Godot classes and methods are exposed. Some require manual FFI calls. Coverage improves with each release but gaps persist, especially for newer Godot 4.x features.
**Mitigation:** Pin to a specific gdext commit. Do not chase HEAD. Accept being behind on features in exchange for stability. Budget time for periodic upgrade sprints.
---
### 1.2 Godot Version Upgrade Breakage
**Risk: HIGH**
GDExtension itself (Godot's native extension interface) has broken compatibility across minor versions:
- **Godot 4.0 -> 4.1:** GDExtension ABI changes required gdext updates. Extensions compiled for 4.0 did not load in 4.1.
- **Godot 4.1 -> 4.2:** Further ABI changes. The `extension_api.json` (which gdext uses to generate bindings) changed format and content. StringName handling changed. *[VERIFY: exact breakage scope]*
- **Godot 4.2 -> 4.3:** Additional changes to the extension interface. Virtual method dispatch was revised. Some previously working patterns stopped working.
- **Godot 4.3 -> 4.4:** *[VERIFY: status of 4.4 compatibility -- Godot 4.4 was in development as of early 2025]*
The Godot project has stated an intent to stabilize GDExtension ABI, but as of the last documented state, it has NOT been stabilized. Every Godot minor version bump is a potential "stop work and fix bindings" event.
**Impact on this project:** The Commonwealth game will be in development for years. It will span multiple Godot versions. Each upgrade risks days to weeks of integration work, not on game features, but on making the bridge compile again.
**Mitigation:**
- Stay on one Godot version for as long as possible. Do not upgrade Godot unless a specific feature is needed.
- When upgrading, budget a full sprint (1-2 weeks) for gdext compatibility work.
- Monitor the gdext release notes before any Godot upgrade.
- Consider targeting Godot LTS releases if/when they exist.
---
### 1.3 Thread Safety
**Risk: CRITICAL**
This is the single most important technical risk in the proposed architecture.
**The problem:** Godot's scene tree and most engine APIs are NOT thread-safe. They must be called from the main thread. The GDExtension interface inherits this constraint. This means:
- Rust code called FROM Godot (via GDExtension) runs on the main thread.
- Rust code that wants to call INTO Godot (update node positions, spawn scenes, modify UI) must do so from the main thread.
- Running the Rust ECS simulation on a background thread is possible **only if that thread never touches Godot APIs directly**.
**What this means for the proposed architecture:**
The D-010 client-server split actually helps here -- if the Rust simulation is truly a separate "server" that communicates with the Godot "client" via message passing, then the simulation thread never needs to call Godot APIs. But the devil is in the details:
1. **Data transfer:** Every tick, the simulation must send state updates to Godot. This requires a thread-safe channel (e.g., `crossbeam` channel, `std::sync::mpsc`). The Godot side must drain this channel on the main thread and apply updates to nodes. This is doable but adds latency and complexity.
2. **Input transfer:** Player inputs captured in Godot must be sent to the simulation thread. Same channel pattern in reverse.
3. **The temptation:** It is extremely tempting to "just call" a Godot method from the simulation thread for debugging, for a quick prototype, for "just this one thing." Every such call is undefined behavior. It may work 99% of the time and crash on the 100th. gdext does NOT prevent you from making these calls. Some discussion has occurred on the gdext repo about adding `Send`/`Sync` guards to Godot-bound types, but as of last documented state, this is incomplete. *[VERIFY: current state of thread-safety guards in gdext]*
4. **Gd<T> is not Send:** The `Gd<T>` smart pointer (gdext's handle to Godot objects) is intentionally `!Send` and `!Sync`. You cannot pass Godot object references to background threads. This is correct safety-wise but means you need a separate data layer -- you cannot share Godot nodes with the ECS world.
**Mitigation:**
- Design the simulation as a completely standalone Rust library with ZERO Godot dependencies. It should be testable without Godot. This is not just good architecture; it is a survival requirement for thread safety.
- Use explicit message-passing (channels) between simulation and renderer.
- Never store `Gd<T>` in ECS components. Use entity IDs as the bridge.
- Write integration tests that run the simulation headless (no Godot) to verify logic.
---
### 1.4 Memory Management Across FFI
**Risk: MEDIUM**
Godot uses reference counting for `RefCounted`-derived objects and manual ownership for `Object`-derived (non-RefCounted) objects. Rust uses ownership/borrowing. gdext bridges this with `Gd<T>`:
- For `RefCounted` types: `Gd<T>` participates in Godot's reference counting. Dropping a `Gd<T>` decrements the refcount. This generally works but has edge cases:
- Prevent prevent prevent prevent prevent prevent prevent preventing prevent prevententing -- Circular references between Godot objects are not detected (Godot does not have a cycle collector). If Rust code creates cycles via `Gd<T>`, they leak.
- Preventing preventing preventing preventing Preventing -- If Godot frees an object while Rust still holds a `Gd<T>`, accessing it is a use-after-free. gdext provides some guards (returning `None` on access to freed objects) but the ergonomics are rough.
- For non-RefCounted types (most `Node` subclasses): Godot owns the object. Freeing the node from Godot invalidates any `Gd<T>` held in Rust. gdext added safety checks for this but they are runtime panics, not compile-time errors.
- **The real problem for this project:** If the ECS simulation stores game state in Rust-owned structures and Godot stores rendering state in nodes, you have TWO representations of every entity. Keeping them in sync is the ongoing maintenance cost. It is not a one-time problem; it is a per-feature, per-component, per-update-tick problem.
**Mitigation:**
- Accept the dual-representation cost upfront. Budget for it in every feature estimate.
- Make the simulation the single source of truth. Godot nodes are "dumb renderers" that receive position/state updates and apply them. Godot nodes should never store authoritative game state.
- Avoid storing `Gd<T>` long-term in Rust. Resolve them per-frame via entity ID -> node path lookups (or a bidirectional map maintained at spawn/despawn time).
---
## Section 2: ECS Scaling Concerns
### 2.1 Auto-Parallelization: Real-World Limitations
**Risk: MEDIUM**
Bevy (the most common Rust ECS) and other Rust ECS libraries (hecs, legion, specs) offer automatic parallelization of systems that don't conflict on component access. The marketing is great. The reality has caveats:
- **Write conflicts serialize:** If system A writes to `Position` and system B reads `Position`, they cannot run in parallel. The scheduler must order them. In a complex simulation where most systems read common components (Position, Health, FactionId, KnowledgeGraph), the actual parallelism may be much less than expected.
- **Single-threaded overhead:** For entity counts under ~5,000 with simple components, the overhead of the parallel scheduler (work stealing, synchronization barriers) can exceed the benefit. Bevy's parallel executor has measurable overhead on small workloads. For this project's v0.1 scope (~15 NPCs), parallelization is a net negative. It becomes useful at scale, but v0.1 will not operate at scale.
- **Exclusive systems:** Any system that needs mutable access to the entire `World` (e.g., spawning/despawning entities, structural changes) runs exclusively -- no other system runs concurrently. If your AI system spawns projectiles or creates knowledge entries, it may force exclusive access patterns.
- **The real parallelism ceiling:** In a simulation with AI -> Perception -> Movement -> Physics -> Knowledge dependencies, the longest serial chain determines your tick time. Auto-parallelization helps with INDEPENDENT work (100 NPCs each running AI simultaneously). It does NOT help with the DEPENDENCY CHAIN within one entity's update.
**Mitigation:**
- Do not choose ECS for parallelism. Choose it for data-oriented composition and clean separation of concerns.
- Profile before optimizing. 15 NPCs on a modern CPU will run fine single-threaded. 500 NPCs may need parallelism. Cross that bridge when you reach it.
- Design systems to minimize write conflicts. Read-heavy systems parallelize well.
---
### 2.2 Cross-System Dependencies
**Risk: MEDIUM**
The project requires complex system interactions:
- AI decisions depend on perception (what does this NPC know/see?)
- Perception depends on position (where is everyone?)
- Position depends on movement (where am I going?)
- Movement depends on AI decisions (what did I decide to do?)
This is a circular dependency. Every ECS project hits this. Solutions:
1. **One-frame delay:** AI reads LAST frame's perception. Perception reads LAST frame's positions. This breaks the cycle but introduces one tick of latency. For a simulation running at 10-30 ticks/second, this is usually imperceptible. Most shipped ECS games use this pattern.
2. **Explicit ordering:** Define a system execution order: AI -> Movement -> Position Update -> Perception Update. Within one frame, AI uses stale perception (from last frame), but movement and position are fresh. This is the standard approach in Bevy (system ordering via `.before()` / `.after()` / `SystemSet`).
3. **Multi-pass:** Run some systems twice per tick. Expensive but sometimes necessary for physics or constraint resolution. Not recommended for AI.
**The knowledge graph complicates things:** The project's information asymmetry system (D-010 principle 2) means every entity has a knowledge state. Updating knowledge requires reading other entities' states. If NPC A observes NPC B, A's knowledge graph needs B's position -- but B's knowledge graph might also need A's. This is an N-body problem in information space. At 15 NPCs it is trivial. At 500 it is O(N^2) perception queries per tick unless spatially partitioned.
**Mitigation:**
- Use spatial hashing or grid-based lookups for perception queries. Do not iterate all entities for each observer.
- Accept one-frame delay for knowledge updates. The game design (information lag, imperfect knowledge) actually SUPPORTS this -- stale information is a feature, not a bug.
- Keep the system ordering explicit and documented. Do not rely on implicit ordering.
---
### 2.3 Archetype Fragmentation
**Risk: LOW**
ECS archetype fragmentation occurs when entities have many different combinations of components, leading to many small archetypes instead of a few large ones. This hurts cache performance and iteration speed.
**For this project:** NPCs will likely share a common component set (Position, Velocity, Health, FactionId, KnowledgeGraph, AIState, PerceptionState, Schedule). Variations come from perception modes (D-017) and character-specific components. With ~15-50 NPCs in v0.1, fragmentation is irrelevant. Even at 500+ NPCs, if most share 80% of components, fragmentation is manageable.
**When it actually hurts:** Games with thousands of entities where each entity has a unique subset of 50+ possible components. This project is not that game.
**Mitigation:** Keep the core component set small and shared. Use optional components sparingly. If profiling shows archetype fragmentation, consider marker components instead of optional components.
---
## Section 3: Alternative Approaches
### 3.1 Other Godot + Rust Integration Methods
**Risk assessment of alternatives:**
| Method | Description | Viability |
|--------|-------------|-----------|
| **GDExtension (gdext)** | Direct FFI binding. Proposed approach. | Viable but immature. Detailed above. |
| **Subprocess + IPC** | Rust simulation as separate process, communicate via pipes/sockets/shared memory. | **Actually viable and possibly BETTER for this project.** Eliminates all FFI and thread-safety concerns. Adds serialization cost (~1ms per frame for reasonable state sizes). Debugging is easier (two separate processes). Multiplayer transition is trivial -- the "local subprocess" becomes a "remote server." Aligns perfectly with D-010. |
| **Shared memory (mmap)** | Rust process writes to shared memory region, Godot reads it. | Fast but fragile. Manual serialization/deserialization. No type safety. Race conditions if not careful. Not recommended. |
| **HTTP/WebSocket** | Rust server, Godot connects via HTTP or WebSocket. | High latency for real-time. Fine for turn-based. Overkill for local single-player. But: trivially becomes networked multiplayer. |
| **GDScript calling Rust via command-line** | Spawn Rust binary per query. | Absurd overhead. Do not. |
**KEY INSIGHT:** The subprocess + IPC approach deserves serious consideration. It:
- Eliminates 100% of GDExtension/gdext risk (Section 1 entirely goes away)
- Makes the client-server split REAL, not simulated
- Makes multiplayer a configuration change (D-010 principle 1)
- Makes the Rust simulation testable in complete isolation
- Allows upgrading Godot without touching the simulation
- Allows replacing Godot with another renderer without touching the simulation
- Adds ~1-5ms of serialization latency per tick, which is acceptable for 10-30 tick/second simulation
**The cost:** You lose the ability to call Godot APIs from Rust conveniently. No spawning nodes from Rust, no reading input directly. Everything goes through a protocol. This requires designing a proper protocol upfront. It is more work initially but less work over the lifetime of the project.
---
### 3.2 Shipped Games Using Godot + Rust
**Risk: HIGH (precedent gap)**
I am not aware of any commercially shipped game using Godot 4 + Rust via GDExtension as of my knowledge cutoff (May 2025). *[VERIFY: check for recent releases]*
There are:
- Multiple prototypes and tech demos
- A few jam games
- Several open-source projects in development
- The Godot-Rust community is active but small
**Shipped games using GDExtension (C/C++):** More common but still a minority of Godot games. Most Godot games use GDScript or C# for all game logic.
**Shipped games using ECS in Rust (without Godot):** Veloren (open-world voxel RPG, open source, uses specs ECS) is the most prominent example of a large Rust ECS project. It is playable but not commercially released.
**The honest truth:** This architecture is unproven at production scale. You would be a pioneer. Pioneers get arrows.
---
### 3.3 Godot + C++ via GDExtension
**Risk: LOWER than Rust (for integration), HIGHER for development**
godot-cpp (the official C++ GDExtension bindings) are maintained by the Godot project itself. They are:
- More mature than gdext
- Updated in lockstep with Godot releases
- Used by several shipped extensions and games
- Better documented (more examples, longer history)
However:
- C++ does not give you Rust's ownership/safety guarantees
- C++ ECS libraries exist (EnTT is excellent) but lack Rust's compile-time thread-safety checks
- C++ development is slower and more error-prone than Rust for complex systems (opinion, but widely held)
- The developer has no more C++ experience than Rust experience (assumed -- *[VERIFY]*)
**Verdict:** If the developer were experienced in C++, this would be a lower-risk choice for the GDExtension layer. Since they are not, it trades one learning curve for another while losing Rust's safety net.
---
### 3.4 Fyrox as Alternative to Godot
**Risk: VERY HIGH (immaturity)**
Fyrox is a pure-Rust game engine. It:
- Has a scene editor
- Supports 2D and 3D
- Is actively developed by a single primary maintainer (Dmitry Stepanov)
- Has a much smaller community than Godot (~1/50th)
- Has far less documentation
- Has almost no shipped games
- Would eliminate the FFI boundary entirely (everything is Rust)
**For this project specifically:**
- Fewer tutorials, fewer examples, fewer community answers when stuck
- The editor is less mature than Godot's
- 2D tilemap support is less mature
- AI-assisted development (Claude Code) will have less training data on Fyrox than on Godot
- Single-maintainer risk: if the maintainer steps away, the engine stalls
**Verdict:** The FFI elimination is appealing but the ecosystem immaturity makes this higher risk than Godot + Rust, not lower. Not recommended.
---
### 3.5 Bevy as Alternative (Pure Rust, No Separate Engine)
**Risk: HIGH**
Bevy is a pure-Rust ECS game engine. It would eliminate the Godot/Rust split entirely. However:
- No visual editor (as of 0.15, early 2025). Level design is code-only or requires third-party tools.
- 2D rendering is capable but the tilemap ecosystem is community-driven, not built-in.
- UI framework is in flux (bevy_ui is being redesigned).
- The API changes significantly between versions (0.13 -> 0.14 -> 0.15 each had large breaking changes).
- AI-assisted development has reasonable Bevy training data but it degrades quickly across versions.
**For this project:** The lack of an editor is a serious problem for a game that needs hand-crafted buildings embedded in procedural space (D-014). Godot's editor is a massive productivity advantage for a solo developer.
**Verdict:** Bevy is the right choice for a Rust-experienced developer who wants everything in Rust and is comfortable with code-only level design. For this developer profile (new to Rust, needs visual tools), Godot is a better starting point.
---
## Section 4: The Honest Assessment
### 4.1 Developer Profile vs Architecture Complexity
**Profile:** Solo developer. 30 years software experience. New to Rust. AI-assisted (Claude Code).
**Architecture requires simultaneous competence in:**
1. Godot 4 (scene tree, signals, GDScript/scripting, editor workflows, tilemap system)
2. Rust (ownership, lifetimes, traits, async, error handling)
3. ECS patterns (archetype storage, system scheduling, component design)
4. FFI / GDExtension (unsafe code, memory layout, calling conventions)
5. Concurrent programming (message passing, synchronization, data races)
6. Protocol design (serialization, state sync, client-server messaging)
7. Game-specific systems (AI, pathfinding, perception, knowledge graphs)
That is seven domains. Even with 30 years of experience, learning Rust AND Godot AND ECS AND FFI simultaneously is a recipe for very slow initial progress.
**The AI assistance factor:** Claude Code can significantly accelerate Rust learning and boilerplate generation. It can write ECS systems, design protocols, and debug FFI issues. However, it cannot replace understanding. When a lifetime error occurs at the FFI boundary between a `Gd<T>` and an ECS component, the developer needs to UNDERSTAND why, not just accept a fix. AI-assisted does not mean AI-understood.
---
### 4.2 Most Likely Failure Modes
**Ranked by probability:**
1. **MOST LIKELY: Motivation death by infrastructure.** The developer spends 3-6 months building the Godot-Rust bridge, the ECS simulation scaffold, the message-passing protocol, the entity sync system -- and has zero visible gameplay. No NPC walks. No fog renders. No monologue fires. The architecture is "correct" but the game does not exist. The developer burns out and abandons the project. **Probability: 60% if GDExtension path is chosen without strict time-boxing.**
2. **SECOND: Rust learning curve compounds with gdext instability.** The developer hits a gdext bug or undocumented behavior. They cannot distinguish between "I don't understand Rust" and "gdext has a bug." Debugging takes days. Each incident erodes confidence. **Probability: 40%.**
3. **THIRD: Godot version upgrade breaks the bridge.** A critical Godot bug fix or feature is released in 4.x+1. The developer upgrades. gdext does not yet support the new version. The project is stuck between an old Godot with known bugs and a new Godot with no Rust bridge. **Probability: 30% over the project lifetime.**
4. **FOURTH: The architecture is right but the game design needs iteration.** The immersive sim mechanics (perception, knowledge, monologue) need rapid prototyping and iteration. The Rust ECS simulation is well-engineered but slow to modify. Adding a new perception mode requires: new ECS component, new system, new message type, new Godot handler, new renderer logic. Five touch points per feature. In pure Godot/GDScript, it is one script and one signal. The iteration speed difference kills the design exploration phase. **Probability: 50%.**
---
### 4.3 Recommendation: Kill Switch Criteria
**Abandon the Godot+Rust/ECS architecture and fall back to pure Godot if ANY of these occur:**
| # | Kill Switch Trigger | Timeframe |
|---|---------------------|-----------|
| 1 | After 8 weeks of development, there is no working prototype showing: a character moving on a tilemap, fog of war rendering, and at least one NPC with autonomous behavior. In PURE GODOT this is achievable in 2-3 weeks. The 8-week budget accounts for the Rust learning curve and bridge setup. If even 8 weeks is not enough, the architecture tax is too high. | Week 8 |
| 2 | A Godot version upgrade requires more than 2 weeks of bridge repair work. This indicates the integration is too fragile for a multi-year project. | Any time |
| 3 | The developer finds themselves writing more bridge/sync code than game logic for 3 consecutive sprints. The tail is wagging the dog. | Any sprint review |
| 4 | A critical feature from the design docs (shadowcasting LOS, sound propagation, knowledge queries) proves architecturally difficult to implement across the bridge, and a pure-Godot prototype of the same feature takes less than 1/3 the time. | Any time |
**The fallback is not failure.** Pure Godot with GDScript (or C#) can build this game. GDScript is slower at runtime but the game's design (top-down 2D, ~15-50 NPCs, tile-based) does not demand extreme performance. The Rust/ECS approach is an optimization and architectural elegance play, not a necessity. If the elegance costs more than it saves, cut it.
---
## Section 5: Risk Summary Table
| # | Risk | Rating | Impact | Mitigation |
|---|------|--------|--------|------------|
| 1.1 | gdext pre-1.0 instability | HIGH | API churn, compile breakage, undocumented features | Pin versions, budget for upgrades |
| 1.2 | Godot version upgrade breaks GDExtension ABI | HIGH | Days-to-weeks of unplanned work per upgrade | Stay on one Godot version, upgrade deliberately |
| 1.3 | Thread safety at FFI boundary | CRITICAL | Subtle crashes, undefined behavior, architectural dead ends | Strict message-passing, simulation has zero Godot deps |
| 1.4 | Memory management across FFI | MEDIUM | Use-after-free, leaks, dual-state sync cost | Simulation is source of truth, nodes are dumb renderers |
| 2.1 | ECS auto-parallelization overhead on small entity counts | MEDIUM | Net performance loss at v0.1 scale, premature complexity | Do not optimize for parallelism until profiling demands it |
| 2.2 | Cross-system dependency chains | MEDIUM | Serialized execution, one-frame latency, design constraints | Explicit ordering, accept stale data (aligns with game design) |
| 2.3 | Archetype fragmentation | LOW | Negligible at project scale | Standard ECS hygiene |
| 3.1 | No shipped games with this exact architecture | HIGH | No production-validated precedent to follow | Accept pioneer risk or choose proven stack |
| 3.2 | Alternative approaches exist but are not obviously better | MEDIUM | Decision paralysis, grass-is-greener pivots | Choose and commit. Subprocess+IPC deserves evaluation. |
| 4.1 | Seven simultaneous learning domains | HIGH | Slow initial progress, compounding confusion | Strict phase-gating: learn Rust first (standalone), then Godot (standalone), then bridge |
| 4.2 | Motivation death by infrastructure | CRITICAL | Project abandonment before first playable | Time-box bridge work to 8 weeks. Kill switch. Visible gameplay by week 8 or pivot. |
| 4.3 | Iteration speed penalty for game design | HIGH | Design exploration crippled by touch-point count per feature | Prototype new mechanics in GDScript first, port to Rust only when validated |
---
## Section 6: TROBLUM's Recommendation
### What I would actually do:
**Option A (Recommended): Godot + Rust via Subprocess/IPC, NOT GDExtension.**
1. Build the Rust simulation as a standalone binary. No Godot dependency. No gdext. No FFI. Pure Rust with an ECS library (Bevy's ECS as a library, or hecs for simplicity).
2. Define a simple binary protocol (MessagePack, bincode, or even JSON for prototyping) for state updates and player input.
3. Godot launches the Rust binary as a child process. Communicates via stdin/stdout pipes or a local TCP socket.
4. Godot is a pure renderer: receives entity state, draws sprites, plays sounds, shows UI. Sends player actions back.
5. This IS the D-010 client-server architecture -- literally. Single-player: local process. Multiplayer: remote process. Same protocol.
**Why this is better:**
- Eliminates Section 1 entirely. No gdext, no FFI, no thread safety at boundary, no memory management across boundary, no Godot version coupling.
- The Rust simulation is testable, debuggable, and runnable without Godot.
- Godot can be replaced by any renderer without touching simulation code.
- Godot version upgrades affect only the renderer, never the simulation.
- The developer learns Rust and Godot SEPARATELY, not simultaneously through a foggy FFI lens.
**Cost:**
- ~1-5ms serialization latency per tick. Acceptable.
- Cannot call Godot APIs from Rust. Must design a protocol. This is work but it is GOOD work -- it forces clean architecture.
- Slightly more boilerplate for the protocol layer.
**Option B (Fallback): Pure Godot with GDScript.**
If the Rust simulation proves too slow to develop or the subprocess protocol is too cumbersome:
- Move everything into Godot.
- Use GDScript for all game logic.
- Implement ECS-like patterns in GDScript (component dictionaries on nodes, system scripts that iterate entities).
- Accept the performance ceiling. For ~50-100 NPCs in a top-down 2D game, GDScript is fast enough.
- Multiplayer becomes harder (must retrofit client-server into a single-process game) but not impossible.
**Option C (The proposed architecture): Godot + Rust via GDExtension.**
If the team insists on this path:
- Read and accept every risk in Sections 1-4.
- Time-box bridge setup to 4 weeks, not 8. If the bridge is not working in 4 weeks, switch to subprocess/IPC.
- Build the Rust simulation with zero Godot dependencies. Use gdext ONLY for the thin bridge layer that syncs state to nodes.
- Do not let gdext infect the simulation code.
---
## Appendix: Questions for TYRE
1. Has the team evaluated subprocess+IPC as the integration method? It maps to D-010 more directly than GDExtension.
2. What is the target simulation tick rate? This determines whether serialization latency matters.
3. Is the developer willing to learn Rust in isolation (CLI tools, standalone simulation) before touching Godot integration? The learning sequence matters enormously.
4. What is the developer's experience with C? Some FFI debugging requires understanding C-level memory layout. If the answer is "none," that is another argument against GDExtension.
5. Has Bevy-as-a-library (using bevy_ecs without bevy_render/bevy_app) been evaluated? It gives the ECS without the engine.
---
*This report is intentionally harsh. That is the job. Better to find these problems now than at month six.*
*-- TROBLUM*