Linked repository document
Gododin parity matrix
docs/parity-matrix.md
This document retains its design, example or evidence context. References outside this guide show their repository paths.
Owner of the competitive comparison required by ADR 0045. Gododin must not be seriously behind godot-rust on any axis it claims to care about. An axis marked Behind/Blocking is a 1.0 release blocker.
Last revalidated: 2026-09-07 (axis 10, on the developer journey and the reload gates' executed receipts). Native editor reload scope and source comparison added: 2026-09-05 under ADR 0057. Earlier measurements retain their individual pins and dates.
Status clarification (2026-09-12, no new measurements): the pins below identify the source reviews and workloads that actually ran. They do not change when the active target advances; current-target qualification must reobserve affected claims. The plan owns implementation progress.
Rules this document follows
- Every row cites checkable evidence: a file and line at a pinned commit, or a version-pinned documentation URL for a source not vendored here.
- Every row states whether it is source-verified (established by reading the implementations) or measured (established by running a named workload). A source-verified row may not be restated as a measurement.
- A claim about a competitor states the narrowest version that is true, including any feature flag or build configuration that changes it.
- No axis is left without evidence. An axis with nothing to cite is recorded as No evidence and carries no verdict.
Verdict vocabulary:
| Verdict | Meaning |
|---|---|
| Ahead. Hold. | Gododin leads. May not regress into parity without a decision record. |
| Parity. Hold. | Neither side is meaningfully ahead on the axis as scoped. The floor ADR 0045 requires is met, so 1.0 is not blocked; falling back below it requires a decision record. Any residual is named in the row. |
| Behind. Blocking. | Gododin is behind on an axis it claims, the gap is closable, and 1.0 is blocked until it closes. |
| Behind. Mitigated. | Gododin is behind on an axis it claims, the gap is not closable given the language, and a named compensating control carries the risk instead. Not a release blocker; the compensating control's evidence is. |
| Behind. Not contested. | Gododin is behind and does not compete here. Recorded so the omission is deliberate. |
| No evidence. | Nothing citable exists on either side. No verdict, and no claim may be made. |
Behind. Mitigated. exists so an honest, permanent deficit has somewhere to live.
Without it such a finding drifts into Not contested, which would be false: the
axis does matter, and something is carrying the risk.
Pinned sources
| Source | Pin | Note |
|---|---|---|
| godot-rust (gdext) | commit f15913efff055721d900bee731ebdd4da09fe4a4 |
Local checkout at /Users/eelco/dev/prj/misc/gdext. |
| godot-rust native editor reload (axis 10 only) | commit d1183d70c71b12816d77de116fc821a4f72bf4f1 |
Source-reviewed reload tests, native reconstruction contract, and desktop CI; not a new competitor timing run. |
| Godot native reload (axis 10 only) | then-locked 4.8-dev4, commit b56a91878e7c94977e4af978968e41d0670c0a8b |
Public extension reload lifecycle is enabled in editor mode at this source pin. |
| Godot C# glue | Godot 4.8.0-dev, commit 20a52a8988 |
Read from the local checkout at /Users/eelco/dev/prj/misc/godot, which has since advanced past that commit. This citation is on the 4.8 line but is not the then-pinned 4.8-dev4 snapshot (b56a91878). Rows citing it say so; equivalence to that snapshot or the current target has not been verified. |
| Godot C# documentation | https://docs.godotengine.org/en/4.7/tutorials/scripting/c_sharp/c_sharp_collections.html and .../c_sharp_basics.html |
Prose manual is not vendored here — the vendored corpus is class-reference XML only. Cited at 4.7 while the target at the time of review was 4.8-dev4; equivalence of the C# collection and marshalling shapes to that snapshot or the current target has not been verified. Paraphrased, not quoted. |
| Gododin | working tree at the date above | The typed surface is generated into bindings/typed/godot and cited from its receipt. The rendering narrowing/helper facade is authored from tools/apigen/templates/rendering_facade.odin.txt, but its class operations delegate to that generated base and own no parallel MethodBind table or ptrcall bodies. |
ADR 0052 accepts work on the named builtin/utility and bulk residuals and the then-unmeasured complete iteration loop. ADR 0061 subsequently implemented typed builtins/utilities, and Phase 15 measured the complete iteration workloads. Those changes do not extend the scope of a compiler-only comparison. Broader authored value coverage is distinct from engine-call coverage; asynchronous responsiveness is distinct from computation speed. New scopes need their own named workloads and measurements before they can extend any current claim. The implementation plan owns that work's status and ordering.
ADR 0053 adds a separate developer journey and removes the permanent reading of native fixture limits. The typed-operation verdict measures its stated projection/boundary scope; it does not establish persistent scene editing, general authored-base/virtual support, scalable native instance storage, or automatic preparation of every authored engine call. ADRs 0054/0055/0058 and the installed developer journey subsequently implemented and exercised those paths. Their evidence is linked from the qualification audit, while the Native PRD records the value forms and engine-base cases that still need work. A separate journey receipt does not extend a typed-operation comparison's measured scope.
Verdicts at a glance
| # | Axis | Verdict |
|---|---|---|
| 1 | Bulk packed-array transfer | Parity. Hold. |
| 2 | Typed engine surface reachable without an escape hatch | Parity. Hold. |
| 3 | RID and device ownership discipline | Ahead. Hold. |
| 4 | Render-thread role model | Ahead. Hold. |
| 5 | Declared per-operation cost | Ahead. Hold. |
| 6 | Slice and alias safety on the bulk path | Behind. Mitigated. |
| 7 | Language type-system power | Behind. Not contested. |
| 8 | Package ecosystem | Behind. Not contested. |
| 9 | Build and iteration cost (named compiler workloads only) | Ahead. Hold. |
| 10 | Native editor reload | At parity on macOS arm64. Hold. |
1. Bulk packed-array transfer — Parity. Hold.
Phase 10 adds separate Cavern Lab input evidence (repository: examples/cavern_lab/mask-evidence.json)
for process_mask: real Godot inputs up to 1 MiB use one size read and one const
pointer acquisition per non-empty input, and only the size read for empty input.
Variant dispatch adds three type-check/handle-copy/release calls. The observed
read keeps the original backing store shared, while control writes detach through
normal engine copy-on-write. The kernel owns its mutable working copy. This
focused checkout result does not remeasure the staged-upload workload or establish
new throughput, latency, installed-consumer, or platform claims.
Source-verified on the gdext side; measured on the Gododin side. This is the axis Gododin claims as its reason to exist. It was Behind. Blocking. while the path was implemented and unmeasured; the measurement now exists and the row records what it showed.
gdext. PackedArray::as_slice and as_mut_slice
(godot-core/src/builtin/collections/packed_array.rs:266,283) acquire a base
pointer once and construct a slice over contiguous storage. The read path uses the
const accessor and the write path the mutable one — ptr calls ffi_index_const
(:402,410) while ptr_mut calls ffi_index_mut (:423,425) — so only writes
detach copy-on-write. The copy-on-write caveat is documented in the SAFETY
comments in place.
Gododin. The per-element loops are gone. packed_byte_array_from_bytes
(bindings/typed/render/rendering.odin:530) acquires the base pointer once and
performs a single mem.copy_non_overlapping; packed_byte_array_read_exact
(:750) does the same on readback through packed_byte_array_read_base (:598),
which uses the const accessor now bound at :143, so a read no longer detaches
copy-on-write. packed_byte_array_length (:568) establishes the bound from the
engine's own PackedByteArray.size rather than from a null return on an
out-of-range index, which a pointer-and-length move never produces.
Packed_Byte_Staging (:623) retains one array across frames and
rendering_device_buffer_update_staged (:914) submits it, so the per-frame path
constructs, resizes and destroys nothing and the engine performs the only copy.
The compositor fixture uses that staged path.
Sole ownership is structural rather than a uniqueness test, because GDExtension exposes no reference count or uniqueness query for a copy-on-write packed array: the owner never copies the array value, never hands it anywhere retainable, re-acquires the base pointer every frame and treats a changed pointer as detachment, and invalidates on a changed size. It declares publisher and consumer roles and orders them with a release/acquire pair, and it is retained in image state, never in a Render callback.
The measurement. fixtures/bulk_transfer/ owns the workload named
density-field-upload: a pure Odin subsystem produces one frame of an f32
density field, the frame is moved into a Godot storage buffer and read back, and
every frame checksums what came back against what the producer wrote. It runs at
16 B, 4 KiB, 256 KiB and 4 MiB — spanning the compositor's former 16-byte-only
parameter case through the 4 MiB case it now corroborates — for 32 frames each,
against three transports: the
per-element implementation this repository shipped before commit 6eacab2,
restored verbatim in fixtures/bulk_transfer/measure/historical.odin; the
current single-copy path; and the current staged path. The counters are
Engine_Counters and Heap_Counters in
fixtures/bulk_transfer/measure/counting_engine.odin, reset at the start of
every frame, and every frame of every transport must produce identical counts,
so these are per-frame numbers rather than a total divided by frames.
fixtures/bulk_transfer/bulk-transfer-evidence.json is the frozen receipt.
Per frame, one round trip at 4 MiB:
| Measurement | per-element | single copy | staged |
|---|---|---|---|
| Foreign calls | 8,388,616 | 12 | 10 |
| Foreign calls at 16 B | 40 | 12 | 10 |
| Engine array allocations | 2 | 2 | 1 |
| Caller-side buffer the payload passes through | 4 MiB | 4 MiB | none |
| Gododin heap allocations | 0 | 0 | 0 |
| Engine bytes copied | 8,388,608 | 8,388,608 | 8,388,608 |
The staged and single-copy costs are identical at every payload size; the per-element cost is two foreign calls per byte plus a fixed eight. On the staged path the producer's destination pointer is the engine's own backing store — observed by comparing it with the store the engine handed out — so the payload passes through no intermediate buffer, which is ADR 0045 decision 6's third clause read off the run rather than off the source.
Why Parity rather than Ahead. gdext obtains the same shape from
as_mut_slice: one acquisition, no per-element call, no per-frame allocation on
the write path. Gododin now measurably has that shape. Neither side is
meaningfully ahead, and no head-to-head timing exists — none is claimed, and one
would need gdext's own workload measured on the same machine.
Residuals, named rather than netted out. First, the measured engine is an
instrumented GDExtension shim, not Godot: it measures the calls, allocations and
copies Gododin's side causes, not what Godot's buffer_update does internally,
which ADR 0045 decision 6 does not constrain. The mechanism against a real engine
is corroborated separately by fixtures/compositor/, which runs the staged path
on the exact pinned target on a Metal Forward+ device. Its second retained owner
takes the workload's 4 MiB density field directly into the engine backing store;
submission re-establishes the bound from Godot, and the shader refuses to tint
unless the transferred tail voxel matches the published generation. The direct
run, current configured run, and both executed exported applications observe that
condition and reproduce the inspected PNG (fixtures/compositor/compositor-evidence.json
and phase4-evidence.json). The fixture still submits on generation change rather
than every rendered frame, and it records no wall-clock or internal-Godot count,
so this is real-engine bulk corroboration rather than a second measurement.
Second, one engine array allocation per frame remains on the read direction,
because RenderingDevice.buffer_get_data returns a new PackedByteArray by
signature, so any binding that calls it receives a newly allocated array. That is
the engine API's shape rather than a Gododin choice, and no claim is made here
about what gdext's own read path costs, which has not been measured.
Third, the staging owner was byte-typed. The float32 twin now exists in the
generated render facade (render.Packed_Float32_Staging, facade schema 4, with
the same structural ownership rules and its own counted instrumented twins in
the bulk fixture's test suite), and the native-multimesh-stream workload
below closes the MultiMesh case against the real engine. Generalizing further to
image payloads still needs a bulk-producing subsystem that does not exist, and
that remains open in docs/implementation-plan.md rather than closed here.
native-multimesh-stream: the real-engine bulk measurement axis 1 named but
did not have. A pure Odin producer writes one staged PackedFloat32Array per
frame — the engine's own backing store, in the engine's own interleaved
multimesh layout — and one RenderingServer.multimesh_set_buffer call submits
it for 1,000 and 10,000 scene-owned instances, 1,200 warm frames per window,
three runs plus a confirmation build, on the pinned 4.8-dev4 under Forward+/Metal
(fixtures/multimesh_stream/multimesh-stream-evidence.json). The engine's
microsecond clock brackets exactly the staged submit, and identically brackets
the per-instance GDScript baseline's update loop on the same scene, mesh,
material, camera, and light: medians 74.6 us versus 155.6 us at 1,000 instances
and 248.6 us versus 1,146.4 us at 10,000 — 2.09x and 4.61x — with the gap
widening as payload grows because the baseline pays one Godot call per instance
per frame while the staged path pays four foreign calls per frame regardless of
payload. Each run verifies instances 0, count/2, and count-1 bitwise through
multimesh_instance_get_transform against the producer's oracle, exercises the
explicit capacity change (225 us median, staged owner replaced), and retires the
owner with zero residual state. The read direction is not exercised here —
multimesh_get_buffer returns an engine-owned packed array by signature — and
that residual stands.
Hold condition. Any change to the bulk-transfer path reruns the workload: the gate remeasures and requires the frozen receipt to reproduce byte for byte, and the receipt's digest travels in the pinned Phase 5 sentinel, so the measurement cannot outlive the code it measured.
2. Typed engine surface reachable without an escape hatch — Parity. Hold.
Source-verified on the gdext side; measured on the Gododin side. This axis was Behind. Blocking. on two gates. Both are closed; the row records what is now true and what remains as a named residual rather than as a blocker.
gdext. Generates the full class API from extension_api.json. RenderingDevice
carries no member exclusions — it appears in
godot-codegen/src/special_cases/special_cases.rs only at :356, inside the
abstract-class list. Every member is typed with no per-operation authoring, and
gdext exercises its generated surface against a live engine in its own integration
tests.
Gododin. The hand-authoring cap is gone. tools/apigen/typedgen projects the
locked metadata into bindings/typed/godot: 1,050 class handles and 15,591
operations, one per non-virtual class method, published with a receipt and
verified byte for byte. Coverage records a disposition for all 17,033 declared
class methods — 14,527 plain, 1,049 full-arity for default arguments, 15 routed to
the dynamic Variant path for varargs, and 1,442 virtuals excluded — so the
resolutions named in ADR 0045 decision 16 reproduce exactly. The surface is also
reachable rather than only complete: 41 singleton accessors and 924 constructors
are generated from the declared singletons and from is_instantiable, and the
126 classes the metadata refuses are recorded as such. These counts move with
every Godot pin advance; bindings/typed/godot/receipt.toml and its coverage
document are the authority, and this row is restated from them at each
revalidation.
Execution, which was the last blocking gate. fixtures/typed_surface/ loads a
GDExtension into the exact pinned Godot snapshot, fills the generated package's
Bound_Interface from real interface pointers, resolves eleven operations through
classdb_get_method_bind by the class name, method name and engine hash the
generator emitted, takes first handles from two singleton accessors and both
constructor branches across three instances, calls through every bound operation,
releases what it owns with no ObjectDB leak, and refuses four malformed calls
before the engine. The dynamic vararg path, RID provenance return, and typed Array
and Dictionary return bodies all execute. Three processes per run, and the pass
sentinel is pinned in the Phase 5 stage together with the receipt's digest.
The same run settles the demand-driven claim for a shipped artifact rather than an object file: the loaded library carries exactly 45 generated symbols — twelve referenced operations, eleven binders, five entry points and seventeen shared runtime procedures — out of a surface declaring 15,591 operations and 965 entry points.
Drift, which was the other blocking gate. project_reviewed_semantics
(tools/apigen/typed_policy.odin) resolves all 38 reviewed rows against the
projection by canonical identity and engine hash at generation time, which is
where ADR 0045 decision 28 puts it, and validate_facade_implements_policy
(tools/apigen/render_policy.odin:264) requires the authored template to declare a
procedure for every reviewed typed_name. Both are proven to fail closed by
perturbing a policy row.
Residual closed 2026-09-06. The class-method residual below — builtin class
methods, constructors, operators, and utility functions being raw-only — is now
generated: tools/apigen/typedgen projects all four builtin surfaces from the
same semantic model (999 methods including six vararg, 156 constructors, 114
utilities including twelve vararg, and 751 operators, with the 96 Nil-owned
records refused as the stated refused_nil_type disposition because the Nil
pseudo-type has no Odin value representation). Binding self-resolves through the
four variant_get_ptr_* interface getters the version-3 Bound_Interface
carries, and every emitted body shape executed against exact 4.8-dev4 under the
refrozen fixtures/typed_surface receipt. The marshaling is measured by the
engine's own dispatch tables, not asserted: floats are 8-byte doubles at every
call position (PtrToArg<float> is PtrToArgConvert<float, double>), which the
model now records per category, and only storage layout stays real_t-width.
The original residual text is retained below for the record.
Residual, named rather than netted out (historical). This axis is scoped to class methods,
which is the target
docs/implementation-plan.md states and the target that is met. gdext also
generates typed wrappers for builtin class methods and utility functions; Gododin
publishes those in the raw stratum only — the pinned target declares 999 builtin
methods, 751 operators, 156 constructors and 114 utility functions — so a project
needing one of those typed drops to bindings/raw. That is a real narrowing of
"without an escape hatch" and it is recorded here so a decision on whether it
becomes its own axis is deliberate rather than an omission.
All thirteen emitted class-method body shapes execute under the pinned live receipt. The rendering facade was also rebased onto the generated base on 2026-09-03. Its authored code now owns reviewed narrowing, ownership, workflow, builtin, and staging behavior; generated class operations own the MethodBind slots and ptrcall bodies. The live typed receipt and both compositor receipts reran after the publication changed, so closing ADR 0045 decision 36 did not weaken this axis.
Hold condition. Regenerating the surface invalidates the live receipt by digest and requires the run to be repeated, so the evidence cannot outlive what it tested.
3. RID and device ownership discipline — Ahead. Hold.
Source-verified.
gdext. Rid is enum { Valid(NonZeroU64), Invalid }
(godot-core/src/builtin/rid.rs:39-44), asserted u64-sized at :50. It carries
no resource kind, device affinity, producer identity, or liveness. Its
documentation scopes the guarantee to "will not cause any UB" (rid.rs:19) and
expects bad calls to surface as Godot errors rather than being prevented. The
type's own "Safety caveat" section makes the guarantee weaker still, so this
characterization is conservative.
C#. Rid is a readonly struct wrapping a single private readonly ulong _id
(modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs:21-23 at Godot 4.8.0-dev
20a52a8988). No kind, device, producer, or liveness.
Gododin. Owned_RID (bindings/typed/render/rendering.odin:84) carries
value, kind, device_kind, device, producer_id, creator_role,
local_owner, and live; Borrowed_RID (:95) carries the same minus liveness.
Cross-device use, double-free, and wrong-role use fail before the engine call.
Hold condition. ADR 0045 decisions 18 and 19 make provenance a generated
property of every operation rather than a reviewed one, so these checks broaden to
the whole surface instead of the 27 reviewed operations. Decision 20 forbids the
generated surface from asserting resource kind, which is the one part of this axis
that was ever invented: kind is consulted semantically at exactly one site today,
bindings/typed/render/rendering.odin:730, and stays a reviewed fact.
4. Render-thread role model — Ahead. Hold.
Source-verified. State this axis carefully; an earlier draft overstated it.
gdext. Generates CompositorEffect, but only when the experimental-godot-api
feature is enabled: the class is listed in is_class_experimental
(godot-codegen/src/special_cases/special_cases.rs:208) and experimental classes
are deleted from codegen unless that feature is on (:145). With the feature
enabled, _render_callback is an ordinary generated virtual with no role check.
Threading is the crate-wide experimental-threads feature, whose documentation
states that "The safety aspects are not ironed out yet; there is a high risk of
unsoundness at the moment" (godot/src/lib.rs:153).
It is not true that gdext has no CompositorEffect support. The accurate claim
is that it is feature-gated and carries no render-thread role model.
C#. No render-thread role model. No evidence gathered on C# CompositorEffect
support; this row makes no claim about it.
Gododin. ABI 4.0 Render role under ADRs 0042 and 0044. Call_Role is
{ Invalid, Main, Render } (bindings/typed/render/rendering.odin:11). The role is
declared per authored virtual, a Render callback receives callback-local context
and no Host services, and wrong-role dispatch is rejected.
5. Declared per-operation cost — Ahead. Hold.
Source-verified on the competitor side; measured on the Gododin wrapper side.
gdext and C#. Neither models per-operation submission, synchronization, copy,
or allocation cost. A blocking sync is indistinguishable from any other generated
method. No claim is made about integrations this repository has not examined —
godot-cpp, godot-jvm, godot-swift and others are unexamined.
Gododin. Every reviewed operation records capability, device_scope,
ownership, copy, submission, synchronization, and failure
(bindings/policy/compositor.json), carried into the generated plan and
documentation. The named reviewed-render-typed-vs-raw workload in
fixtures/render_cost/ compares three promoted facade calls with direct ptrcalls
to the same prebound MethodBinds for 256 iterations per path. Both paths make 768
ptrcalls and zero Odin heap allocations; their observed scalar-placement,
submission, and synchronization entry points agree. One invalid typed state per
operation is rejected before an engine call. This is a wrapper-boundary
measurement against an instrumented shim, not a wall-clock, GPU-duration, or
Godot-internal-allocation claim.
Hold condition. ADR 0045 decisions 15, 23, and 24 keep this honest: where the
metadata establishes nothing the surface says nothing rather than emitting an
"unknown" value, absence is presented as absence, and only a reviewed operation may
claim the derived direct guarantee. The lead is narrower than it first appears —
cost is declared for reviewed operations, not across the whole surface — and saying
so plainly is what keeps the axis defensible.
6. Slice and alias safety on the bulk path — Behind. Mitigated.
Source-verified. This is the one axis where Rust's type system directly buys safety on the axis Gododin claims as its reason to exist, and closing axis 1 will not remove it.
gdext. as_slice(&self) -> &[T] and as_mut_slice(&mut self) -> &mut [T]
(godot-core/src/builtin/collections/packed_array.rs:266,283) return borrow-checked
slices. resize(&mut self, size: usize) (:214), push (:160), and clear
(:151) all take &mut self, so holding a slice across any of them is a compile
error. The slice cannot outlive the array, cannot alias it, and cannot survive a
reallocation. No runtime check is involved and none is needed.
Gododin. Odin slices carry no lifetime or aliasing information, so the same
guarantees are unavailable at compile time. ADR 0045 decision 10 substitutes a
runtime detector — the staging owner re-acquires the base pointer each frame and
treats a changed pointer as detachment — plus a structural sole-ownership
convention, because GDExtension exposes no uniqueness query. ADR 0045 decision 11
substitutes a declared thread role and publication protocol for the cross-role
alias that Rust's Send/Sync would flag.
Why Mitigated rather than Blocking. The gap cannot be closed in Odin. ADR 0045
decision 12 names decisions 10 and 11 as compensating controls for a permanent
language deficit rather than ordinary diligence. The risk is carried by the
evidence that ADR names, and that evidence now exists in
fixtures/bulk_transfer/measure/staging_test.odin, gated in both active tiers:
resize invalidation, detected base-pointer detachment at an unchanged length,
bounds established from the engine on both directions, a bound re-established on
the consuming side at submission, teardown ordering, no retained slice after
invalidation, and the cross-role publication protocol with a Main publisher and a
Render consumer. A companion case proves the distinction the read path depends
on: the mutable accessor detaches a shared store and the const accessor does not.
Those tests are not ordinary diligence — they are the substitute for a
compile-time guarantee a competitor has. Trimming them as over-cautious would
silently convert this row to an unmitigated deficit.
What would change this. Nothing in the language. Only a stronger runtime detector, or evidence that the compensating controls are insufficient, which would move the row toward Blocking.
7. Language type-system power — Behind. Not contested.
Rust's type system is stronger than Odin's: ownership and borrowing, Send/Sync,
exhaustive sum types, and traits. Odin has distinct types, enums, and unions, and
no lifetime, aliasing, or thread tracking. Gododin does not contest this.
Recorded with one qualification, because a flat verdict here misleads about the
Godot boundary specifically. gdext largely does not spend that type system on the
boundary's resource problems: Rid is a bare u64-sized enum with no device or
liveness (axis 3), threading is opted out of as experimental and the render
callback carries no role check (axis 4), and per-operation cost is unmodelled
(axis 5). Boundary safety is therefore not decided by this axis; it is decided by
axes 3 through 5, where Gododin leads, and by axis 6, where it does not.
The honest summary: Rust's type system protects you from your own code. Gododin's modelling protects you from the engine boundary. Axis 6 is where the first of those also reaches the boundary.
8. Package ecosystem — Behind. Not contested.
gdext offers cargo and crates.io. Odin has no ecosystem of comparable size. godot-rust is also the more mature project, with more maintainers and more eyes. Gododin does not contest any of this and does not claim otherwise. Not a release blocker.
9. Build and iteration cost — Ahead. Hold.
Measured, both sides, on one machine: Apple Silicon, macOS 25.6.0. Dependencies warm, each project's own crates or packages clean, debug/unoptimized profile.
Workload. Type-check the project's complete generated engine API surface for one Godot target from clean.
| Measurement | gdext | Gododin |
|---|---|---|
| Type-check the complete generated surface | cargo check -p godot-core --features codegen-full — 15.78s |
odin check bindings/typed/godot — 2.1–2.6s over three runs; remeasured 2026-09-06 at the expanded scope (1.99–2.46s over three runs) |
| Produce a linkable artifact of that surface | cargo build -p godot-core --features codegen-full — 19.49s |
Not applicable; Odin compiles only referenced procedures |
| Build an image that uses three operations | Not measured as an isolated workload | 1.7–2.5s over three runs, 7.1 KB object from the generated package; remeasured 2026-09-06 after the builtin surface joined — the object stayed 7.2 KB |
| Reduced default surface | cargo check 5.39s, cargo build 5.90s |
Not applicable; Gododin publishes one surface |
The Gododin figure includes bindings/raw, because the typed package imports it
and the compiler type-checks it too — so the 32,602 raw records and every builtin
storage type, enum, native structure, and interface type are inside the 2.0
seconds. The 2026-09-06 remeasure covers the same workload after ADR 0061's
builtin surface joined the package (451,851 generated lines, up from 335,298):
the clean type-check stayed under 2.5 seconds and the three-operation object
stayed at 7.2 KB, because dead-strip cost is paid per referenced procedure, not
per declared one.
Scope caveat, stated rather than netted out. These are each project's complete generated surface as it exists, not identical scopes. gdext also generates builtin class methods, utility functions, and a virtual trait per class; Gododin's typed package generated neither builtin methods nor virtuals when this axis was first measured — the builtin methods, constructors, operators, and utility functions closed that gap on 2026-09-06 (ADR 0061), leaving only per-class virtual traits ungenerated, which Gododin's architecture handles through authored virtuals instead. gdext's generated output is 547,056 lines across 974 class files plus builtins, utilities, and virtuals; Gododin's is 451,851 lines. Line counts are therefore not comparable and are not being compared — only the times are.
Version caveat. gdext at the pinned commit generates against Godot 4.5 through
gdextension-api 0.5.1 (prebuilt 3a0f8c3d). Gododin generates against the
pinned 4.8-dev4 snapshot. The comparison is across different API versions.
The structural reason, not a tuning difference. Rust compiles a crate; an
unused generated method still costs compile time in godot-core, which is why
gdext ships a reduced default surface and puts the full one behind
codegen-full. Odin dead-strips an unreferenced procedure and its data, so
Gododin does not need the equivalent choice: an image that references three
operations carries three. That property is measured on axis 2's evidence and gated
by tools/apigen/typed_surface_test.sh.
What would move this verdict. A workload where Gododin's advantage does not hold — a large image referencing most of the surface, or an incremental edit-rebuild loop, neither of which is measured here.
The 2026-09-05 Phase 8 generated-bridge revalidation re-measured both axis 9
workloads on the locked toolchain after the project-image build input changed to
the generated bridge and authored class packages. The complete-surface type
check ran 2.1–2.6s over three runs (previously 1.6–2.1s) and the
three-operation probe build ran 1.7–2.5s (previously 1.8–2.0s), with the
probe's generated-package object at 7.1 KB again and typed_surface_test.sh
confirming the object still carries exactly its three referenced operations plus
the shared runtime. Both figures moved within run-to-run noise of their previous
bands and stay an order of magnitude ahead of the gdext columns, so the verdict
is reused on fresh measurement rather than inherited from the handwritten image.
The migrated fixtures' builds (starter, Cavern Lab, native-basic, compositor)
compile in the same seconds-scale order through the six-stage owner. Authoring
line count or attribute count is not folded into this timing axis.
The 2026-09-05 Phase 9 revalidation followed ADR 0058's receiver and
use-derived-inventory work, which regenerated the typed surface twice: once to
add the per-class checked casts and once to correct class-method floats to the
ptrcall ABI's f64. The complete-surface type check re-measured at
1.5–2.2s over three runs on the locked toolchain, inside the previous band,
and typed_surface_test.sh reconfirmed demand stripping: the three-operation
probe object still carries exactly its three referenced operations plus the
shared runtime, with the 1,050 cast helpers stripped when unreferenced. The
live typed-surface receipt re-froze against the regenerated surface with all
thirteen body shapes executing and zero leaks, keeping axis 2 on fresh
evidence.
Capability note, not a timing row. gdext links native C dependencies
through cargo build scripts. Gododin's sealed build previously admitted no
foreign library at all — every staged input is .odin-only and the link
command was fixed — so a project whose subsystem imported a vendored C
collection could not produce an image.
ADR 0064 closes that gap
on macOS arm64: [project] foreign_libraries admits project-tree static
archives, staging carries them, the link stage links them into the project
image, and receipts record their digests. The image's dynamic dependency
closure stays libSystem only, the same closure this axis's artifacts already
declare. The rewind-lab example's Box2D link is the first executed consumer;
the Windows/Linux linker admissions ride the desktop workstream.
10. Native editor reload — At parity on macOS arm64. Hold.
Source-verified capability comparison, added by ADR 0057. The row compared the gdext reload experience against Gododin's gap; the gap closed under ADR 0057 and the receipts below. The desktop spread (Windows x86-64, Linux x86-64) rides the per-platform gates of active Phase 11 (ADR 0065), like every other platform-bounded capability.
gdext. At d1183d70c71b12816d77de116fc821a4f72bf4f1, the
reload test
checks changed method behavior and restoration of an exported value on a retained
Godot object. The native-instance contract
recreates native instances and restores storage properties; private Rust fields
are reconstructed, not universally preserved. The
CI matrix
includes reload tests on macOS, Windows, and Linux. Reading the test and CI is
source evidence, not a claim that this review executed them. Automatic source
compilation is a build-tool concern; it is separate from native library reload.
Gododin. The generated descriptor
(cmd/gododin-stage/main.odin (repository: cmd/gododin-stage/main.odin)) sets
reloadable = true, and the editor addon (repository: editor/addons/gododin/plugin.gd)
watches the authored Odin roots, rebuilds through the configured coordinator,
re-syncs the descriptor, and activates the new image in the same editor process
with instance reconstruction and stored-property restoration (ADR 0057).
Executed receipts on the sealed
gododin-phase10-2026-09-07.1 publication (reload gates first closed on the
.7 publication), pinned exact 4.8-dev4:
fixtures/native_components/reload-installed-small-evidence.jsonandreload-installed-scale-evidence.json— 31 successful in-process reloads each (1 and 64 live component pairs), including a compiler-failure case the editor surfaces, a superseded-save collapse, an incompatible-schema refusal, and bounded resident-publication accounting, all as an installed-SDK consumer.fixtures/compositor/compositor-reload-evidence.json— one focused authoring session on the compositor fixture: source edit toGODODIN_NATIVE_RELOADEDin the same editor process, the previous publication released cleanly, and the post-reload accepted editor shutting down with zero owners.examples/cavern_lab/journey-evidence.json(cavern-native-authoring) — the developer journey's iterate stage records the human-action loop end to end: an exposed-member addition and a method-body change reload automatically inside the running authoring editor, a broken revision is diagnosed and repaired through its compiler diagnostic, unsaved scene and Resource edits survive the reload, saved values and identities persist across close/reopen, the regenerated class reference documents the new member, a re-signed engine copy stops at the authoredcave_region_generatewith source-line and scoped-receiver symbols, and the recorded scenario restarts after a native edit with the changed method's output (revision 20 to 300). Save-to-observed reload latency is recorded per stage in the same receipt, separately from kernel timing.
Required closure. None on the implemented platform. The reload bounds above (the plan's at-least-30-successive-reloads gate, failure/repair, restoration, preserved unsaved work, current symbols, bounded retained publications) are executed receipts. Windows x86-64 and Linux x86-64 repeat the same workloads under their platform gates before those platforms claim support.
Continuous native replacement in an already-running game and arbitrary private state migration are outside this comparison. The pinned engine enables native extension reloading in editor mode. Editor reload parity cannot be replaced by a fast editor restart or a procedure-body-only capability ceiling.
Not an axis: language preference
Some people prefer a language with few constructs and generally one way to do
things over a large and expressive one. That is a real reason people choose Odin
and it is recorded in README.md as a preference. It is not an axis here because
it is not a technical claim and no evidence would settle it. It must not be
restated as one.
Supporting note: the C# data path
Not a numbered axis, because the parity floor is defined against gdext. Recorded because it is the clearest illustration of axis 1.
ConvertNativePackedByteArrayToSystemArray
(modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs:423 at
Godot 4.8.0-dev 20a52a8988) allocates new byte[size] and Buffer.MemoryCopys
the engine buffer into it. Every crossing is a managed allocation plus a full copy;
there is no handle type and no pointer into the engine's store. The C# collections
documentation describes packed arrays as replaced by System.Array types, and the
C# basics page warns that passing raw arrays or strings to the C# API requires
comparatively expensive marshalling. The official compute-shader tutorial's C#
sample performs a Buffer.BlockCopy and hands a managed byte[] to
StorageBufferCreate — two copies and a managed allocation before data reaches the
GPU. That sample is one-shot setup code; the same pattern on a per-frame path would
add collection pressure, which is a statement about the pattern, not about the
sample.
Revalidation
Revalidate this document when any of the following changes:
- the rendering facade, either generated coverage document, or the bulk-transfer path;
fixtures/bulk_transfer/, whose frozen receipt carries axis 1's measurement and whose staging tests carry axis 6's compensating-control evidence;fixtures/typed_surface/, whose frozen receipt carries axis 2's execution evidence and is invalidated by any regeneration of the typed surface;fixtures/compositor/, whose two frozen receipts carry axis 1's real-engine corroboration of the staged path and axis 6's rendering evidence;bindings/policy/*.json, either coverage schema,Bound_API, orBound_Interface;- the pinned sentinels in the Phase 5 test stage, which carry axis 2 and axis 9 evidence;
- ADR 0050's generated authored-class bridge, project-image source shape, or capability-injection support, which must rerun the axis 9 measurements rather than inheriting them from the handwritten image;
- a pinned competitor commit, or a cited documentation source's version; or
- an axis verdict, which additionally requires updating
docs/implementation-plan.md.
Losing an Ahead or Parity verdict requires its own decision record under ADR 0045 decision 2. Weakening a compensating control on a Behind. Mitigated. axis requires one under ADR 0045 decision 12, because the control is the only thing carrying a risk a competitor eliminates at compile time. Reaching 1.0 requires that no axis is marked Behind/Blocking.
Current status: axis 10, native editor reload, is At parity on macOS arm64. Hold. under ADR 0057; Windows/Linux implementation and qualification remain required. Axis 1 closed on its own stated terms — the density-field-upload workload measures the staged path at a per-frame cost independent of payload, with no Gododin allocation and no intermediate buffer, against the per-element implementation it replaced — and its residuals are named in the row: an instrumented engine rather than Godot, the read direction's engine-owned array, and a byte-typed staging owner. Axis 2 reached parity: the surface is generated whole, carries its own entry points, executes all thirteen emitted body shapes against a live engine under a pinned receipt, and its policy drift seam is closed at generation time. ADR 0061 subsequently closed its typed builtin/utility gap; the row names the current coverage bounds and resolutions. Axis 6 is permanently behind and is carried by compensating controls whose evidence is now in place and gated. Axes 3 through 5 lead and must not regress. Axes 7 and 8 are not contested. Axis 9 has both sides measured and leads within its compiler workloads. The expanded parity gate remains open until axis 10's real-editor workload passes across the desktop matrix; the implementation plan owns that work and the remaining product gates.
The 2026-09-02 revalidation was triggered by the ADR 0047 Godot update
transaction, which regenerated the typed surface, republished the rendering
facade, and moved the qualification target from exact 4.7.2 to the official
4.8-dev4 snapshot. No verdict moved. Axis 2's execution evidence was reobserved
on the new target with the same seven bound operations, eight calls, two
releases, four rejections and zero leaks, and fixtures/typed_surface/'s receipt
is rebound to the regenerated publication. Axis 1's real-engine corroboration and
axis 6's rendering evidence were reobserved by
fixtures/compositor/live/run.sh, which reproduced the inspected PNG byte for
byte across the target change. Counts that were literals here are now cited from
bindings/typed/godot/receipt.toml, because they move with every pin advance and
went stale in prose once already.
The later 2026-09-02 documentation revalidation moved typed coverage to schema 3:
the complete operation table now carries exact Odin signatures and a sparse
authenticated overlay for the 38 reviewed rows. The generated GDI and
gododin doc therefore expose review presence without inventing an unknown
value, and only a reviewed row can set direct. The typed source was byte
identical, the exact-target live fixture reproduced its three-process sentinel
and was rebound to the new coverage digest, and no axis verdict moved.
The 2026-09-03 body-shape revalidation extended the exact-target live fixture without regenerating the surface. Eleven operations and five entry points now drive thirteen engine calls across all thirteen emitted bodies; the vararg input and result Variants and the returned Array and Dictionary are each destroyed once, the valid camera RID carries the observed producer, sequence, and Main role, all three constructed objects are destroyed, and three processes report zero leaks. The loadable library carries exactly 45 generated symbols and still excludes everything it does not reference. Axis 2 remained Parity. Hold.; its named surface residual at that checkpoint was typed builtin methods and utility functions staying raw-only, subsequently addressed by ADR 0061.
The 2026-09-03 real-engine bulk revalidation moved the density-field-upload workload's 4 MiB case through a retained compositor staging owner. The producer wrote directly into the Godot packed-array backing store and the shader made the visible tint conditional on the transferred tail voxel matching the published generation. The direct Forward+/Metal run, the current configured run, and both executed exports passed, all four verified the tail condition, and all reproduced the inspected PNG byte for byte with zero owners. Axis 1 remains Parity. Hold.; the quantitative receipt still measures the Gododin-side cost against its instrumented shim and this run adds no internal-Godot or timing claim.
The 2026-09-03 documentation and cost revalidation made axis 5's Gododin side
measured rather than source-only. fixtures/render_cost/ freezes three reviewed
typed-versus-raw comparisons with identical observed boundary effects and no Odin
heap allocation, while retaining the declared classifications for scalar copy,
explicit submit, and blocking sync. The generated GDI now presents reviewed
facades and stable human-readable policy lines before the mechanical base call.
The native-basic catalog also emits its exact authored signal-signature marker;
the locked compiler accepts a matching Signal/Callable pair and rejects a
different marker. No parity verdict or platform-support claim moved.