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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions