Scene & Resource Serialization
Purpose
Section titled “Purpose”Scenes and shared data are stored as text files with stable UIDs, so they can be loaded, instanced, diffed in git and edited by the editor:
*.mscene— a node tree with property values, nested scene instances with overrides, persistent groups and signal connections (PackedScene).*.mres— a standaloneResource(skies now; materials, meshes, shapes, themes later).
Property access is source-generated: MainframeEngine.Generators emits a NodeTypeInfo (factory,
typed getter/setter delegates, hints, signals, migrations) for every node and resource type, so loading
and saving use no reflection. Decisions: JSON scenes, no binary bake,
source-generated type registry,
scene format 2.
Key types
Section titled “Key types”| Type | File | Notes |
|---|---|---|
[Export], [ExportGroup], [Signal], [SignalHandler], [Tool], [TypeName], [SerializedVersion], [SerializedMigration] | Serialization/Attributes.cs | namespace MainframeEngine |
TypeRegistry, NodeTypeInfo, ExportPropertyInfo<TOwner,TValue>, SignalInfo, MigrationInfo, ExportHints | Serialization/ | namespace MainframeEngine.Serialization |
ValueCodec<T>, Codecs, PropertyBag | Serialization/Codecs.cs, PropertyBag.cs | JSON value encodings; migration input |
SceneWriter, SceneDocument, ResourceTable, PropertyApplier | Serialization/ | internal reader/writer |
Resource, PackedScene, ResourceLoader, SceneSaver, ResourceSaver | Resources/ | |
JsonMigration, JsonMigrations, DurableFile | Serialization/JsonMigration.cs, DurableFile.cs | the version upgrade loop of JSON documents (project.mfproj, save games, settings); crash-safe file replacement |
AssetDatabase, AssetMeta, AssetIndex, AssetUid | Resources/ | UIDs ↔ paths |
NodeRegistrationGenerator | MainframeEngine.Generators/ | Roslyn incremental generator (netstandard2.0) |
ConfigFile | Serialization/ConfigFile.cs | Godot’s ConfigFile over JSON: player settings and other key/value files (below) |
Property model
Section titled “Property model”public partial class DirectionalLight3D : Light3D // `partial` is not required{ [Export] public Vector3 Color { get; set; } = Vector3.One; [Export(Range = "0,16,0.01")] public float Energy { get; set; } = 1f; [ExportGroup("Shadow")] [Export] public bool CastsShadows { get; set; } = true; [Signal] public event Action<Node3D>? BodyEntered;}[Export]marks a property or field as serialized and inspector-visible. It must be readable and writable from the declaring assembly (public or internal; no init-only setters).- Hints:
Range = "min,max[,step]",File = "*.png,*.jpg",Directory,Multiline,Flags,NodeType(forNodePath),Translatable(player-facing text onstring/string[]/List<string>: extracted bymf-l10n, shown withNode.Atr, stored as source text; see Localization).[ExportGroup("Name")]starts an inspector group for this and the following members (empty name ends it). - Supported types:
bool, integer and floating-point primitives,string, enums,Vector2/3/4,Quaternion,System.Drawing.Color,Transform3D,Transform2D,NodePath,Resourcesubclasses (inline or external), and arrays /List<T>of these. Anything else is a compile error (MFG002). [TypeName("...")]overrides the scene type name (default: the class name; names must be unique across loaded assemblies).[Tool]marks nodes that process in the editor.[EditorIcon("tabler-name", Family = EditorIconFamily.Space3D)]on a type gives it an editor icon and colour family (types without one inherit their nearest base type’s;""keeps the icon and sets only the family);[Export(Icon = "…")]or[EditorIcon("…")]on a member sets its inspector icon. The XML doc<summary>of types and exported members becomes the editor’s Add Node description and inspector tooltip. See Editor: icons.
Source generator
Section titled “Source generator”MainframeEngine.Generators runs on every project that references it as an analyzer (the engine, the
unit tests, every game such as the Demo):
<ProjectReference Include="..\MainframeEngine.Generators\MainframeEngine.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />For each class deriving from Node or Resource (generic and file-local types are skipped) it emits, in
one TypeRegistration_<Assembly> class, a [ModuleInitializer] that calls TypeRegistry.Register with:
- the type name, CLR type and base type, and a factory
static () => new T()(none for abstract types or types without an accessible parameterless constructor); - an
ExportPropertyInfo<TOwner, TValue>per member:static o => o.Member/static (o, v) => o.Member = vand a codec chosen at compile time (Codecs.Get<float>(),Codecs.EnumOf<T>(),Codecs.ResourceOf<T>(),Codecs.ListOf(...),Codecs.ArrayOf(...)), hints and group; - a
SignalInfoper[Signal]event: delegate type, parameter types, typed add/remove accessors and a forwarder factory for deferred/one-shot connections; MigrationInfos, the serialized version and traits (node/resource/tool/abstract);- editor metadata: the type’s
[EditorIcon]name and family (NodeTypeInfo.Icon/IconFamily, declared only — the editor resolves inheritance) and its doc summary (Description); per memberExportHints.IconandExportHints.Description. Summaries are read from the///trivia (no documentation file needed), stripped to plain text (<see cref>→ the name, tags dropped, at most 600 characters);<inheritdoc/>gives none.
The pipeline is incremental: per-type models are value-equatable records, so edits that do not change a
type’s shape do not regenerate. Diagnostics (all errors): MFG001 inaccessible/unwritable export, MFG002
unsupported type, MFG003 bad signal event, MFG004 duplicate type name, MFG005 invalid migration method,
MFG006 a type with exports/signals that cannot be registered (e.g. nested in a private type), MFG010
Translatable on a member that is not text.
Extension point. Models (TypeModel in Models.cs) carry
one collection per member feature; TypeModelBuilder fills them and each feature has its own emitter. M5 added
Replicated and Rpcs (from the same member walk) and ReplicationEmitter, which writes a second file,
MainframeEngine.Replication.g.cs, only for assemblies with networked members, without touching registration
(diagnostics MFG007–MFG009; see Networking: replication).
TypeRegistry and NodeTypeInfo
Section titled “TypeRegistry and NodeTypeInfo”TypeRegistry.Get(name | Type),GetNearest(Type)(nearest registered base),All,CreateNode(name),Changedevent. A lookup miss runs the module initializers of loaded assemblies that reference the engine (registration happens on first access to an assembly);EnsureRegistered(assembly)forces it.UnregisterAssembly(assembly)before unloading a game assembly; the reloaded assembly registers again (a same-named type from a newer load replaces the old one). This is the editor’s code-reload path.NodeTypeInfo.Properties/Signalsinclude base types (base first),FindProperty,FindSignal,CreateInstance(), andDefaultInstance— a pristine instance (created once, never added to a tree) that defines “default” for saving.ExportPropertyInfooffers boxedGetValue/SetValuefor tools (the inspector) andValueEquals/CopyValue; serialization uses the typed path.
File format
Section titled “File format”Format 2 (ADR 0102):
{ "format": 2, "uid": "scn_491645bea2fa", "resources": { "Sky_5kj71": { "type": "Sky", "props": { "Mode": "Panoramic", "Panorama": "Content/Sky/sky_10_2k.png" } }, "StandardMaterial3D_7b21a": { "ref": "res_7b21aa90c1d4", "path": "Content/Materials/Crate.mres" } }, "nodes": [ { "name": "Main", "type": "Node3D" }, { "name": "Environment", "parent": ".", "type": "WorldEnvironment", "props": { "Sky": { "res": "Sky_5kj71" } } }, { "name": "Sun", "parent": ".", "type": "DirectionalLight3D", "props": { "RotationDegrees": [-26.56506, 0, 0], "Color": [1, 0.95, 0.8], "Energy": 0.8 } }, { "name": "Player", "parent": ".", "instance": "scn_a12b77d03e9f", "path": "Content/Scenes/Player.mscene", "props": { "Position": [0, 0, 2] }, "overrides": { "Hitbox": { "Radius": 0.5 } } }, { "name": "Badge", "parent": "Player/Hitbox", "type": "Node3D" }, { "name": "Hat", "parent": "Player", "type": "Node3D" } ], "connections": [ { "from": "Player/Hitbox", "signal": "BodyEntered", "to": ".", "method": "OnPlayerHit", "flags": "Deferred" } ]}- Written with
Utf8JsonWriter(UTF-8, non-ASCII unescaped), then laid out bySceneJsonLayout(SceneFormat.FormatJson): two-space indent and\non every OS, one member per line, except arrays of up to 16 scalars (vectors, colours, transforms, group names) and resource references ({ "res": "…" }), which stay on one line. Read withJsonDocument(comments and trailing commas tolerated for hand edits). The.metasidecars and the runtime index use a source-generatedJsonSerializerContext. - Only non-default values are written: a property is skipped when it equals the type’s
DefaultInstance(codec equality; resources by reference). - Values: vectors, quaternions, colors (RGBA floats 0..1) and transforms (column vectors + origin) are
arrays; enums are names (flags
"A, B");NodePathis a string; non-finite floats are strings ("NaN","Infinity"); resources are{"res": "key"}into the table,nullwhen unset. nodesis a flat list in tree order (parents before children, siblings in order): the root first, then every node owned by the scene root. An entry isname,parent(path from the root,"."for the root’s children; the root has none),type, optionalv(type version when not 1),propsandgroups(persistent only). Reparenting a node changes its ownparentline (and its descendants’), not the indentation of a subtree.- Nested instances store the sub-scene’s UID (
instance) with the path as a hint, the instance’s name and parent, its root properties that differ from a pristine instance of the sub-scene (props), andoverrideskeyed by path inside the instance (nodes owned by the instance or by instances nested in it). Nodes this scene added inside an instance are listed like any other node, theirparentpath running through it ("Player/Hitbox"). Changing a sub-scene therefore flows into every instance that did not override the value. The instance’svandoverrideVersions(path → version, when not 1) record the type versions the values were written with, so they migrate like any other entry. Inline resources are compared by value when diffing, so an instance never records an “override” that only differs by resource identity. - Resources: inline resources (no file) are written once even when referenced many times; external ones
(
ResourceSaver.Saved) areref(UID) +path(hint). An inlinePackedScenecannot be referenced (save it first). Table keys areType_xxxxx(five base-36 digits) and stable: an inline resource remembers the key it was loaded with (Resource.SceneLocalId, Godot’sresource_scene_unique_id) and keeps it on every save, so adding or removing a resource never renames the others. A new resource’s key is hashed (FNV-1a) from where it is first used (node path and property, or the referencing resource’s key and property), so saving the same tree twice writes the same bytes; an external reference’s key is hashed from its UID. Collisions within a file are re-hashed. The table is in discovery order. - Format 1 files (
"root"with nested"children", resources keyed"1","2"… in discovery order, children added inside an instance listed under it withparentrelative to the instance) still load; their numbered keys are not kept, so the next save writes format 2 with fresh keys. - Connections: persisted (
ConnectFlags.Persist) connections between nodes of this scene, made in it (not in a sub-scene’s file), with paths relative to the root; extra flags as"flags". - Unknown types load as
MissingNode/MissingResource, keeping the type name, version and raw properties, and are written back unchanged (their children load normally). Resources their raw properties reference ({"res": key}) are resolved and re-keyed into the new file’s table, so they survive a re-save (M3). - Removed engine types (
RemovedNodeTypes, M3) load as their replacement instead:Box3d/QuadbecomeMeshInstance3Ds with a primitive mesh and a material carrying their colour; saving writes the new type. Games can register upgrades for their own retired types (a type-level counterpart of[SerializedMigration]). - Files other than
.mscene/.mres(images, models) are imported — see Asset pipeline; a scene instancing a model stores its UID + path like any nested scene. Unknown properties are ignored with a warning; a bad value throwsInvalidDataExceptionnaming the file, node and property.
Loading and instancing
Section titled “Loading and instancing”flowchart LR
F["*.mscene"] -->|"ResourceLoader.Load (cached by UID)"| PS["PackedScene (parsed document)"]
PS -->|"Instantiate()"| T["new node tree: factories + generated setters"]
T --> R["resources: inline (shared per PackedScene) / external (ResourceLoader)"]
R --> N["nested instances → Instantiate recursively, apply props + overrides"]
N --> O["owners assigned, persistent groups added"]
O --> C["persisted connections bound (OriginScene = root)"]
C --> A["caller: parent.AddChild(root) → EnterTree / Ready"]
PackedScene.Instantiate()builds the tree outside the scene tree: no lifecycle callbacks run until the caller adds it. Nodes from the file getOwner= scene root (inner nodes of a nested instance keep the instance root as owner); the root’sSceneFilePathis the scene file. Self-instancing (directly or through a nested scene) is rejected.PackedScene.Pack(root)(Godot’spack) andPackedScene.Parse(json)create in-memory scenes;Instantiate<T>()casts the root.ResourceLoader.Load<T>(pathOrUid)loads.mscene/.mresfiles, caches them by UID (and path) and adds a reference;Resource.Release()drops it and the last release evicts the resource (OnUnloaded, once, which also releases the external resources it loaded).ClearCache()runs at engine shutdown. APackedSceneholds one reference on each external resource and sub-scene it uses — including those of content it had before a re-save, which live instances may still use — until it unloads.SceneTree.ChangeSceneToFileholds the scene’s reference while the scene is current.ResourceLoader.LoadThreaded<T>(pathOrUid)(Godot’sload_threaded_request) loads on the thread pool and returns aResourceLoadTask<T>: pollStatus(InProgress,Loaded,Failed), takeResult(the reference is the caller’s;Disposereleases one nobody took). The cache is thread-safe and shared withLoad.
Asynchronous loading (ADR 0183)
Section titled “Asynchronous loading (ADR 0183)”SceneTree.ChangeSceneToFileAsync(pathOrUid, SceneLoadOptions?) changes the scene without blocking the main loop and
returns a SceneLoad. Nodes are cheap and side-effect free and Instantiate builds outside the tree, so most of the
work runs on a worker thread:
| Stage | Thread | Work |
|---|---|---|
Loading | worker | ResourceLoader.Load<PackedScene> (and its external resources) |
Instantiating | worker | Instantiate(), outside the tree |
Preparing | worker | each ISceneLoadable in the scene (depth-first): LoadInBackground(SceneLoadProgress) |
Decoding | thread pool | ImagePrefetch: the images of the textures created so far, and their coverage mip chains |
Entering | main | the scene replaces the current one at the start of a tick (its OnReady run; the tree holds the loader reference) |
WarmingUp | main | WarmUpFrames (default 2) frames rendered behind the loading screen, until every loadable’s PollLoaded is true |
Done / Failed / Canceled | — | Completed is raised; Error holds a failure |
ISceneLoadable(both members optional):LoadInBackgroundbuilds whatOnReadywould — child nodes, meshes, CPU data — while the node is outside the tree; never touch the tree, servers, Vulkan, RmlUi, physics or audio there.PollLoadedruns once per frame after the scene entered, for work that needs frames (GPU warm-up). The synchronous path never calls either, soOnReadymust still do the work whenLoadInBackgrounddid not run.LoadWeightsets the node’s share of the bar. Engine helpers for loadables:Terrain3D.BuildNow()(chunks, collision, water, foliage tiles) andTreeScatter.Rebuild(progress)(variants, impostors, batches) build before the node enters the tree, and their ready then skips the build.- Progress (
Progress, 0–1, never back) weights the stages: loading 6 %, instantiating 4 %, preparing 55 % (split byLoadWeight, each loadable reporting through itsSceneLoadProgress.Report(fraction, stage)), decoding 12 %, entering 5 %, warming up 18 %.StageTextis the newest label of a loadable still at work, else the stage’s own. ImagePrefetch(internal): while a load runs, textures created on its flow (Texture2D.FromFile, andFromPixelswithPreserveAlphaCoverage) queue their decode / mip chain on the thread pool;Texture2D.DecodePixels(a copy), the GPU upload and terrain layer packing (shared, read-only) find them ready. Held until the load completes, then dropped.- Cancellation:
SceneLoad.Cancel(), a newerChangeScene*/UnloadCurrentSceneorShutdowncancel a load in progress; the worker stops at its next check (SceneLoadProgress.CancellationToken) and what it built is freed on the main thread. Errors (a missing file, a loadable that throws, anOnReadythat throws while entering) end inFailedwith the current scene untouched (or the half-entered one). SceneTree.CurrentLoadis the load in progress;SceneTree.IsLoadingis true while one runs or aLoadingScreenis shown. Tests:SceneLoadTests(stages and monotonic progress, a worker-built loadable, the loop ticking while a loadable blocks, sync/async equivalence of the saved and live trees, failures, cancellation, replacement, hold,LoadThreaded),ImagePrefetchTests,LoadingScreenTests; the Forest’sValleyTestscompare its valley built both ways node for node.- References from files resolve by UID first; the path is only a hint, so a moved file still loads (with a warning when the UID is unknown but the hint exists).
Saving
Section titled “Saving”SceneSaver.Save(root, path)writes the nodes owned byroot(runtime-spawned children are skipped), nested instances as references + overrides (diffed against a freshly instantiated pristine copy), the resources they use and the persisted connections. The file’s UID is kept across saves (read from the existing file or the asset database; a newscn_UID otherwise), the database is updated and a cached copy of the scene picks up the new content. Writes go throughDurableFile(below).SceneSaver.ToJson(root, uid)serializes without touching disk.- Before writing,
SceneSaver.Savecalls the internalISceneSaveHook.OnSceneSaving(scenePath)on every node under the root that the scene or one of its instances owns (runtime children without an owner, such as a[Tool]node’s generated terrain, are skipped with their subtrees: they are not saved, ADR 0169). Nodes with data of their own write it there: aTerrain3Dsaves its layer images andterrain.mresinto<scene>_terrain/. A throwing hook fails the save before the scene file is written.ToJsonruns no hook. ResourceSaver.Save(resource, path)writes a.mres(format,uid,type,v,resources,props) and makes the resource external (ResourcePath,Uid), so scenes reference it from then on.
Durable writes (DurableFile)
Section titled “Durable writes (DurableFile)”DurableFile.Write(path, bytes, backups) replaces a file so that a crash or power loss at any point leaves a complete
copy. ResourceSaver.WriteAtomically uses it with no backups for .mscene, .mres and project.mfproj; save games
use it with rolling backups (Project and GameHost → Save games).
- Write
path.tmpwith aFileStreamandFlush(flushToDisk: true)(fsync/FlushFileBuffers). - First write (no target yet), or no backups: move the temp file over the target.
- Otherwise rotate
.bak{N-1}→.bak{N}… (the oldest falls off), thenFile.Replace(tmp, path, path.bak1).
On Windows the replace is ReplaceFileW (atomic); on Unix it is a hard link plus a rename. At every moment the target,
.bak1 or a fully flushed .tmp is complete, so a reader that validates its data tries RecoveryCandidates(path, backups) in that order (target, .tmp, .bak1 … .bakN), puts the first good one back with Restore, and moves a
damaged target aside with QuarantineCorrupt (path.corrupt-{utc}, never deleted). .NET cannot fsync a directory,
so a rename can still be lost on power failure on some file systems; the same recovery order covers it. The steps run
through an internal IDurableFileOps shim, which the tests use to simulate a crash between any two of them.
UIDs and the asset database
Section titled “UIDs and the asset database”- UIDs are a kind prefix and 12 lowercase hex digits (
scn_,res_,tex_,aud_,mdl_,ast_); 8–16 digits are accepted. 48 bits keeps accidental collisions across branches negligible (the original sketch used 32). - Scene and resource files carry their own
uid. Other assets get a<file>.metasidecar (AssetMeta:uid,importer,settings), created by the editor (Scan(createMissingMeta: true)). AssetDatabasemaps UID ↔ project-relative path (/separators).Refresh()readsContent/assets.index.jsonwhen present (shipped builds;WriteIndex()produces it) and scansContent/otherwise;AssetDatabase.Current(rooted atContentPaths.BaseDirectory— the app folder, never the working directory, since M3) fills itself lazily on the first UID lookup.Move(from, to)moves a file and its meta and updates the map.
Versioning
Section titled “Versioning”- File format:
format(currently 2). Newer formats are rejected; older ones load (format 1 differs in layout only:SceneDocument.Parsereads both). - Other JSON documents (
project.mfproj, save games, settings) share one upgrade loop:JsonMigrations.ReadVersionreads the version key (missing means 1; newer than the build or below 1 throwsInvalidDataException), thenJsonMigrations.Upgrade(root, version, current, migrations, source, what)runsJsonMigration(From, Action<JsonObject>)steps one at a time on the JSON tree, and only then is the tree read into the typed object; a missing step throws naming the file andwhat(“save version”). Saving always writes the current version. Steps are static lambdas overJsonObject: no reflection, trim- and AOT-safe.ProjectSettingsFormat.Upgradeand itsProjectMigrationare a thin wrapper over it. Save games carry two numbers:format(the engine’s container, engine migrations) andversion(the game’s payload, the game’sSaveSchemamigrations). - Types:
[SerializedVersion(n)]sets a type’s layout version (default 1); entries storevwhen it is not 1. On load,[SerializedMigration(from)]methods (static void M(PropertyBag)) upgrade older data step by step (rename, convert or drop properties) before it is applied; saving writes the current version. Example:MigratedNodein the unit tests renamesVelocity→Speed(v1→v2) and widens a scalarSizeto a vector (v2→v3). - Moving properties into a new sub-resource.
PropertyBag.SetInlineResource(name, typeName, bag)stores{"type": …, "props": {…}}as a value; the resource codec reads that shape as a new inline resource of the file (DeserializationContext.CreateInlineResource, whose{"res": …}references resolve against the file’s table). Only migrations produce it: the next save writes an ordinary sub-resource.WorldEnvironmentv1 → v2 (ADR 0169) moves its tonemap, auto exposure, glow, light shafts, SSAO and adjustment keys (PostProcessProfile.MovedPropertyNames) into an inlinePostProcessProfileonPostProcess; an entry without them gets none. Instance overrides migrate the same way (a v1 override of a post key replaces the instance’s profile with an inline one).
Config files (ConfigFile)
Section titled “Config files (ConfigFile)”ConfigFile is Godot’s ConfigFile (sections holding keys) for options and other small key/value data; the player’s
settings.json (UserSettings) is one. Two deviations from Godot: the file is
JSON (one parser, the same JsonMigrations, readable by every tool) and values are typed through overloads
(GetBool/GetInt/GetDouble/GetString/GetStrings, SetValue(…)) instead of a Variant, so reading a number
never boxes. Structured values go through the game’s source-generated JsonTypeInfo<T> (Get<T>/Set<T>): no
reflection, trim- and AOT-safe.
{ "format": 1, "version": 1, "game": { "subtitles": true, "fov": 80 }}- Each section is a top-level object;
format(the engine’s container layout,ConfigFile.CurrentFormat) andversion(the game’s own,ConfigFile.Version; 0 = not versioned, not written) are reserved. Comments and trailing commas are tolerated (SceneFormat.ReadOptions); a top-level value that is not an object is dropped with a warning. Writing is UTF-8, LF, indented. - Never crashes on player edits. A value of the wrong type returns the caller’s default and logs one warning per key (writing the key forgets it). Erasing a section’s last key erases the section, as in Godot; a null string or structured value erases the key.
- Load (
ConfigFile.Load(path, currentVersion, migrations)): a missing file is an empty config. An unparseable file is moved aside aspath.corrupt-{utc}(DurableFile.QuarantineCorrupt), thenDurableFile.RecoveryCandidates(a flushed.tmp, then.bak1) are tried and the first valid one is restored as the file; failing that the config starts empty. A newerformator gameversionis never moved or rewritten: the config starts empty with an error logged, and the newer content survives as the backup of the next save.ConfigFile.Parseis the strict variant and throwsInvalidDataException. - Versions. With
currentVersion> 0, an olderversion(missing = 1) runs through the game’sJsonMigrationsteps (JsonMigrations.Upgrade) before any value is read, andVersionbecomescurrentVersion; with 0 the file’s ownversionis kept as is. Engineformatsteps work the same way (none yet). - Save writes through
DurableFile.Writewith one backup (.bak1): temp file, flush to disk, replace. - The values live in a
JsonObjecttree: meant for options, not per-frame reads (callers cache values).
Performance
Section titled “Performance”Saving and loading run at load time and may allocate. Measured with BenchmarkDotNet
(baseline.json): SceneSaveLoadRoundTrip1k
(serialize, parse and instantiate a 1 000-node scene).
Game value types
Section titled “Game value types”A game struct marked [SerializableValue] may be an [Export] type (or an array/list element) once its codec is
registered (Codecs.Register(Codecs.FloatArray<T>(...)), before anything loads). Generated registration uses
Codecs.Deferred<T>(), which looks the codec up when a value is read or written, so a module initializer of the game can
register it in any order. Crash Site Defense uses this for its copies of Godot’s math structs (Godot.Vector2, Color, …).
Known issues
Section titled “Known issues”- Renaming a node inside a sub-scene breaks overrides that target it by path.
- Generator models keep source spans (for diagnostics), so an edit above a node type in its file regenerates the registration file (still correct; only the incremental cache is missed).
- Saving a node instantiated from an in-memory
PackedScene(no file) as part of another scene writes only the instance root: its inner nodes are owned by the instance, not the saved scene. - Connection binding uses reflection (
Delegate.CreateDelegate,MethodInfo.Invokefor deferred/one-shot forwarders); fine for load time, not trimming-safe yet. - Resource reference counting covers loader references only; nodes do not add or release references when a resource property changes.
Related docs
Section titled “Related docs”Scene graph & nodes · Demo · Testing · Editor · Networking: replication · Shaders