Skip to content

ENG2-P4-02: Command-buffer FFI core: packed opcode buffer, engine drains under one lock per frame #781

Description

@aram-devdocs

Parent

  • Program: ENG2 — GoudEngine v2 Rebuild (see the pinned master tracking issue)
  • Phase / Milestone: Phase 4 — FFI v2 Convergence (eng2-p4-ffi-v2)
  • Batch / Group: Batch 4.1 — Core mechanisms, Grp A (parallel with Grp B / ENG2-P4-03)
  • Runbook spec: docs/src/runbook/phases/phase-4.md (committed with the roadmap)

Summary

Implement the command-buffer FFI core designed in RFC-0007 (ENG2-P4-01): a packed opcode buffer submitted from the SDK side and drained by the engine under a single lock acquisition per frame, targeting 10k-50k ops/frame at O(1) FFI crossings.

Architecture Context

Layer: FFI (Layer 5) — goud_engine/src/ffi/ (see tools/lint_layers.rs — downward-only deps).
Modules/types touched:

  • goud_engine/src/ffi/ — new command-buffer submission/drain module (opcode encode/decode, per-frame drain entry point).
  • goud_engine/src/component_ops/batch_ops.rs — the batch op internals this generalizes; command-buffer ops for component write/read-request route through the existing _impl functions (per .agents/rules/component-ops.md: logic lives once in _impl, both FFI and command-buffer paths call it).
  • goud_engine/src/context_registry/ — the drain must take the per-context registry/storage locks exactly once per frame, not once per op.

Boundary constraints (only those that apply):

  • FFI exports: #[no_mangle] extern "C", #[repr(C)], null checks, // SAFETY: comments on every unsafe block touching the opcode buffer.
  • Per .agents/rules/component-ops.md: storage state lives behind CONTEXT_COMPONENT_STORAGE/COMPONENT_TYPE_REGISTRY Mutexes — take the lock, operate, drop it; do not leak a MutexGuard across the drain call.

Pattern to follow: goud_component_add_batch (goud_engine/src/ffi/component/batch.rs:72) — one lock acquisition, then an internal loop over the entity slice (:145-172 region). The command buffer generalizes this to a heterogeneous opcode stream instead of one homogeneous batch.

Scope

  • Implement packed opcode buffer format per RFC-0007 (ENG2-P4-01): component write/read-request, transform set, spawn/despawn, draw-submit opcodes.
  • Implement the engine-side drain entry point: one lock acquisition per frame, decode + dispatch all queued ops to existing _impl functions.
  • 0-alloc decode path (per RFC-0007 requirement) — no per-op heap allocation while decoding the opcode stream.
  • Tests: spec test at goud_engine/tests/spec/ covering encode/decode round-trip and the single-lock-per-frame invariant; unit tests for each opcode kind.
  • Docs/rules updates: .agents/rules/ffi-patterns.md gets the command-buffer pattern documented alongside the existing per-function FFI convention.

Acceptance Criteria

  • A stream of 10k-50k mixed ops (component write/read-request, transform set, spawn/despawn, draw-submit) drains in a single engine-side lock acquisition (lock-count assertion in a bench or test, not just eyeballing).
  • 0-alloc decode verified via the alloc-budget harness (ENG2-P0-15) or an equivalent allocation-counting test.
  • PERF (area:performance): informational baseline captured for the eventual S3 gate (ENG2-P4-08 owns the hard gate numbers: ≤ 2 crossings, ≤ 1 ms engine-side, 0 steady-state heap allocs per design-expansion.md recalibrated S3 row).
  • 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

Breaking. This adds a new FFI submission surface (the command buffer) that per RFC-0007 and roadmap-inputs.md decision 1 is not kept alongside a permanent legacy compat shim — the per-object flat exports it eventually supersedes get demoted/removed in ENG2-P4-05/07, not maintained in parallel indefinitely. Throne follow-up: THR-A-02 (adopt batch spawn + bulk transform upload + command-buffer submission; delete per-entity SetModelPosition/SetModelRotation loops), gated on THR-A-01 (bump to the v2 FFI surface) first.

Blocked By

ENG2-P4-01 (RFC-0007 must decide opcode format, ownership, and drain semantics before this executes).

Files Likely Touched

  • New/Modified/Generated: goud_engine/src/ffi/ (new command-buffer module), goud_engine/src/component_ops/batch_ops.rs (opcode dispatch targets), goud_engine/tests/spec/

Agent Notes

  • Existing proof-of-concept for "one lock, many ops": goud_component_add_batch (goud_engine/src/ffi/component/batch.rs:72) acquires the context storage map once, then loops entity_slice internally (:145-172 region) instead of taking a lock per entity. The command buffer generalizes this same shape across heterogeneous opcode kinds instead of one fixed batch shape.
  • 682 flat #[no_mangle] exports exist today (grep -rn "no_mangle" goud_engine/src/ffi/ | wc -l) with no bulk/versioned submission path — this issue is the mechanism half of the fix; ENG2-P4-03 is the versioning half; they are explicitly parallel (Batch 4.1, Grp A / Grp B) because they touch different concerns (submission format vs. entry-point versioning) even though both consume RFC-0007.
  • Per .agents/rules/component-ops.md: RawComponentStorage carries unsafe impl Send + Sync (goud_engine/src/component_ops/storage.rs:51,56) whose soundness depends entirely on the registry/storage Mutexes serializing access — the drain path MUST take those locks through the existing guard helpers, not bypass them for speed.
  • Target ops for the opcode set (component write/read-request, transform set, spawn/despawn, draw-submit) come directly from design-technical.md's Batch 4.1 note — do not invent additional opcode kinds without an RFC amendment.

Verification

cargo check && cargo fmt --all -- --check && cargo clippy -- -D warnings
cargo test
./codegen.sh && git diff --exit-code
# perf issues: python3 scripts/bench-gate.py (per perf-dod.md), scene capture attached
# spec test: goud_engine/tests/spec/{eng2_p4_02_slug}.rs

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:ffiFFI boundary layerarea:performancePerformance and optimizationbreaking-changeBreaks the SDK/FFI API; requires a Throne adoption follow-uppriority:criticalMust fix before alpharoadmap:eng2GoudEngine v2 rebuild program (ENG2)size/Ltarget:goudengineGoudEngine repo onlytype:featureNew feature or enhancement

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions