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
Acceptance Criteria
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
Parent
eng2-p4-ffi-v2)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/(seetools/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_implfunctions (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):
#[no_mangle] extern "C",#[repr(C)], null checks,// SAFETY:comments on every unsafe block touching the opcode buffer..agents/rules/component-ops.md: storage state lives behindCONTEXT_COMPONENT_STORAGE/COMPONENT_TYPE_REGISTRYMutexes — take the lock, operate, drop it; do not leak aMutexGuardacross 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-172region). The command buffer generalizes this to a heterogeneous opcode stream instead of one homogeneous batch.Scope
_implfunctions.goud_engine/tests/spec/covering encode/decode round-trip and the single-lock-per-frame invariant; unit tests for each opcode kind..agents/rules/ffi-patterns.mdgets the command-buffer pattern documented alongside the existing per-function FFI convention.Acceptance Criteria
design-expansion.mdrecalibrated S3 row).cargo check && cargo fmt --all -- --check && cargo clippy -- -D warningsclean;cargo testgreen;./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.mddecision 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-entitySetModelPosition/SetModelRotationloops), 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
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
goud_component_add_batch(goud_engine/src/ffi/component/batch.rs:72) acquires the context storage map once, then loopsentity_sliceinternally (:145-172region) instead of taking a lock per entity. The command buffer generalizes this same shape across heterogeneous opcode kinds instead of one fixed batch shape.#[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..agents/rules/component-ops.md:RawComponentStoragecarriesunsafe 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.design-technical.md's Batch 4.1 note — do not invent additional opcode kinds without an RFC amendment.Verification