Gododin Gododin Documentation

Linked repository document

ADR 0057: Require native editor reload at least at godot-rust parity

docs/decisions/0057-native-editor-reload-parity.md

This document retains its design, example or evidence context. References outside this guide show their repository paths.

View original Markdown · Download · Return to the guide

Status: Accepted — implemented and workload-qualified on macOS through C1/C3, with complete iteration measured at C4. Final clean-machine and other-platform qualification remain in the current plan. The dated implementation notes below retain their original checkpoint context. Date: 2026-09-05

Extends ADRs 0045 and 0056. Supersedes the blanket editor reload exclusion in ADRs 0034/0038/0052/0056 and the interpretation of ADRs 0035/0053/0055 that every native edit must close the Godot editor. Completed process-lifetime Host/image and durable-workspace evidence remains valid for the behavior it exercised. This decision changes no implemented ABI, generated artifact, lock, or support status. ADR 0051's permanent scripting exclusion remains.

Context

The user has accepted automatic rebuilding and native reload inside Godot's editor as required development behavior, with godot-rust as the minimum parity baseline. Treat this as achievable implementation work. A feasibility study, optional experiment, or restart-only fallback cannot replace that commitment.

The previous scope grouped editor reload together with the stronger problem of replacing code in a running game while preserving arbitrary native memory. The useful editor workflow does not require that stronger promise. gdext rebuilds native instances and restores stored Godot properties while the editor stays open. Its implementation and tests provide concrete prior art:

These are source-verified capability observations, not new Gododin execution or performance results. Existing compiler timings do not measure this workflow.

Decision

  1. Require automatic rebuild and native editor reload. Saving relevant Odin source in an external editor invokes the existing build owner and activates a current accepted artifact in the same Godot editor process. Cover configured Odin roots outside Godot's resource directory. Neither text-editor closure, manual library copying, nor Godot-editor restart is a normal step for edits admitted by the reload contract.
  2. Match the native editor behavior of gdext at minimum. Preserve Godot scene/object and Resource identity, stored property values, authored files, and unsaved work. Reconstruct Odin structs through their lifecycle and restore compatible properties through the generated bridge. Update methods, properties, Inspector presentation, and generated class-reference metadata from the accepted catalog. Ordinary property additions/removals and changed native layouts belong in implementation and qualification; do not impose a blanket procedure-body-only or unchanged-layout limit. Renames and incompatible property changes follow explicit identity/conversion rules and diagnose loss before changing authored data.
  3. Keep native lifetime responsibilities explicit. Old callbacks, workers, render work, native callables, allocations, and engine references must be drained, released, or reconstructed under their actual owner before their code becomes invalid. Rebuild private caches and connections through a documented reload lifecycle; do not promise automatic preservation of arbitrary Odin memory or replay of Godot tree-entry events. State remains ordinary Odin structs and procedures, with callback-scoped Main/Render authority.
  4. Use the existing authorities and public Godot lifecycle. Prefer Godot's GDExtension reload/reconstruction facilities. The build coordinator owns compilation, source supersession, immutable artifact publication, and status; the Host/bridge owns native lifetime and registration. Settle whether the Host stays resident or participates in the public extension lifecycle during implementation, preserving every engine-retained pointer for its valid lifetime. Do not reserve a second catalog, public command, or speculative ABI, or restore the old comprehensive transactional reload protocol by default.
  5. Keep failures recoverable and state truthful. Failed or superseded builds never replace the current artifact. Distinguish published from actually loaded code and diagnostics. A failed activation retains authored work, the GDScript recovery UI, and a usable repair/restart route without claiming an unproved rollback of native execution. Current Play/export gating remains. Controlled restart handles diagnosed engine/toolchain incompatibilities and recovery; it does not satisfy editor-reload parity for ordinary supported edits.
  6. Implement across the agreed desktop matrix. Loader behavior, native cleanup, symbols, and resource bounds are engineering problems to solve and verify on macOS arm64, Windows x86-64, and Linux x86-64. Historical failures under older toolchains are investigation inputs, not present-day impossibility evidence or a go/no-go gate. Qualify repeated reloads with bounded retained resources; routine accumulation of old images until restart is required is not the target experience.
  7. Integrate into the existing plan. Phase 9 implements and qualifies the first editor reload workflow with Phase 10 at C1. Phase 11 carries it across desktops at C2; C3 exercises later native consumers; Phase 15 measures and optimizes it at C4; Phase 16 requires final clean-machine evidence at C5. Concrete experiments settle interfaces and defects while implementation proceeds; they do not reopen whether the feature is worth doing. The implementation plan owns workloads, bounds, and exit criteria; the Editor/Build PRD owns behavior.
  8. Keep the stronger running-game feature separate. A launched or exported game still loads its native code for that process lifetime. Updating it uses stop/build/relaunch with explicit scenario reconstruction. Continuous native code replacement inside a running game and arbitrary private-state migration remain outside this decision. Editor reload requires no Odin ScriptLanguage, attachable Script resource, full IDE, or reload service in shipped games.

Consequences

Native editor reload becomes a required parity capability and an open release blocker until its named Gododin gate passes. Earlier compiler and binding measurements keep their narrow verdicts; they cannot compensate for this gap. At acceptance the SDK required restart; the implementation resolution below records the landed editor path and the qualification still outstanding.

The project accepts the necessary Host/bridge lifecycle, loader, build-watcher, editor integration, debugger, and desktop qualification work. Missing implementation or a failing platform case requires repair, not silent deferral. A material engine limitation gets an exact, source-backed resolution within the accepted parity scope; changing the product promise requires an explicit later scope decision.

Implementation resolution (2026-09-05, macOS)

Decision 4's open question is settled: the Host and project image both participate in the public extension lifecycle. Both enter teardown and reload; bounded actual residency remains subject to C1 qualification. The reload loop and its lifetime rules:

  • The generated .gdextension descriptor carries reloadable = true, and every registered class supplies recreate_instance. On reload, Godot snapshots stored properties that differ from class defaults, runs the shutdown path, frees each Odin instance through free_instance, reopens the library, and reconstructs every surviving Godot object through recreate_instance — a fresh Odin instance built by the authored constructor — then re-applies the stored properties through the generated setters. Tree-entry notifications are not replayed (BLD-RLD-004's contract).
  • The extension declares its minimum initialization level as Scene, at gdext parity; the engine refuses to reload extensions declaring Core. Shutdown and reload teardown still walk every level down to Core, so the Core-level drain and the zero-owner GODODIN_HOST_PASS finish remain in force per reload.
  • The image teardown requests release of the dyld handle after draining native owners. The image admits no initializer or terminator sections. This is the implemented release path, not proof of actual unmapping or a one-image bound; the follow-up below identifies the remaining failure accounting and residency evidence.
  • Editor sessions admit no per-build environment identity: the Host loads the image published beside its own dylib in the same immutable accepted publication and admits it by canonical path, Mach-O structure, and the compiled target/catalog fingerprint handshake. Runs and exports keep the exact size-and-sha256 artifact admission. Because host and image are always one publication, the reload pair cannot mix builds, and no environment fact has to change for a reload to load the current build.
  • The editor addon owns the loop (BLD-RLD-001): it scans the configured Odin roots from GODODIN_WATCH_ROOTS (covering roots outside res://, atomic saves, and dependency edits) on a 0.5 s signature scan with a 0.35 s debounce into the existing gododin build child; when the coordinator accepts a build, the addon runs gododin editor --project-root <root> --sync-only — the editor owner installing the rewritten descriptor through the owned addon — and then activates it with GDExtensionManager.reload_extension (or load_extension after a failed startup load). OK, NEEDS_RESTART, and FAILED are reported truthfully in the dock; a failed activation keeps the authored scene work and retries on the next accepted build. Reload identity, progress, and the loaded-build record stay in the dock and the build state; no second catalog, reload ABI, or public reload command exists.
  • The whole editor addon is excluded from source snapshots, packaging plans, and staged bundles: it is Gododin-owned tooling whose descriptor names the per-build Host by absolute path, never an authored input and never shipped.
  • Verified on the pinned 4.8-dev4 editor: successive automatic edit → build → sync → reload cycles in one editor process (unchanged PID), each with a zero-owner teardown of the previous build. The C1 named workloads (native-editor-reload-small/-scale, 30 successive reloads, property restoration, memory and owner bounds) remain pending evidence.

Qualification follow-up (2026-09-06)

The source-pinned Odin/Raylib template review refines implementation and qualification under decisions 2–6; it changes neither the public Godot reconstruction approach nor the running-game scope.

  • Keep immutable, complete Host/image publications as the activation boundary. Publication and descriptor-sync failures must be distinguished from successful activation; a completed child process alone proves neither.
  • Native teardown, loader-handle release, and actual library residency are separate observations. The reviewed implementation cleared failed-release handles; the resolution below preserves them and reports restart-required recovery. Neither zero native owners nor a successful loader close alone proves that old mappings are gone. The template's deliberate retention of earlier images is not the selected lifetime policy.
  • Exercise stored-property restoration through equal-size layout changes and incompatible type changes. Exercise cleanup/reconstruction of strings referring into image storage, native callbacks/connections, and allocator-backed state. No old Odin memory is reinterpreted as a new type; stale native entry is rejected before dispatch into retired code.
  • Carry build-specific PDB identities into Windows implementation. Emit the matching symbol reference during linking, preserve binary/symbol pairing, and qualify breakpoint rebinding and retirement with an attached debugger.

The Editor/Build PRD owns the resulting behavior and the plan owns C1 ordering, workloads, and measured bounds; its Phase 11 owns the Windows work at C2. These items remain pending implementation or execution as marked there. The source review itself adds no runtime evidence or support promotion.

Threaded retirement and handoff resolution (2026-09-06)

The live Node/Resource reconstruction fixture exposed two defects in the earlier implementation. Godot frees extension instances during class unregistration, so the Host cannot demand zero instances before that operation on reload. Resources occupy the catalog's Core level: verify the final drain after Core, and restore Core registrations before Scene when reload resumes at the declared Scene minimum. Ordinary process shutdown keeps its existing owner checks.

Successful dlclose still left both old libraries mapped. The locked Odin runtime emits native TLS even for ordinary context and core:os use. Apple's dyld policy marks images with MH_HAS_TLV_DESCRIPTORS as never unloadable. This explains the observed retention; it is not a reason to defer reload or disable threading.

The configured macOS build now uses the locked Odin compiler's LLVM IR output and the matching upstream LLVM llc -emulated-tls mode. Authority SDK packaging includes the small emitter, which uses the already authenticated managed LLVM library; gododin.lock owns its identity. Odin sources, thread-local semantics, assertion checks, and compiler debug information are retained. The debug link stage produces adjacent dSYM bundles before staging objects disappear; publication records each binary's matching symbol UUID and digest. Attached-debugger qualification remains C1. -no-thread-local is not used. This adds a compilation tool, not a game/runtime dependency or compiler fork.

runtime/thread_storage implements LLVM's private emulated TLS contract per library. Each thread has separate aligned storage. Exiting threads run Odin's registered arena cleaners and release their blocks. After callbacks/workers drain, library retirement also cleans storage belonging to surviving engine threads, deletes the pthread key and its destructor, and releases the mutex. Host and image invoke runtime startup/cleanup explicitly; neither depends on loader constructors/destructors. The default allocator remains nil until the existing owner supplies it. Native build validation rejects residual native TLS, and editor image admission rejects the dyld retention flag before loading. No Host/image ABI field or general worker authority is added.

The CLI verifies the accepted Host/image/descriptor/catalog digests and publishes the descriptor without first removing the live file. The addon requires a successful sync exit and matching selection, then records the Host that actually initialized. It rejects equal-width stored-property type changes before engine activation. A catalog alias cannot rename Godot's name-keyed saved property data; public-name changes with declared continuity are diagnosed before activation. Content hashing catches rapid equal-size saves; pending edits survive an in-flight build. Loader-release failure retains its handle and identity, pins the Host, and latches restart-required recovery before another generation can activate. If the process cannot retain the Host or establish that latch, it terminates with a diagnostic instead of returning into invalid callbacks. The sync sentinel reports selection, not loaded code.

Focused evidence and commands live with the reload fixture. The isolated TLS fixture has executed 30 actual unmaps with three concurrent callers, two worker threads surviving across unloads, and a thread-exit case per cycle. The frozen lifetime receipt (repository: fixtures/native_components/reload-evidence.json) records 31 successful reloads each with one and 64 Node/Resource pairs, one unchanged editor PID per workload, and one settled Host/image pair throughout. Retained RSS growth remained below 14 MiB against the predeclared 128 MiB bound; full mapping snapshots, publication identities and symbol logs are retained. These automated runs shared compiler resources and do not qualify iteration speed.

The current compositor's focused renderer, recovery and accepted editors, game run, and both executed exports passed on the same native profile with the unchanged inspected PNG and zero owners. That matrix also exposed and fixed recovery-flag parsing and removal of the developer addon's setting from private run/export copies; the immutable accepted settings stay unchanged. Its receipt (repository: fixtures/compositor/phase4-evidence.json) records the scope and the completed release-application continuation after a harness-only toolchain comparison correction.

The installed starter/Cavern Lab workloads, debugger-attached qualification, compositor reload interaction, shared SDK batch, and desktop gates remain required C1/C2 work. These focused results do not close parity or promote platform support.