Skip to content

ENG2-P8-05: Engine event bus with FFI subscription (#547) #803

Description

@aram-devdocs

Parent

  • Program: ENG2 — GoudEngine v2 Rebuild (see the pinned master tracking issue)
  • Phase / Milestone: Phase 8 — Capability Gaps (eng2-p8-capabilities)
  • Batch / Group: Batch 8.2 — Gameplay services, Group A
  • Runbook spec: docs/src/runbook/phases/phase-8.md (committed with the roadmap)

Summary

Add a generic, type-erased engine event bus that SDK code can subscribe to for custom (game-defined) event types, with FFI publish/subscribe exports, unifying the pattern that today is reinvented per subsystem. Closes #547.

Architecture Context

Layer: Layer 3 (Services, ecs/ — the bus lives alongside other ECS resources) building on the existing Layer 1 core/event/ primitives; Layer 5 (FFI) for the subscription surface.
Modules/types touched:

  • goud_engine/src/core/event/ — reuse EventQueue, EventReader, EventWriter, Events<E> (queue.rs, reader.rs, writer.rs, resource.rs) as the underlying double-buffered storage; do not reimplement double-buffering.
  • goud_engine/src/ffi/event_bus.rs (new) — a type-erased publish/subscribe surface: register a named/typed channel, publish raw bytes, drain/poll subscribed events.
  • goud_engine/src/ecs/ — bus registration as a world resource so systems and FFI share the same event storage.

Boundary constraints (only those that apply):

  • FFI exports: #[no_mangle] extern "C", #[repr(C)], null checks, // SAFETY: comments.
  • Events cross the FFI boundary as raw byte payloads plus a type/tag identifier (mirroring how component_ops already crosses typed Rust data across FFI as bytes + type_id_hash — see .agents/rules/component-ops.md); it does not know concrete SDK-side types at compile time.

Pattern to follow: goud_engine/src/core/event/mod.rs design (Event/EventQueue<E>/EventReader<E>/EventWriter<E>/Events<E>) is the correct underlying primitive — reuse it. Do NOT extend the bespoke per-subsystem patterns already in the tree (ffi/ui/events.rs's callback_registry keyed by usize, or ffi/physics/physics2d_events.rs's CollisionCallback + deterministic pull API) — this issue replaces the need for new instances of that pattern going forward, though it does not need to migrate the existing three call sites.

Scope

  • Type-erased publish/subscribe core: game code (via FFI) registers a named event channel (or type-id hash, mirroring component_ops's type_id_hash convention), publishes raw byte payloads, and other code drains them per-frame.
  • FFI exports: goud_event_bus_publish(context, channel_id, data_ptr, len), goud_event_bus_drain(context, channel_id) -> count + accessor, or a callback-registration variant — decide one consistent model (poll-based, matching Events<E>'s existing double-buffer semantics, is preferred over callbacks per the codebase's general avoidance of callback re-entrancy into engine state).
  • Wire the bus as a per-GoudContext resource so multiple contexts don't cross-publish events.
  • Tests: spec test + unit tests for publish/drain ordering, double-buffer semantics (published this frame, readable next frame per existing Events<E> contract), and multi-subscriber fan-out.
  • Docs/rules updates: mdBook page + a note in .agents/rules/ecs-patterns.md establishing the bus as the sanctioned path for new cross-boundary event needs, so future subsystems don't add a fourth bespoke callback pattern.

Acceptance Criteria

  • A custom event type published from one SDK call is observable via drain/poll from another FFI call within the same frame-boundary contract as Events<E> (published now, readable starting next read cycle).
  • FFI event-bus exports have wrappers in all 10 SDKs and pass python codegen/validate_coverage.py.
  • cargo check && cargo fmt --all -- --check && cargo clippy -- -D warnings clean; cargo test green; ./codegen.sh && git diff --exit-code (drift gate)

Breaking Change & Throne Follow-up

None — additive/internal (new FFI exports only; existing per-subsystem event bridges in ffi/ui/events.rs, ffi/animation/events.rs, ffi/physics/physics2d_events.rs are untouched).

Blocked By

None.

Files Likely Touched

  • New: goud_engine/src/ffi/event_bus.rs, possibly goud_engine/src/ecs/resource/event_bus.rs
  • Generated: SDK bindings under sdks/*/ for the new event-bus FFI exports
  • Modified: goud_engine/src/ffi/mod.rs (register module), codegen/goud_sdk.schema.json

Agent Notes

  • The engine already has a real, generic, double-buffered typed event system at goud_engine/src/core/event/ (queue.rs, reader.rs, writer.rs, resource.rs, plus traits.rs for the Event marker trait) — any Send + Sync + 'static type auto-implements Event per the doc comment in core/event/mod.rs. core/events/ (plural, separate directory: window.rs, frame.rs, app.rs) uses this to define built-in engine lifecycle events (AppStarted, WindowResized, FrameStarted, etc.), entirely Rust-side.
  • None of this generic machinery is reachable from FFI. Instead, three independent, mutually-inconsistent bridging patterns already exist in the tree, confirming the "reinvented per subsystem" framing: ffi/animation/events.rs polls crate::core::event::Events<AnimationEventFired> directly with a payload-type discriminant (PAYLOAD_NONE/PAYLOAD_INT/PAYLOAD_FLOAT constants at the top of the file); ffi/ui/events.rs instead uses a hand-rolled callback_registry() (Mutex<HashMap<usize, CallbackRegistration>>) with function-pointer callbacks; ffi/physics/physics2d_events.rs uses yet a third shape — a CollisionCallback plus a separate deterministic "capture during step, pull after" API (capture_step_collision_events, collision_event_at, collision_event_count). No generic named/typed channel exists that arbitrary SDK-side game code can publish or subscribe to for its own custom event types.
  • This closes Add typed event bus/queue system #547 ("Add typed event bus/queue system"), which is currently open.
  • Decide the publish/subscribe transport shape carefully: the codebase already leans toward poll/drain over callbacks for anything touching engine state re-entrancy (physics uses "capture then pull", not a live callback into the physics step) — prefer a poll-based FFI model here too rather than the ffi/ui/events.rs callback style, to avoid re-entrancy into the same per-context lock a publish call might be holding.

Verification

cargo check && cargo fmt --all -- --check && cargo clippy -- -D warnings
cargo test
./codegen.sh && git diff --exit-code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions