graphics.gd FFI Performance Breakthrough (Discussion #277)
Key points
- graphics.gd (a Go GDExtension binding for Godot) has reduced Go→Engine FFI overhead to 8.1 ns/op (arm64 method call) with Go 1.26 plus all in-house optimizations — faster than GDScript's 12.8 ns/op.
- On x86_64 with Go 1.25/1.26 and partial optimizations: MethodBind return = 44.17 ns, void = 23.30 ns (ring batch amortized) vs GDScript 23.10 ns.
- The WebAssembly path remains 9–18x slower than desktop: return value 786.1 ns, void 209.2 ns, vs GDScript 50.62 ns.
- The claim of being "ahead of safe Rust (gdext)" is qualitative; no comparable public ns/op numbers exist for gdext safe mode.
- The 23.30 ns void figure is amortized (tight loop with ring batching); isolated calls with return values approach 44 ns.
- GDScript is a user-perceived latency floor, not the theoretical FFI limit (bare
extern "C"is ~4–10 ns). - Uses a direct bridge: Go exposes
//go:wasmexport gd_on_*entry points, avoiding JS trampolines, value boxing, and 64-bit splitting. No pre-bridge baseline was published, so the net benefit cannot be quantified. GOOS=js GOARCH=wasmlacks full cgo, so graphics.gd uses a generated ABI shim.- Thread safety on web (commit
c83a441) distinguishes frame goroutines from free goroutines under a single OS thread.
Benchmark numbers
| Source | Hardware | MethodBind return | MethodBind void | GDScript | |---|---|---|---|---| | Post opening | x86_64 | 44.17 ns | 23.30 ns | 23.10 ns | | Post end | arm64 | 8.114 ns | 13.10 ns | 12.84 ns | | Web | wasm32 | 786.1 ns | 209.2 ns | 50.62 ns |
Caveats:
Go 1.26 cgo improvement
A single commit (7244e92) by Michael Knyszek removes the _Psyscall state, eliminating an atomic Store and an atomic CAS on the syscall path:
| Benchmark | Hardware | Before | After | Δ | |---|---|---|---|---| | CgoCall-64 | EPYC 7B13 | 43.69 | 35.83 | −17.99% | | CgoCall-8 | Apple M1 | 28.55 | 19.02 | −33.40% | | CgoCallWithCallback-8 | M1 | 72.76 | 57.38 | −21.14% | | Syscall-8 | M1 | 195.6 | 178.1 | −8.95% |
The ~30% figure is a cross-hardware geometric mean. Historical context: Go 1.24 added #cgo noescape/nocallback; Go 1.26 delivers the _Psyscall elimination.
The three-tier optimization stack
Tiered by call hotness, all landed within ~10 days in February 2026:
1. Leaf methods (~25% of API) — jumponly.Call: assembly trampoline issues a direct CALL to the ptrcall function, skipping cgo entirely. Leaf detection via Clang LibTooling static analysis; virtual methods are excluded. (Commit b26bcf6, 667 files.)
2. Main-thread sequential void calls — ring.Main.Buffer+Flush: a 256-slot ring buffer packs arguments and executes batched calls with a single cgo transition. Flush triggers: buffer full, return value needed, or object Free. (Commit eb7eef4.)
3. Non-leaf, non-main-thread, return-value calls — noescape.Call + cgocall using //go:noescape to skip escape analysis.
Coordination: jumponly checks ring.Main.Pending() before calling — flushes if a return value is needed, or routes via noescape to preserve batching for void calls.
The object representation was also rebuilt around [1]gdreference.Object + pointers.Generic[T,S], letting escape analysis eliminate heap allocations with almost no API breakage.
FFI cross-language comparison
| Path | ns/op |
|---|---|
| Rust bare extern "C" | 4–10 |
| gdext builtin_ffi (cached) | 9–10 |
| Go 1.26 cgo (arm64, fully optimized) | 8.1 |
| GDScript call | 12.8 |
| gdext utilities_ffi (cached) | 30–32 |
| graphics.gd MethodBind return | 44.17 |
| gdext "safe mode" (estimated) | ~50–100+ |
Go 1.26 cgo is in the same order of magnitude as Rust gdext's cached FFI.
WebAssembly path
Ecosystem context
| Binding | Language | Stars | Web support | Godot | |---|---|---|---|---| | godot-cpp | C++ | official | full | 4.3–4.6 | | gdext | Rust | 4,440+ | experimental | 4.6–4.8 | | Godot C# | C# | official | none | 4.x | | SwiftGodot | Swift | 1,700 | none | 4.4 | | graphics.gd | Go | ~790 | yes | 4.7.0 | | GodotJS | JS | 1,059 | — | 4.1 |
graphics.gd's unique position: the only binding combining zero-SDK full-platform support (including Web/Android, cross-compiling via zig cc), native binaries without .NET runtime, and strongly typed RIDs. Risks: API drift before 1.0, high bus factor (essentially one maintainer, Splizard), and sparse sample projects.
Path to 1.0
The maintainer distinguishes two kinds of stability:
1. API Stability — driven by Godot minor upgrades (5–10 breakages per minor on average, 1–2 hitting GDExtension users; e.g., JSONRPC.set_scope rename in 4.5, OpenXRExtensionWrapper.xr_version in 4.6). graphics.gd refuses major-version import path suffixes.
2. Runtime Stability — semantic version tags alone don't help; only test coverage (currently 47 test cases) and sample project completeness matter.
Conclusion: the main blocker to 1.0 is runtime stability (test coverage and sample projects), not API stability. Recent commit activity focuses on Godot 4.7 adaptation, multi-platform CI, thread safety, and reproducible builds — release-grade polish, but with no explicit 1.0 announcement yet.