[{"id":"M4743f5","fact":"ERuntimeVirtualTextureMaterialType has NO Mask4 or Displacement values in 5.3.2 (its values are BaseColor, BaseColor_Normal_DEPRECATED, BaseColor_Normal_Roughness, BaseColor_Normal_Specular, BaseColor_Normal_Specular_YCoCg, BaseColor_Normal_Specular_Mask_YCoCg, WorldHeight, Count), and the enum lives under Public/VT/ not Classes/VT/.","source":"Engine/Source/Runtime/Engine/Public/VT/RuntimeVirtualTextureEnum.h:34-44 (enum body). Classes/VT/ exists but holds RuntimeVirtualTexture.h etc., not this enum header","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE 5.3.2 fork sharp edges"},{"id":"Mc3fe82","fact":"UCurveTable::AddRichCurve hard-check()s that the table is not in SimpleCurves mode (an editor CRASH, not an error), and UCurveTableFactory::MakeNewCurveTable pre-seeds a stray \"Curve\" row — in SimpleCurves mode whenever ConfigureProperties was skipped, because InterpMode is never initialised in the factory ctor so it is zero (RCIM_Linear) and takes the AddSimpleCurve branch. Call EmptyTable() immediately after creation and read GetCurveTableMode() before any row op.","source":"Engine/Source/Runtime/Engine/Private/CurveTable.cpp:554-557 (AddRichCurve :554, check :556; the mirror check in AddSimpleCurve at :575) — CORRECTION: the path is Private/CurveTable.cpp, NOT Private/Curves/CurveTable.cpp. Factory: Editor/UnrealEd/Private/Factories/CurveTableFactory.cpp:63-77 (exact) with the ct","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE 5.3.2 fork sharp edges","value":"`check(CurveTableMode != ECurveTableMode::SimpleCurves);`"},{"id":"Mc737ae","fact":"'The editor is closed' is not the invariant UBT tests - three processes must be gone: UnrealEditor.exe, UnrealEditor-Cmd.exe (a mod cook runs under this name and holds the plugin DLLs) and LiveCodingConsole.exe (holds a mutex UBT checks independently and survives the editor exiting). Aborting a UE build leaves partial intermediates: one abort cost a following 2,673-action rebuild of work that should have been ~30 actions.","source":"docs/01_POSTMORTEMS.md PM-008 (:1096-1170); docs/00_ARCHITECTURE.md § Build (:446-472). The doc adds that there is no marker saying a build was aborted - the only tell is the next build's action count.","engine":"","ue":[],"bl":[],"section":"UE build","value":"`Get-Process UnrealEditor,UnrealEditor-Cmd,LiveCodingConsole -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,MainWindowTitle`"},{"id":"M5ed41e","fact":"CORRECTED TO FOUR, NOT FIVE. Four cooked-asset operations take the editor DOWN rather than returning an error: UUserDefinedStruct editor entry points (CastChecked on null EditorData), duplicating a cooked UNiagaraSystem (access violation in FVersionedNiagaraEmitterData::PostLoad), duplicating a cooked UStaticMesh, and generating simple collision on one. list_material_expressions is the fifth row of the same table and does NOT crash - it honestly reports numExpressions:0, cooked:true.","source":"`UStaticMesh duplicate: Assertion failed: Owner->IsMeshDescriptionValid(0) at StaticMesh.cpp:3086; simple collision: EXCEPTION_ACCESS_VIOLATION reading 0x50, GeomFitUtils.cpp GenerateBoxAsSimpleCollision dereferencing GetMeshDescription(0) with no null check`","engine":"docs/02_GOTCHAS.md §6c - its own table has five rows of which the UMaterial row is a clean report, so the doc's own summary line 'All five take the editor down' contradicts its table. Engine StaticMesh.cpp:3086 verified verbatim on 5.3 as `checkf(Owner->IsMeshDescriptionValid(0), TEXT(\"Bad MeshDescription on %s\"), ...) |","ue":["5.3"],"bl":[],"section":"UE cooked assets"},{"id":"M83d891","fact":"What still works on cooked assets: reflection reads (get_property, list_object_properties, including deep paths), cached runtime tables such as FMaterialCachedParameters (which is why list_material_parameters works where expression listing cannot), the runtime BlueprintCallable surface via add_function_call, and remove_collision (it touches only BodySetup/AggGeom, never MeshDescription, so it needs no guard).","source":"docs/02_GOTCHAS.md §6c § What still works on cooked assets (lines 823-833); the remove_collision row is explicitly noted as confirmed live rather than inferred from the shared 'collision code' framing","engine":"","ue":[],"bl":[],"section":"UE cooked assets"},{"id":"M2bf912","fact":"UMaterialExpression is UCLASS(abstract, Optional, ...) and UMaterial's expression collection lives in the equally-Optional UMaterialEditorOnlyData; CLASS_Optional means the class's data is saved SEPARATELY from other object types (sidecar/optional segment), which is why material-graph endpoints cannot rely on reaching a cooked base-game material.","source":"Engine/Source/Runtime/Engine/Classes/Materials/MaterialExpression.h:183-184; Materials/Material.h:309-310; flag semantics at Runtime/CoreUObject/Public/UObject/ObjectMacros.h:198 (\"Optional class data is saved separately to other object types\"), harvesting at CoreUObject/Private/UObject/SavePackage/PackageHarv","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE cooked content","value":"`UCLASS(abstract, Optional, BlueprintType, hidecategories=Object, MinimalAPI) / UCLASS(MinimalAPI, Optional)`"},{"id":"Mdc9793","fact":"Cooked textures carry no FTextureSource, so compression-setting changes and mip regeneration are impossible on pak-mounted textures — the engine's own header says to always check Source.IsValid before using Source.","source":"Engine/Source/Runtime/Engine/Classes/Engine/Texture.h:1092-1096 (WITH_EDITORONLY_DATA guard at :1092, comment :1093-1094, FTextureSource Source :1096)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE cooked content","value":"`/* Dynamic textures will have ! Source.IsValid() ; … Always check Source.IsValid before using Source. */`"},{"id":"Md75bef","fact":"FFoliageInfo::Instances is editor-only data serialized only when !Ar.ArIsFilterEditorOnly, so cooked InstancedFoliageActors load with EMPTY instance arrays — base-game foliage instances cannot be seen or removed. The FoliageInfos type map itself does survive.","source":"Engine/Source/Runtime/Foliage/Public/InstancedFoliage.h:275-296 (Instances declared at :284 inside WITH_EDITORONLY_DATA); Private/InstancedFoliage.cpp:503-514 (the guarded serialize) and :4386 (`Ar << FoliageInfos;`, unguarded)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE cooked content","value":"`#if WITH_EDITORONLY_DATA if (!Ar.ArIsFilterEditorOnly && !(Ar.GetPortFlags() & PPF_DuplicateForPIE)) { … Ar << Info.Instances; }`"},{"id":"M041366","fact":"FNameMap::GetName and TryGetName assert that a mapped name's type matches the map's type. NARROWED: this file proves the assert exists, NOT the loose-uncooked-asset-shadows-a-cooked-package causation that was attached to it - that causation is only supported by the separately measured NumCells 0-vs-2233 signature.","source":"Engine/Source/Runtime/Core/Public/Serialization/MappedName.h:128 (GetName) and :136 (TryGetName) - note the path is Runtime/Core/Public/..., not Core/Public/...","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE cooked content","value":"`check(MappedName.GetType() == NameMapType);`"},{"id":"Mcff6cc","fact":"CORRECTED COUNT - the table has eleven rows and TEN were misguarded (the eleventh, Misc/StringOutputDevice.h splitting out at 5.7, was guarded at 5.7 and is marked correct). Measured across five engines on 2026-09-08: FHttpRequestHandler TFunction->TDelegate, the GC keep-flags enumerator, UPCGNode::GetNodeTitle, UIKRigController::SetSkeletalMesh and MVVM AddViewModel all actually changed at 5.4; EAutomationTestFlags scoping, LandscapeProxy::AddTargetLayer, MVVM bAllowEventConversion and the Landscape Layers rebuild","source":"docs/02_GOTCHAS.md §14 § READ THIS FIRST (2026-09-08), lines 1713-1745, read from each engine's own headers. Spot-verified during this pass: FHttpRequestHandler is a TFunction typedef on 5.3 (Runtime/Online/HTTPServer/Public/HttpRequestHandler.h:19) and already a TDelegate alias on 5.4 - confirming the 5.4 boundary, no","engine":"","ue":[],"bl":[],"section":"UE engine versions","value":"`spell guards MIF_ENGINE_AT_LEAST(major, minor), never long-hand ENGINE_MINOR_VERSION >= N (the long-hand form hides from a sweep grepping for the macro)`"},{"id":"Md68dbf","fact":"UIKRetargetProcessor::Initialize takes (mesh, mesh, asset, bool) on 5.3/5.4, inserts a const FRetargetProfile& fourth on 5.5, and is rewritten entirely on 5.6/5.7 - a three-branch case no single boundary reaches, and 5.4 and 5.6 compile happily on either side of it, so probing only the ENDS of a claimed range finds nothing.","source":"docs/02_GOTCHAS.md §14 § AND ONE VERSION BELONGED TO NEITHER ARM (lines 1739-1742)","engine":"","ue":[],"bl":[],"section":"UE engine versions"},{"id":"M282496","fact":"The engine-version trap runs in six directions and only two are findable by reading headers: (A) 5.3 has it and the newer engine deleted it, (B) the newer engine has it and 5.3 never did, (C) both have it and the newer compiler refuses it anyway (C2445 on a TSubclassOf/raw-pointer ternary), (D) same name and tree but a different KIND of thing, (E) same type moved to a different HEADER, (F) same name, arity and return type with a different PARAMETER TYPE.","source":"docs/02_GOTCHAS.md §14 Directions A-F (lines 1811-1978). All three worked examples (D, E, F) verified directly across the installs during this pass. CITATION CORRECTED: the header is under Runtime/Online/HTTPServer, not Plugins/Experimental.","engine":"","ue":[],"bl":[],"section":"UE engine versions","value":"`D: FHttpRequestHandler is `typedef TFunction<bool(...)>` on 5.3 and `using ... = TDelegate<bool(...)>` on 5.4+ (Runtime/Online/HTTPServer/Public/HttpRequestHandler.h:19) - TDelegate needs CreateLambda; E: FStringOutputDevice lives in Containers/UnrealString.h `"},{"id":"M1aec77","fact":"EInternalObjectFlags::GarbageCollectionKeepFlags is an ENUMERATOR on 5.3 and a MACRO over | on 5.7, because the 5.7 set also ORs Borrowed and splits AsyncLoading into two phases; the 5.3 spelling is an undeclared identifier on 5.7 and the compiler blames the enclosing const object's initialisation, pointing at the wrong line. The separate GARBAGE_COLLECTION_KEEPFLAGS macro is unchanged in both trees.","source":"Engine - both lines verified verbatim during this pass at the exact cited numbers. GARBAGE_COLLECTION_KEEPFLAGS separately verified identical: `(GIsEditor ? RF_Standalone : RF_NoFlags)` at GarbageCollection.h:21 on 5.3 and :28 on 5.7. Handled by MIF_GC_KEEP_INTERNAL_FLAGS in MifBridgeAssetOps.cpp. docs/02_GOTCHAS.md §1","engine":"","ue":[],"bl":[],"section":"UE engine versions","value":"`5.3 ObjectMacros.h:614 GarbageCollectionKeepFlags = Native | Async | AsyncLoading | LoaderImport, || 5.7 ObjectMacros.h:677 #define EInternalObjectFlags_GarbageCollectionKeepFlags (Borrowed | Native | Async | AsyncLoadingPhase1 | AsyncLoadingPhase2 |`"},{"id":"M7d5bcc","fact":"CORRECTED BOUNDARY - FStaticMeshBatchRelevance::LODIndex holds invalid data on 5.5, 5.6, 5.7 and 5.8, NOT on 5.4. On 5.5+ it is `int8 LODIndex : 1` beside a private correctly-packed UnsignedLODIndex:4 + bInvalidLODIndex:1 that GetLODIndex() reads instead, with a present-tense deprecation message. On 5.4 the member is deprecated (tagged 5.4) but is still a plain valid int8 that GetLODIndex() returns directly, and on 5.3 it is a plain int8 with no deprecation at all - so the deprecation TAG says 5.4 while the data ac","source":"Engine Runtime/Renderer/Public/StaticMeshBatch.h read on all five installs during this pass - 5.3 :58 plain int8 no deprecation; 5.4 :44 + :63-64 (GetLODIndex returns LODIndex itself); 5.5/5.6/5.7/5.8 :57 + :77 + :85-86 identical. docs/01_POSTMORTEMS.md 2026-08-28 states '5.4+', which is one version too early. The fix","engine":"","ue":[],"bl":[],"section":"UE engine versions","value":"`5.5-5.8: UE_DEPRECATED(5.4, \"Public LODIndex member is deprecated and doesn't contain valid data anymore! Use GetLODIndex() function instead.\") int8 LODIndex : 1; with int8 GetLODIndex() const { return bInvalidLODIndex ? -1 : int8(UnsignedLODIndex); } || 5.4`"},{"id":"M04067a","fact":"ALandscapeProxy::CreateLayerInfo(Name, Template) kept its signature at 5.6 but its body became a one-line forward that never passes InTemplate on - no annotation, no warning, no symbol change, so the deprecation and symbol-drift audits are both blind to it and only reading the body finds it.","source":"Engine Runtime/Landscape/Private/LandscapeEdit.cpp - both bodies read verbatim during this pass at those exact lines. docs/02_GOTCHAS.md §14 § The fifth one exists because a DECLARATION can stay intact.","engine":"","ue":[],"bl":[],"section":"UE engine versions","value":"`5.3 LandscapeEdit.cpp:2773-2775: { ULandscapeLayerInfoObject* LayerInfo = ALandscapeProxy::CreateLayerInfo(InLayerName, GetLevel(), InTemplate); check(LayerInfo); ... } || 5.6 LandscapeEdit.cpp:3238-3241: { return UE::Landscape::CreateTargetLayerInfo(InLayer`"},{"id":"M901e56","fact":"CopyMeshFromStaticMesh clamps its LOD index against GetNumLODs() (render LODs) but then unconditionally reads GetSourceModel(LOD).BuildSettings in WITH_EDITOR builds — an index into SourceModels, which a cooked mesh does not populate. Pre-check GetNumSourceModels() > LOD, and on failure bypass the wrapper and call FStaticMeshLODResourcesToDynamicMesh::Convert directly.","source":"Engine/Plugins/Experimental/GeometryScripting/Source/GeometryScriptingCore/Private/MeshAssetFunctions.cpp:141 (the GetNumLODs clamp) and :155-158 (the unguarded GetSourceModel read) — CORRECTION: the path is the plugin, not Runtime/GeometryScriptingCore/. Exported bypass at Runtime/MeshConversionEngineTypes/Pu","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE mesh / cooked content","value":"`const FMeshBuildSettings& LODBuildSettings = FromStaticMeshAsset->GetSourceModel(UseLODIndex).BuildSettings;`"},{"id":"M33936e","fact":"The empty UPackage a failed read plants is LOADED but has NO asset registry row - so the registry, not FindPackage, separates a planted package from a real one. Measured through MifBridge's two shared resolvers, not only duplicate_asset: get_property (ResolvePropertyTarget) and list_graphs (ResolveBlueprint) on missing paths both planted one.","source":"Live 2026-09-10 on the 5.3 probe (port 8843). inRegistry is IAssetRegistry::GetAssetsByPackageName(pkg, _, /*bIncludeOnlyOnDiskAssets*/ false), as H_describe_package calls it (MifBridgeCooked.cpp). Re-runnable: tools/test_describe_package.py T3401.","engine":"5.3 (live). Not measured on other engines.","ue":["5.3"],"bl":[],"section":"asset registry","value":"describe_package on the missing path, before -> after the read: loaded false -> true, inRegistry false -> false, exports []"},{"id":"ZN2","fact":"**An editor that finds the shared server running with other arguments shuts it down and starts its own, while other editors are open.** The 5.4.4 probe logged \"Found existing instance running on port 8558 with different run context, will attempt shut down\". It stopped the old instance through the data dir's .lock and relaunched it with its own arguments plus `--owner-pid` of itself. The old instance had been started with `--cache-bucket-limit-overwrites`, which 5.4's arguments lack. The 5.5 probe did the same at its own launch, 19:00. It ran the zenserver in the shared Common/Zen/Install (5.8.13), not its in-tree one (5.4.5). So each probe launch in a sweep restarts the cache server under every other open editor.","source":"the message: `Engine/Source/Developer/Zen/Private/ZenServerInterface.cpp` @5.4.4-release :2360; live: a scratch project :769-795 (2026-09-12 18:46); a scratch project :1033-1057 (19:00 - this file is rewritten at that probe's next launch)","engine":"5.4.4, 5.5","ue":["5.4","5.5"],"bl":[],"section":"UE Zen - the one local cache server every editor shares"},{"id":"ZN3","fact":"**Command-line levers in 5.4.4**: `-NoZenAutoLaunch` makes the editor CONNECT to [::1]:8558 instead of launching a server, and `-NoZenAutoLaunch=host[:port]` connects to another host and port (both only where `ALLOW_SETTINGS_OVERRIDE_FROM_COMMANDLINE`); `-ZenDataPath=<dir>` replaces the data path when the dir is valid. What a connect-only editor does when nothing is listening was NOT read.","source":"`Engine/Source/Developer/Zen/Private/ZenServerInterface.cpp` @5.4.4-release: `FServiceSettings::TryApplyAutoLaunchOverride`, `NoZenAutoLaunch` :1063, `-NoZenAutoLaunch=` :1073; `DetermineDataPath` :643, its `ZenDataPath=` override :677-685","engine":"5.4.4","ue":["5.4"],"bl":[],"section":"UE Zen - the one local cache server every editor shares"},{"id":"ZN5","fact":"**An unreachable Zen server does not take its cache node out of the hierarchy.** The store sends a readiness request. When it fails, it logs \"Unable to reach ZenServer HTTP service\" - a Warning when the service is local, Display otherwise - and then sets `bIsUsable = true` all the same, so the node stays. A Zen node is dropped at creation only when its Host or its data path is 'None', each returning nullptr with a \"Disabled because\" line. So `-NoZenAutoLaunch` with no server running is not a clean fallback: it leaves the node in place. What that costs per request was NOT measured.","source":"`Engine/Source/Developer/DerivedDataCache/Private/ZenCacheStore.cpp` @5.4.4-release: readiness request :1660-1671, \"Using ZenServer HTTP service\" :1677, \"Unable to reach\" :1685 and :1692, `bIsUsable = true` :1708; `CreateZenCacheStore` :2102, Host 'None' :2109-2110, path 'None' :2127-2128","engine":"5.4.4","ue":["5.4"],"bl":[],"section":"UE Zen - the one local cache server every editor shares"},{"id":"ZN4","fact":"**A DDC graph with no local Zen node ships in 5.4 to 5.7 but not in 5.3 or 5.8**: `[InstalledNoZenLocalFallback]` lists Pak, CompressedPak, EnginePak, EnterprisePak, a FileSystem Local, ZenShared, Shared and Cloud - no ZenLocal. So `-ddc=InstalledNoZenLocalFallback` cannot be one switch for the whole sweep.","source":"`Engine/Config/BaseEngine.ini` @5.4.4-release :2556-2566; presence counted by grep in the installed UE_5.3 to UE_5.8 BaseEngine.ini, 2026-09-12: 0, 1, 1, 1, 1, 0","engine":"5.3 - 5.8 (installed)","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE Zen - the one local cache server every editor shares"},{"id":"U107","fact":"**Live Coding took three separate C++ changes in one session with the editor running, including new plain members and two new methods on a UCLASS**, and each succeeded in 60 to 105 seconds. The successes with layout changes carry `Live coding succeeded, data type changes may cause packaging to fail if assets reference the new or updated data types`; pure function-body changes carry a plain `Live coding succeeded`.","source":"measured 2026-09-19 on UE 5.8 across five compiles. **The warning is the thing to act on**: accepting a layout change at runtime is not the same as it being safe to cook, so anything past a function body still wants a real `tools/rebuild.py` before packaging. Ledger U55's conservative rule - function bodies only - remains the right default.","engine":"","ue":[],"bl":[],"section":"UE materials, actor identity and PIE"},{"id":"U722","fact":"**Importing a colour-mapped 8-bit TGA (header ColorMapType 1, ImageTypeCode 1, 8 bits per pixel: the \"pseudo-color\" palette form some exporters write for grayscale maps) through `import_texture` crashed the editor on a `check()` in the TGA reader: `TextureDataSize == Width * Height * 1` failed. The reader has a branch for exactly that header, so the size handed to it by the import path did not match one byte per pixel. The same images saved as 8-bit grayscale PNG import cleanly.**","source":"`Engine/Source/Runtime/ImageWrapper/Private/TgaImageSupport.cpp:362-364` (the pseudo-color branch and its check); live crash, editor log `Assertion failed: TextureDataSize == (int64) TgaHeader->Width * TgaHeader->Height * 1 [...TgaImageSupport.cpp] [Line: 364]`, on a 4096 x 4096 roughness map with that header","engine":"5.8.3, MifBridge 1.3.0","ue":["5.8"],"bl":[],"section":""},{"id":"U716","fact":"**The engine's static-mesh merge, given a package and asset name that already hold a StaticMesh, REBUILDS that mesh in place (same object, same address), so every reference to it survives: no consolidate, redirector or rename is needed (extends U406).** `MergeComponentsToStaticMesh` calls `CreatePackage` on the name, wraps the write in an `FStaticMeshComponentRecreateRenderStateContext` for the existing mesh found there, then `NewObject<UStaticMesh>` at the same name, which `StaticAllocateObject` rebuilds without moving it. Measured on stock 5.8 and 5.3 probes by the merge agent (2026-11-06): a 300x100x100 mesh (162 vertices) became 500x500x400 (270), the same object at the same path, with a placed actor and a Blueprint component template still referencing it, and the registry row still a StaticMesh. MifBridge's `merge_static_mesh_actors {replace:true}` relies on this, finishing the mesh's build and flushing rendering commands first.","source":"read from disk 2026-11-06: 5.8 `Engine/Source/Developer/MeshMergeUtilities/Private/MeshMergeUtilities.cpp` lines 2557 (`CreatePackage`), 2586 (`FStaticMeshComponentRecreateRenderStateContext`), 2588 (`NewObject<UStaticMesh>`); 5.8 `Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectGlobals.cpp` line 3568 (\"Replace an existing object without affecting the original's address or index.\"); 5.3 `MeshMergeUtilities.cpp` lines 2476, 2508, 2510; 5.3 `UObjectGlobals.cpp` line 3364; live: `tools/test_merge_replace.py` (commit 465623012).","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"","newer":true},{"id":"U717","fact":"**On UE 5.8 the HTTP server does not reuse an address and port by default, so a second editor's MifBridge listener cannot bind a port another process already holds: the bind fails.** The default comes from the console variable `HTTPServer.DefaultReuseAddressAndPortEnabled`, false. MifBridge now reports that failure by port, source and holder (commit d22a6f81d).","source":"read from disk 2026-11-06: 5.8 `Engine/Source/Runtime/Online/HTTPServer/Private/HttpServerConfig.cpp` lines 11-15; live: `tools/test_bridge_port_live.py` (commit d22a6f81d).","engine":"5.8","ue":["5.8"],"bl":[],"section":"","newer":true},{"id":"U718","fact":"**Removing a primitive from a render scene only QUEUES the removal, and the end-of-frame catch-up skips any scene that rendered that frame, so a scene that just drew keeps the primitive's render data until its NEXT update.** `FScene::RemovePrimitiveSceneInfo_RenderThread` enqueues the delete; the queue is applied in `FScene::Update`, which stamps `LastUpdateFrameCounter`; `FScene::EndFrame` applies it only when `LastUpdateFrameCounter != GFrameCounterRenderThread`. `FlushRenderingCommands` does not apply it. So a scene capture that has just rendered (render_thumbnail's sky preview) still holds the drawn mesh and its materials after the component is unregistered, and a delete, re-merge, level load or garbage collection in that window frees what the queue still points at (the R25/R44/R80 crash family). Draining it with `UpdateAllPrimitiveSceneInfos` on the render thread, as ApplyWorldOffset and UpdateStaticDrawListsForMaterials do, empties it. Measured on a stock 5.8.3 probe by the render agent (2026-11-06): two sky renders in one batch saw 5 then 6 scene primitives before the fix and 5 and 5 after; live components showing the drawn mesh stayed at 1 across four rounds (3, 5, 7, 9 before). The crash itself never reproduced on the probe, so the link from this to the field crashes is inferred, not shown.","source":"read from disk 2026-11-06: 5.8 `Engine/Source/Runtime/Renderer/Private/RendererScene.cpp` lines 1996-1999 (`EnqueueDelete`), 4302-4310 (`EndFrame` skips a scene updated this frame), 5534 (`FScene::Update`), 6844 (`LastUpdateFrameCounter = GFrameCounterRenderThread`); 5.3 `Engine/Source/Runtime/Renderer/Private/ScenePrivate.h` lines 3520-3526, 5.3 `RendererScene.cpp` line 6501; live: `tools/test_sky_render_lifetime.py` LT105 (commit 546776647).","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U719","fact":"**A MaterialInstance with no static permutation resource of its own draws with its PARENT's material resource, and calling `CacheShaders` on it compiles nothing: it only runs `InitStaticPermutation`.** `GetMaterialResource` returns the instance's own resource only when `bHasStaticPermutationResource`; otherwise it returns `Parent->GetMaterialResource(...)` (on 5.3 the instance version always defers to the parent). So waiting on the instance's own compile sees no work and reports finished at once, and the shader map that matters is the parent's. Measured on a stock 5.8.3 probe (2026-11-06): before the fix `material_statistics {compile:true}` answered `{polls: 0, compilationFinished: true}` then failed with no usable shader map; it now compiles the owning material and reports `usesParentShaderMap`.","source":"read from disk 2026-11-06: 5.8 `Engine/Source/Runtime/Engine/Private/Materials/MaterialInstance.cpp` lines 2098-2111 (`GetMaterialResource`) and 2913-2916 (`CacheShaders`); 5.3 same file lines 1790-1803 and 2487-2490; live: `tools/test_material_statistics.py` T7404, T7405 (commit a2046e5c9).","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U721","fact":"**On UE 5.3, an open spline whose LAST point is linear (`CIM_Linear`) has a ZERO tangent there, so `GetDirectionAtDistanceAlongSpline` at the spline's end returns a zero direction.** `AutoSetTangents` gives a linear point the tangent `NextPoint.OutVal - ThisPoint.OutVal`, and for the last point of an open spline `NextIndex` is the last point itself, so the difference is zero; `EvalDerivative` on or past the last key returns that point's `ArriveTangent`. Anything that builds geometry from the direction at the end (a sweep's last ring, a marking's or a lane's end, an offset) gets a zero vector there. MifBridge's road tools now take the direction just inside the spline when the engine answers zero (`MifRoadNet::DirAt`, commit 96b953060). On 5.8 `GetDirectionAtDistanceAlongSpline` can evaluate through `Spline.EvaluateDerivative` instead, and the same spline answered a non-zero direction live; 5.4-5.7 are not measured.","source":"read from disk 2026-11-07: 5.3 `Engine/Source/Runtime/Core/Public/Math/InterpCurve.h` lines 759 (`NextIndex` is `LastPoint` for the last point when not looped), 803-807 (linear tangent = next minus this) and 410-414 (`EvalDerivative` at the last key returns `ArriveTangent`); 5.3 `Engine/Source/Runtime/Engine/Private/Components/SplineComponent.cpp` line 1276 (`GetDirectionAtDistanceAlongSpline`); 5.8 `SplineComponent.cpp` lines 1033-1035; live: `tools/test_road_network.py` 23/24 on stock 5.3 before 96b953060 and 24/24 after, 24/24 on stock 5.8.","engine":"5.3","ue":["5.3"],"bl":[],"section":"","newer":true},{"id":"U724","fact":"**`RecacheMaterialInstanceUniformExpressions` reaches every instance whose parent chain contains the given material, not only its direct children: it iterates all UMaterialInstance objects and walks each one's `Parent` chain, recaching on a match. Editor only.** This is what `UMaterialInstance::PostEditChangeProperty` calls for a non-interactive edit, which is why set_metahuman_skin_look follows a reparent with an update: SetMaterialInstanceParent's recache alone was followed by a uniform expression assert on the next draw of a placed character (live, set_metahuman_skin_look restore, stock 5.8 probe, 2026-11-08; one crash, not a repeat count).","source":"5.8 `MaterialInstance.cpp:135` and 5.8 `MaterialInstance.cpp:4835`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U725","fact":"**A MetaHuman body mesh's UVs sit wholly outside 0-1, in UDIM tile 1002, so its `T_region_body_*` masks must be read from block 1002: reading block 0 leaves hands and arms unmasked.** Measured live through mesh_uv_info: outside01 = 20473 of 20473 triangles on the body mesh of one rigged, textured character; `T_region_body_A` builds at 8192x4096 with blocks 1001 and 1002 (set_metahuman_skin_look proof, stock 5.8 probe, 2026-11-08). One character measured.","source":"live measurement, 2026-11-08, docs/02_GOTCHAS.md","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U726","fact":"**ForcedLodModel is 1-based: 0 auto-selects and N forces LOD N-1, so \"ForcedLodModel 1\" means LOD 0; a LODSync component has its own 0-based ForcedLOD, -1 by default, which it applies when in range.** Comment and field in the engine headers, and the branch in the sync code. Live on a stock 5.8 probe through capture_camera on 2026-11-08: a body left unforced drew LOD 3 on some shots, up to 7 calls in a row on one run, and LOD 0 at once on others, so a single capture does not show a character's best detail.","source":"5.8 `SkinnedMeshComponent.h:692` and 5.8 `LODSyncComponent.h:74` and 5.8 `LODSyncComponent.cpp:201`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U213","fact":"**UE 5.8.2's MetaHuman likeness pipeline runs LOCALLY and is scriptable - no Epic cloud, no GUI-only step.** `UMetaHumanCharacterEditorSubsystem::ConformToTargetMeshes(character, targetMeshKey, conformTargetParams)` is `UFUNCTION(BlueprintCallable, Category=\"MetaHuman|Conforming\")` and its own comment calls it a \"Python/Blueprint-friendly wrapper around ConformTargetMeshesAsync that runs with bBlocking=true\". Beside it, `TrackFaceLandmarksFromImage(InImageData /*BGRA8*/, InWidth, InHeight, OutCurveTrackingPoints)` is also BlueprintCallable and runs face-contour detection through local NNE models (\"The first call after editor start synchronously loads the face contour tracker NNE models\"). **The conform needs GEOMETRY**: the params carry target mesh data plus keypoints, and the landmark call is documented for \"a front-facing portrait of the target mesh - typically a SceneCapture2D render\". So a photograph alone cannot produce the head shape.","source":"Engine/Plugins/MetaHuman/MetaHumanCharacter/Source/MetaHumanCharacterEditor/Public/MetaHumanCharacterEditorSubsystem.h:1800-1826`; mesh slots (StaticMesh or SkeletalMesh, Head/Body/Combined) at `Private/Tools/MetaHumanCharacterEditorMeshImportTool.h:65-77`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19 from the installed engine"},{"id":"U214","fact":"**The OLD Mesh-to-MetaHuman path still requires Epic's cloud and an Epic login, which is why the MetaHumanCharacter path above is the one to use.** `UMetaHumanIdentity` carries `LogInToAutoRigService()` and `IsAutoRiggingInProgress()`, raises `FAutoRigServiceRequest`, and reports failures under the title \"MetaHuman Service Error\". Both functions are script-exposed (`execLogInToAutoRigService` in the generated header), so the dependency is on the SERVICE, not on the UI.","source":"`.../MetaHuman/MetaHumanAnimator/Source/MetaHumanIdentity/Private/MetaHumanIdentity.cpp:47-48, 487, 496, 531`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19"},{"id":"U215","fact":"**`MetaHumanCoreML` cannot provide a Windows inference path - it is Apple-only by its own descriptor**, `SupportedTargetPlatforms: [Mac, IOS, TVOS, VisionOS]`, `EnabledByDefault: false`, described as \"CoreML models and dependencies for MetaHuman inference on Apple platforms\". Its presence in a 5.8 install is not evidence of an on-device ML face path on this machine.","source":"`.../MetaHuman/MetaHumanCoreML/MetaHumanCoreML.uplugin`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19"},{"id":"U216","fact":"**UE 5.8 ships LOCAL audio-driven facial animation, so a MetaHuman face can be animated without an iPhone.** `MetaHumanAnimator` carries a `MetaHumanSpeech2Face` module whose `FSpeech2FaceInternal::GenerateFaceAnimation(FSpeech2Face::FAudioParams, ...)` is built from `FAudioDrivenAnimationModels`, and `MetaHumanPipeline` exposes it as a `SpeechToAnimNode`. The NNE models ship as content: `NNE_AudioDrivenAnimation_AudioEncoder`, `NNE_AudioDrivenAnimation_AnimationDecoder` and `whisper_encoder`, with a Whisper attribution file. **SHIPS is what is confirmed here, not RUNS** - the solver has not been executed on this machine. Separately, the iPhone footage path recognises device classes iPhone11OrEarlier / iPhone12 / iPhone13 / iPhone14OrLater / OtheriOSDevice / StereoHMC, mapping to exactly two tuned solver configs, `iphone12` and `iphone13`; an unspecified device logs a warning and assumes iPhone 13.","source":"Engine/Plugins/MetaHuman/MetaHumanAnimator/`: `Source/MetaHumanSpeech2Face/Private/Speech2FaceInternal.h:15-19`, `Source/MetaHumanPipeline/Public/Nodes/SpeechToAnimNode.h:10,56`, `Content/Speech2Face/` asset list, `Source/MetaHumanConfig/Private/MetaHumanConfig.cpp:119-137`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19 from the installed engine"},{"id":"U217","fact":"**The Live Link Face receiver port is UDP 11111, from the engine's own defaults - and the receiving side is Apple ARKit code that works on Windows.** `FAppleARKitLiveLinkConnectionSettings::Port = 11111` and `FAppleARKitSettings::LiveLinkPublishingPort(11111)`. The three plugins a Windows project needs to RECEIVE the stream are `LiveLink`, `AppleARKit` and `AppleARKitFaceSupport`, and **all three are `EnabledByDefault: false`**, so a project that has not enabled them shows no Live Link source and looks like a network fault. The MetaHuman Animator plugin's NAME is `MetaHuman` (its descriptor is `MetaHuman.uplugin`, FriendlyName \"MetaHuman Animator\"), so enabling it by its friendly name in a .uproject silently does nothing.","source":"Engine/Plugins/Runtime/AR/AppleAR/AppleARKitFaceSupport/Source/AppleARKitFaceSupport/Private/AppleARKitLiveLinkConnectionSettings.h:14`; `.../AppleARKit/Source/AppleARKit/Public/AppleARKitSettings.h:51`; the four `.uplugin` descriptors","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19 from the installed engine"},{"id":"U118","fact":"**A stock UE 5.8 install does NOT include the MetaHuman Creator Core Data, and a clean COMPILE against the MetaHuman modules cannot detect that - code and data install separately.** `FMetaHumanCharacterEditorModule::IsOptionalMetaHumanContentInstalled()` returns false unless ALL of these exist under the MetaHumanCharacter plugin's content dir: `Content/Optional/`, `Content/Optional/TextureSynthesis/` containing at least one `*.ar` file (found recursively), and `Content/Optional/BodyTextures/` containing at least one `*.uasset`. Each failure has its own log line - the TextureSynthesis one says \"Skin editing in the MetaHuman Creator plugin will not function correctly\", the BodyTextures one \"default textures for body skeletal meshes not found\" - so **the documented consequences are about SKIN AND TEXTURES specifically**, not about conforming. The toast's button opens the Epic Games Launcher at `ue/library`, which is Epic's own intended install route. The warning is suppressible with cvar `mh.Character.SuppressContentWarnings` - suppressing it does NOT install anything, it only hides the fact.","source":"Engine/Plugins/MetaHuman/MetaHumanCharacter/Source/MetaHumanCharacterEditor/Private/MetaHumanCharacterEditorModule.cpp:44-152`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19, and MEASURED on this machine the same day: `Content/Optional` does not exist, the plugin is 730 MB, and the engine's `.egstore` component manifests list only UE_5.8, QuixelBridge_5.8 and FabPlugin_5.8"},{"id":"U121","fact":"**The footage-ingest API to use on 5.8 is CaptureManager, NOT MetaHumanCaptureSource - the latter is deprecated but still present and still script-callable.** `UMetaHumanCaptureSourceSync` is marked `UE_DEPRECATED(5.7, \"MetaHumanAnimator/MetaHumanCaptureSource is deprecated. This functionality is now available in the CaptureManager/CaptureManagerDevices module\")`, yet remains `BlueprintType` with a full BlueprintCallable \"MetaHuman|Footage Ingest API\" - `Startup`, `Refresh`, `GetTakeIds`, `GetTakeInfo`, `GetTakes`, `SetTargetPath`, `IsProcessing`, `CancelProcessing` - and its own comment says the blocking form is \"Useful when initializing from blueprints or python\". Its `CaptureSourceType` for a folder of phone takes is `EMetaHumanCaptureSourceType::LiveLinkFaceArchives`, with the folder in `StoragePath`. The replacement lives in four plugins under `Engine/Plugins/VirtualProduction/CaptureManager/` (App, Core, Devices, Editor) and is device/capability shaped: `ULiveLinkDeviceCapability_Ingest::RunIngestProcess/UpdateTakeList`.","source":"`.../MetaHumanAnimator/Source/MetaHumanCaptureSource/Public/MetaHumanCaptureSourceSync.h:16-77`; `.../VirtualProduction/CaptureManager/CaptureManagerApp/Source/LiveLinkCapabilities/Public/Ingest/LiveLinkDeviceCapability_Ingest.h:54-63`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19"},{"id":"U123","fact":"**A long `py <code>` console command is SILENTLY DROPPED - it never reaches the editor and leaves no trace anywhere.** A ~900-character `py exec(...)` ran fine; a ~1400-character one returned an empty capture, wrote nothing to the log, and did not appear even as a `Cmd:` line, while a short probe sent immediately afterwards logged normally. So the failure is not an exception, not a Python error and not a filter artefact - the command is discarded before execution, and the only symptom is silence. **Use `py \"<absolute path to .py>\"` for anything non-trivial**, which has no such limit and is also how Epic's own automation is meant to be run. **[Reviewed 2026-09-21 against the code - STALE in part: strings over 1023 chars are refused before dispatch with a named error (MifBridgeCommon.cpp:1469-1482, 1678-1696)]**","source":"MifBridge `run_console_captured` -> UE console","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19 against the live 5.8.2 editor; re-runnable by sending the two lengths and diffing `Saved/Logs/*.log` for the `Cmd:` echo","status":"superseded"},{"id":"U124","fact":"**UE Python raises a DeprecationWarning as a HARD ERROR, so a deprecated property aborts the whole script**, and 5.8's FootageCaptureData carries a chain of them: `audios` says use `Audio`, `audio` in turn says use `AudioTrack`, and `FootageCaptureMetadata.width`/`height` are deprecated as \"Redundant property\". Writing any of them raises rather than warning. The fields that still work and that the solver keys on are `image_sequences`, `depth_sequences`, `camera_calibrations` and `metadata` with `device_class` + `frame_rate`.","source":"live 5.8.2 editor, `unreal.FootageCaptureData` / `unreal.FootageCaptureMetadata`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19 while assembling a capture asset; each name failed in turn with its own DeprecationWarning text"},{"id":"U125","fact":"**Epic ships the canonical MetaHuman automation as Python inside the plugins - read it instead of reverse-engineering the C++.** `Engine/Plugins/MetaHuman/MetaHumanAnimator/Content/Python/` holds `create_identity_for_performance.py` (the full footage->identity->conform recipe), `create_capture_data.py`, `process_performance.py`, `export_performance.py` and `utility_functions_MH.py`; `Engine/Plugins/MetaHuman/MetaHumanCharacter/Content/Python/examples/` holds ~20 more including `example_conform_from_identity.py`, `example_auto_rig.py`, `example_assembly.py` and `example_modify_skin.py`. **The key structural fact they reveal: the AutoRig SERVICE is only invoked for \"prepare for performance\" (creating the rigged DNA), never for the conform itself** - `face.conform()` runs locally and `import_from_identity` consumes its conformed mesh. Also: the sync ingest API does NOT create the FootageCaptureData asset, the caller assembles it from the ingested pieces.","source":"the two Content/Python directories named above","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"read verbatim 2026-09-19 from the installed engine"},{"id":"U126","fact":"**A hand-assembled FootageCaptureData is missing TIMECODE, and the identity solve fails with a message that blames the depth data.** Building the asset from the ingested assets by name - image_sequences, depth_sequences, camera_calibrations, metadata - produces an asset that looks complete and returns `timecode_present = False`. The identity then reports `LogMetaHumanIdentity: Error: Unable to fit the mesh` followed by `Failed to solve MetaHuman Identity: please check the depth map data is complete`, i.e. it points at the depth map, which was **verified good by eye** (the frame-150 EXR, 360x640 grayf32le, covers head and shoulders fully). The missing piece is that Epic's own `create_capture_data.py` stamps timecode onto every sequence with `unreal.ImageSequenceTimecodeUtils().set_timecode_info(view.video_timecode, view.video_timecode_rate, view.video)` (and the depth equivalent), taken from the live `FMetaHumanTake.views` - data that exists ONLY on the take objects returned by `get_takes()`, never on the assets afterwards. A pose resolves its frame through `pose.timecode_alignment`, so without timecode the RGB and depth frames can resolve to different moments. Epic's script also sets `audio_tracks` (the current name), `capture_excluded_frames`, and `metadata.device_model_name` + `frame_rate` **from take_info** - which reports 60.0 for this take even though the ingested ImgMediaSource's own frame_rate_override is 30000/1000.","source":"`.../MetaHumanAnimator/Content/Python/create_capture_data.py`, `create_capture_data_assets_for_imported_takes()`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19 on the live 5.8.2 editor: hand-built asset gave FIT_EYES_FAILED then SOLVE_FAILED; rebuilding via Epic's flow gave `timecode_present video=[True] depth=[True]`"},{"id":"U127","fact":"**MetaHuman's own depth diagnostic reports \"less than 80% valid depth-map pixels in the region of the face\" when the diagnostics were never COMPUTED - the same message as a genuinely bad depth map.** `UMetaHumanIdentityPromotedFrame::DiagnosticsIndicatesProcessingIssue` tests `if (DepthMapDiagnostics.NumFacePixels == 0 || valid/NumFacePixels*100 < threshold)`, so a zeroed diagnostics struct - which is what a scripted `start_frame_tracking_pipeline` path leaves - trips the FIRST half of that OR and prints a message about the SECOND. **Measured against the actual data, the claim was false**: depth coverage INSIDE the head silhouette at that frame is 99.8% (29,608 pixels spanned, 47 holes), the head spans ~219 px against a 120 px threshold, depth values are sane (min 32.6, median 38.2, max 53.8 - centimetres at selfie distance), and an RGB-over-depth overlay lines up on the face. Thresholds live on the Face part: `MinimumDepthMapFaceCoverage = 80.0`, `MinimumDepthMapFaceWidth = 120.0`.","source":"`.../MetaHumanIdentity/Private/MetaHumanIdentityPromotedFrames.cpp:78-112`; thresholds at `Public/MetaHumanIdentityParts.h:388-394`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19: engine diagnostic vs four independent measurements of the same EXR (`depth_000150.exr`, 360x640 grayf32le)"},{"id":"U130","fact":"**Deleting an asset can leave a stale resident package that makes three sources disagree, and re-creating the same name then fails BECAUSE OF the bridge's own unattended guard.** After `delete_asset`, the .uasset was gone from disk, `EditorAssetLibrary.does_asset_exist` still said true, `load_asset` returned **None**, and `AssetTools.create_asset(...)` returned **None** with the reason in the log: `LogAssetTools: The asset 'X' already exists in package '...'. CanCreateAsset cannot ask the user as the application is running unattended and will return false`. So the overwrite PROMPT is auto-declined by `GIsRunningUnattendedScript` - the same guard that stops modals hanging the bridge (see the modal-hazard suite) - and the caller sees only a null return. `collect_garbage()` plus `scan_paths_synchronous(force_rescan=True)` did NOT clear it. The reliable way through is a different asset name.","source":"log line from `LogAssetTools`; guard behaviour per MifBridge's unattended backstop","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19 on the live 5.8.2 editor while rebuilding a MetaHuman character"},{"id":"U133","fact":"**Assembly requires a RIG, and rigging is the step that leaves the machine - `MetaHumanCharacterEditorSubsystem.request_auto_rigging` reaches Epic's service, it is not local like the conform.** `build_meta_human` refuses a conformed-but-unrigged character with `LogMetaHumanCharacterEditor: Error: MetaHuman Character 'X' not ready for assembly: Character is not rigged.` and returns having built **zero** assets (the call itself does not raise - check the asset count, not the return). Calling `request_auto_rigging` then drives EOS auth: `LogEOSAuth: No existing persistent auth credentials were found for automatic login`, `BeginDesktopCrossplayLogin`, an OAuth `TokenGrantv2` HTTP **400**, and finally `Account Portal overlay failed to load for device auth continuation; falling back to browser based flow`. **With `blocking = True` (which Epic's own example sets, and which a scripted run needs) the game thread sits inside that wait, so the editor stops responding and the MifBridge port goes silent** - the modal-hazard signature, except the thing being waited on is a browser login. The correct Python enum is `MetaHumanRigType.JOINTS_AND_BLEND_SHAPES`; Epic's example comment spells it \"JOINTS_AND_BLENDSHAPES\", which raises AttributeError.","source":"`example_auto_rig.py`; editor log at 03:41-03:43","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-19/20 on the live 5.8.2 editor; editor pid went Responding=False and Chrome launched at the same minute"},{"id":"U136","fact":"**MetaHuman eye fitting can solve ASYMMETRICALLY and the failure is invisible until skin is on.** On a conform that returned `IdentityErrorCode.NONE` with `fit_eyes = True`, the right eye ended up proud of the socket and visibly larger than the left, while the left fitted correctly - a clean success code on a half-wrong result. The subject's eyes are deep-set under overhead lighting, i.e. exactly the condition the engine's own eye-fit error text warns about (\"check that depth map data is complete in the eye region\"). Note what this means for judging output: the unassembled preview wears a debug material and handles eye meshes differently (the engine hides them by default when eyes are not fitted), so eye geometry can only be judged on an ASSEMBLED character.","source":"conform via `make_identity.py`, render via `capture_viewport`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-20 on the live 5.8.2 editor; capture `assembled_face.png`"},{"id":"U137","fact":"**Once a MetaHuman has been ASSEMBLED into a project, creating a new MetaHumanIdentity in that project RAISES** - `identity.get_or_create_part_of_class(unreal.MetaHumanIdentityFace)` throws `RuntimeError: MetaHumanIdentity: /Game/<build>/Common/Face/ARKit/AS_MetaHuman_ARKit_Mapping : Provided Skeleton Face_Archetype_Skeleton does not match bound ControlRigObject Face_Archetype_Skeleton`. The assembly copies archetype assets (skeleton, control rig, ARKit mapping) into its Common folder, and the Identity's Face part then binds against the project-local copies instead of the plugin's. **Consequence for ordering: do all identity work BEFORE assembling, or point each build at its own Common folder.** Separately, this is avoidable for eye problems specifically: `ImportFromIdentityParams.use_eye_meshes = False` drops the identity's fitted eyes at IMPORT time and falls back to template eyes, so a bad eye fit does not require re-solving the identity at all.","source":"traceback from `chain_no_eyefit.py`; params at `MetaHumanCharacterEditorSubsystem.h:392-400`","engine":"5.8.2","ue":["5.8"],"bl":[],"section":"","value":"measured 2026-09-20 on the live 5.8.2 editor after a successful build_meta_human"},{"id":"U140","fact":"**UE 5.8's \"Mesh Terrain\" is an EXPERIMENTAL, EDITOR-ONLY, OFF-BY-DEFAULT plugin that does not exist before 5.8** — not a runtime terrain system and not a Landscape replacement a plugin can assume. Its manifest declares `\"IsExperimentalVersion\": true`, `\"EnabledByDefault\": false`, and exactly ONE module, `MeshTerrainMode`, of `\"Type\": \"Editor\"`. So nothing it authors can be driven from a cooked build through this plugin; the runtime half is the separate `MeshPartition` plugin, the only one of its seven dependencies without an `\"TargetAllowList\": [\"Editor\"]`. It also ships in 5.8 ONLY — the same path does not exist under UE_5.7. Any MifBridge endpoint for it would therefore be 5.8-only and editor-only, which is the `MIF_ENGINE_5_7_PLUS` guard shape, not an unconditional one.","source":"read 2026-09-20 from Engine/Plugins/Experimental/MeshTerrainMode/MeshTerrainMode.uplugin` (fields quoted above verbatim; `Category: \"Mesh Partition\"`, Epic's own `DocsURL` is the `nK7J` \"Introduction to Mesh Terrain\" knowledge-base article). Absence in 5.7 confirmed by `ls -d \"Engine/Plugins/Experimental/MeshTerrainMode\"` returning no such path. `Engine/Source/Runtime` contains `Landscape` and no terrain module in either engine.","engine":"5.8 vs 5.7","ue":["5.7","5.8"],"bl":[],"section":""},{"id":"U141","fact":"**`MeshTerrainMode` exports NOTHING and cannot be called from another module; the API worth binding is the separate `MeshPartition` plugin, which is 204 exported C++ symbols and almost no Blueprint surface.** This answers the question U140 left open. MeshTerrainMode's module folder contains only `MeshTerrainMode.Build.cs` and `Private/` - there is **no `Public/` directory at all**, all 36 of its headers are under `Private/`, and its `Build.cs` declares no `PublicIncludePaths`. So no module can include or link against it: it is a self-contained editor mode, not an API. `MeshPartition` is the opposite - **5 modules, 111 public headers**, exported symbol counts by module `MeshPartition` 76, `MeshPartitionEditor` 111, `MeshPartitionModelingToolset` 15, `MeshPartitionCompute` 1, `MeshPartitionEditorUI` 1. Critically its `MeshPartition` module is **`\"Type\": \"Runtime\"`** (and `MeshPartitionCompute` is Runtime/PostConfigInit), so unlike MeshTerrainMode this half DOES exist in a cooked build. But it is a C++ API, not a scripting one: across all five modules there are only **15 `BlueprintCallable`** in total (4 + 11) against 163 `UCLASS`/`USTRUCT`, and the two interfaces in `MeshPartitionModifierBlueprintInterfaces.h` are both `UINTERFACE(NotBlueprintable)`. **Consequence for this bridge: the reflection path that most MifBridge endpoints use - `get_property`/`set_property`/`call_function` - reaches almost none of this, so any real support is hand-written C++ against those exported symbols, guarded 5.8-only.**","source":"read 2026-09-20 from Engine/Plugins/Experimental/`. Header/`Public` counts by `find <module>/Public -name '*.h'`; export counts by `grep -rho \"MESHPARTITION[A-Z]*_API\"`; module types quoted from `MeshPartition.uplugin`, which also carries `\"IsExperimentalVersion\": true`. MeshTerrainMode's absent `Public/` confirmed by `ls Source/MeshTerrainMode/` returning exactly `MeshTerrainMode.Build.cs` and `Private`.","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U142","fact":"**A `.uplugin` can declare pip dependencies in a `\"PythonRequirements\"` array, and UE installs them into `<PluginDir>/Content/Python/Lib/site-packages` itself - but the mechanism DOES NOT EXIST ON 5.3.** `FPipInstall` walks every enabled plugin and picks out those whose descriptor `HasTypedField(\"PythonRequirements\", EJson::Array)`, then reads each entry for an optional `\"Platform\"` string (absent or `\"All\"` matches any, else compared to `FPlatformMisc::GetUBTPlatform()`), an optional `\"Architecture\"` string (same rule against `PyUtil::GetArchString()`), and a `\"Requirements\"` array of pip requirement strings. Installed packages land in `LibDir/\"site-packages\"` and `LibDir/GetUBTPlatform()/\"site-packages\"`, and the path is added to `sys.path` by `RegisterPipSitePackagesPath` -> `PyUtil::AddSitePackagesPath`. **Version boundary, swept across all six installed engines: `PipInstall.cpp` is ABSENT in 5.3 and present in 5.4, 5.5, 5.6, 5.7 and 5.8**; it is also absent from 5.3.2 source. So a plugin that declares `PythonRequirements` gets the behaviour on 5.4+ and is silently ignored on 5.3, where the key is simply an unread field in the descriptor.","source":"read 2026-09-20 from Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PipInstall.cpp` - field detection at :238, entry parsing at :266-281, `\"Platform\"` at :887 and `\"Architecture\"` at :893, site-packages paths at :50-51, sys.path registration at :156-162. Version sweep by testing for `PipInstall.cpp` under each engine's `PythonScriptPlugin/Source/PythonScriptPlugin/Private`. NOT pinned: the extra-index-url field name, which is read into `OutExtraUrls` but whose JSON key was not read out of the source.","engine":"5.3 vs 5.4-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U143","fact":"**A landscape material supports at most 32 grass types, the number is identical on 5.3 and 5.8, and the grass entries are FULLY REFLECTED - so reading them needs no new endpoint.** `UMaterialExpressionLandscapeGrassOutput` declares `static constexpr int32 MaxGrassTypes = 32`, with a header comment warning that `LandscapeGrassWeight.usf` must be updated in step whenever it is adjusted. The entries live in `UPROPERTY(EditAnywhere, Category = UMaterialExpressionLandscapeGrassOutput) TArray<FGrassInput> GrassTypes`, and `FGrassInput` is a `USTRUCT()` whose `Name` and `GrassType` are `UPROPERTY(EditAnywhere, Category = Grass)` and whose `Input` is `UPROPERTY(meta = (RequiredInput = \"true\"))`. **Because the array and its members are reflected, `get_property` on the expression object reaches the grass entries without any grass-specific endpoint** - which narrows the \"read\" half of the landscape-grass parity gap to a convenience wrapper rather than a capability that is missing. Also confirmed from the same header: `GetInputValueType` returns `MCT_Float` for every input index, which is why a grass density input accepts any expression rather than only a layer weight.","source":"read 2026-09-20 from Engine/Source/Runtime/Landscape/Classes/Materials/MaterialExpressionLandscapeGrassOutput.h` - `FGrassInput` at :16-28, `MaxGrassTypes` at :46 with its .usf comment at :45, `GetInputValueType` at :62, `GrassTypes` at :69-70. `MaxGrassTypes = 32` confirmed identical in the 5.3 copy of the same header. NOT pinned: whether `list_material_expressions`'s per-node `properties` dump includes a nested USTRUCT array - untestable here because this project has 0 `LandscapeGrassType` assets and `M_Landscape` carries no Grass Output node.","engine":"5.3 and 5.8","ue":["5.3","5.8"],"bl":[],"section":""},{"id":"B162","fact":"**`add_group_node` wrote a LIST input element by element with a bare `float()`, and that one loop had three defects the scalar path beside it did not.** `{\"Color\": [NaN, 0, 0, 1]}` returned ok:true and the socket read back `nan`; `[1, 0, 0, 1, 7]` into an RGBA socket died as a raw `IndexError: bpy_prop_array[index] = value: index out of range` with a traceback; `{\"Scale\": [3.0]}` into a Mapping node wrote x alone, left `[3.0, 1.0, 1.0]` and echoed `[3.0]` as applied. A list is now converted whole - finite numbers, exactly the socket's width, `[r,g,b]` into a colour keeping its alpha.","source":"measured 2026-09-20 on 5.0.1 (scratch `dog_ocean.py`: `nan_list`, `long_list`, `short_list`, each read back through `run_python` as the independent path). After the fix `test_blender_node_inputs` N106-N108 pass on all five builds, and `blender_version_matrix --ops set_node_property,add_group_node,link_group_nodes,remove_node,list_group_nodes,add_procedural_texture` reports no raw exception and no divergence on 3.6.23, 4.2.17, 4.4.0, 5.0.1, 5.2.1 with `inputs` now in the payload","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B163","fact":"**An Ocean modifier with `use_foam` writes the foam into the EVALUATED mesh as a `BYTE_COLOR` corner attribute named after `foam_layer_name` - and, while that name is empty (its default), the layer is called `Col`.** The base mesh carries no foam layer; with `use_foam` off the evaluated mesh has none either. A material's Attribute node set to `foam` therefore reads nothing until the name is set.","source":"measured 2026-09-20 on 5.0.1 through `run_python` on the evaluated mesh (scratch `batch2.py` H2: SeaOn `Col CORNER BYTE_COLOR`, SeaOff no colour layer, and `foam` once `foam_layer_name='foam'`), then through the typed ops as `tools/test_blender_set_modifier.py` S101 - `mesh_stats` `evaluated.attributesAddedByModifiers` lacks `foam` before `set_modifier {foam_layer_name:\"foam\"}` and carries it after - 14/14 on every build. The `Col` default itself was read on 5.0.1 only","engine":"3.6, 4.2, 4.4, 5.0, 5.2 (the rename); 5.0 (the `Col` default)","ue":[],"bl":[],"section":""},{"id":"B164","fact":"**`add_modifier`'s curated settings were coerced with bare builtins, and each let a wrong value through as a right-looking one.** `{\"choppiness\": NaN}` on an OCEAN stored `nan` under ok:true; `{\"axisY\": {\"mif\": \"x\"}}` on a MIRROR switched the axis ON (`bool(dict)` is True); `{\"levels\": 2.7}` on a SUBSURF became 2. Now `finite_float`, `finite_int` (a fraction refused, not truncated) and a strict boolean. The matrix's sentinel pass could not reach these: it corrupts TOP-LEVEL payload keys, and these live inside `settings`.","source":"measured 2026-09-20 on 5.0.1 (scratch `batch3.py` H5, the stored values read back through `run_python`). After the fix `test_blender_set_modifier` S111 - all three refused and no modifier left in the stack - passes on all five builds","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B165","fact":"**A CURVE's `resolution_u` and `bevel_resolution` are visible in its EVALUATED mesh's vertex count, so they are readable after all - once something measures the evaluated mesh of a curve.** Two 3-point BEZIER curves differing only in resolution (4 vs 12) evaluate to different vertex counts, and two bevelled ones differing only in bevelResolution (1 vs 5) do too. `mesh_stats` refused a CURVE until 2026-09-20, which is why the second seat recorded both parameters as \"writable, unreadable\" (adv 1).","source":"`tools/test_blender_set_modifier.py` S112 and S113 through `mesh_stats` (base null, objectType CURVE) - 14/14 on every build; against the previous add-on both rows fail with *\"object 'MifSmodCurveLo' is a CURVE, not a MESH\"*","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B166","fact":"**`create_curve` parsed its points with a bare `float()`, so a NaN became a curve point and a string became a raw exception.** `[[NaN, 0, 0], [1, 0, 0]]` returned ok:true with pointCount 2; `[[\"abc\", 0, 0], ...]` returned *\"ValueError: could not convert string to float: 'abc'\"* with a traceback (the object was not created - the parse runs first). And a curve's per-point `radius`, which scales its bevel, was settable by no op - every tube one width. Now `finite_floats` per point, and `radii` (one finite, non-negative value per point, read back). A 3-point line with bevelDepth 0.1 evaluates to a tube **0.2 wide with radii [0.1, 1.0, 0.1] and 0.02 wide with [0.1, 0.1, 0.1]**, through `mesh_stats`.","source":"measured 2026-09-20 on 5.0.1 (scratch `batch2.py` H4), then `tools/test_blender_curve_taper.py` C101-C104 - 5/5 on every build, 1/5 against the previous add-on. `blender_version_matrix --ops create_curve,mesh_stats,convert_object,object_info` with radii in the payload: no raw exception, no divergence, no value drift on all five","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B168","fact":"**An Ocean modifier under a JONSWAP spectrum writes an almost CONSTANT foam layer where PHILLIPS writes a full 0-1 range, at the same settings.** Plane, resolution 10, spatialSize 60, waveScale 1.4, choppiness 1.25, windVelocity 14, foamCoverage 0.35, time 3.0, foam_layer_name `foam`, 9,604 corners: **PHILLIPS min 0.0, max 1.0, mean 0.281569; JONSWAP min 0.093059, max 0.104616, mean 0.097804; JONSWAP with wave_alignment 0.4 min 0.095307, max 0.099899, mean 0.097007** (linear, channel R; G and B equal, A 1.0). A ColorRamp from 0.35 to 0.8 over the JONSWAP layer renders solid black - which is how it was found, rendering an ocean through the typed ops.","source":"measured 2026-09-20 through `mesh_stats {attributeStats:[\"foam\"]}` and confirmed through `run_python` reading `attributes['foam'].data[i].color[0]` on the evaluated mesh (identical to six places), scratch `stats_probe.py` - the same numbers on **3.6.23 and 5.0.1**. The tool behind the read is `tools/test_blender_attribute_stats.py`, 5/5 on all five builds, 1/5 against the previous add-on","engine":"3.6, 5.0","ue":[],"bl":[],"section":""},{"id":"B169","fact":"**`world_info` called a sky-lit world dark, and the world colour writers took NaN and died on a string.** A scene world with Background Colour value `[0,0,0]` and a Sky Texture linked into that socket read `contributesLight:false`, blocker *\"the background colour is black and no environment texture drives it\"* - while a 1 BU cube lit by that world alone rendered (Cycles, 8 samples, 64x64) at **mean luminance 0.5098, max 0.9700**. `set_world {color:[NaN,0,0]}` and `create_world {color:[NaN,0,0]}` stored `nan`; `[\"abc\",0,0]` returned *\"ValueError: could not convert string to float: 'abc'\"* with a traceback, and `create_world`'s left the World in the file holding the name. Now `colorLinked` / `colorDrivenBy` / `skyTextureDriving`, the black blocker only for an UNLINKED socket, and `finite_floats` before anything exists.","source":"measured 2026-09-20 on 5.0.1 (scratch `world_probe.py`, the render's pixels read through `run_python`), then `tools/test_blender_world_sky.py` W100-W106 - **7/7 on 3.6, 4.2, 4.4, 5.0, 5.2; 1/7 against the previous add-on**. `blender_version_matrix --ops set_world,world_info,create_world,list_worlds`: clean on all five","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B174","fact":"**`set_frame_range` wrote the frame range first and refused the rest of the call after it, so a refusal left the range moved.** Against the add-on at e6ed2965, read back through `list_scenes`: `{start: 10, end: 20, frameStep: 0}` refused *\"frameStep must be at least 1\"* with the scene at 10..20; the same with `fps: 24.5` (11..21), `fpsBase: -1` (12..22), `current: Infinity` (13..23) and `usePreviewRange: \"flase\"` (14..24); and `{previewStart: 2.5, previewEnd: 30}` returned ok with `previewFrameStart: 2` (a bare `int()`).","source":"measured 2026-09-21 on 5.0.1 (scratch `frame_probe.py`), then `tools/test_blender_refusal_leaves_nothing.py` D116 - fails against e6ed2965 on 3.6, 4.2, 4.4, 5.0, 5.2; 18/18 on all five after; the 15 suites calling `set_frame_range` clean on all five","engine":"3.6, 4.2, 4.4, 5.0, 5.2","ue":[],"bl":[],"section":""},{"id":"B182","fact":"**Several processes appending whole lines to ONE file on Windows lose and tear lines unless the handle is append-only at the OS level.** Four processes x 20,000 JSON lines of ~190 bytes into one fresh file, Windows 11 Pro 10.0.22631, CPython 3.12.10: `open(path, \"a\")` + write + flush per line kept **35,801** of 80,000 lines whole, **1,939 torn, 44,202 lost**; `os.open(O_WRONLY|O_APPEND)` + `os.write` kept 32,001, **1,884 torn, 47,999 lost** (the C runtime appends by seeking to the end and then writing, and two processes seek to the same end); `CreateFileW` with **FILE_APPEND_DATA only** (share read/write/delete) + `msvcrt.open_osfhandle(O_APPEND)` + `os.write` kept **80,000 of 80,000, 0 torn, 0 lost** at ~22 us a line under four-way contention (~10 us for the other two).","source":"measured 2026-09-21, scratch `journal/append_race.py`; the reason MifBlender/journal.py opens its per-user file that way","engine":"Windows","ue":[],"bl":[],"section":""},{"id":"B195","fact":"**An unlinked Group Output does NOT pass geometry through: a NODES modifier whose group's Group Output has nothing linked into it evaluates the object to EMPTY - 0 vertices - and it vanishes from the viewport, renders and exports, with no error.** `create_node_group` makes Group Input and Group Output on a Geometry socket pair but does not link them, so a group assigned straight after creation empties its object. `list_group_nodes` reports whether the output is reachable; `assign_node_group` says so on the way out.","source":"measured on 3.6.23, 4.2.17, 4.4.0 and 5.0.1: a default cube through a NODES modifier with an unlinked Group Output evaluates to 0 vertices on all four (the measurement recorded in tools/blender-addon/MifBlender/ops_nodes.py's module docstring); `op_create_node_group`'s docstring: 'NOT linked to each other'. Two MCP descriptions said the opposite until 2026-09-21.","engine":"3.6-5.0","ue":[],"bl":[],"section":""},{"id":"B196","fact":"**The two exporters treat modifiers oppositely by default.** FBX (`export_mesh`, `use_mesh_modifiers` on): every enabled modifier is baked in, and with any enabled modifier other than an exported rig's Armature the mesh is written from its evaluated copy, which drops its shape keys - the exporter's own option text says 'WARNING: prevents exporting shape keys'. glTF (`export_scene` .gltf/.glb): `export_apply` defaults to False, so modifiers are NOT applied unless asked.","source":"read from the exporters Blender ships, 2026-09-21: io_scene_fbx/__init__.py `use_mesh_modifiers` default True, description 'Apply modifiers to mesh objects (except Armature ones) - WARNING: prevents exporting shape keys' (5.2 :403-408, 3.6 :455-460), export_fbx_bin.py evaluates the mesh when modifiers apply (5.2 :2692-2731); io_scene_gltf2/__init__.py `export_apply` default False (5.2 :679-684, same on 3.6/4.2/4.4/5.0); ops_mesh.py pins use_mesh_modifiers True; ops_io.py sets export_apply only when applyModifiers is given.","engine":"3.6-5.2","ue":[],"bl":[],"section":""},{"id":"B199","fact":"**A fluid domain's `cache_frame_start` / `cache_frame_end` have setters and no update callback, so a write from Python reaches the original and not the evaluated copy that simulates: a domain made with cacheEnd 50, then written 240, simulated 50 frames.** Tagging the object (`update_tag`) after the write makes it reach the copy (`ops_common.tag_fluid_domain`).","source":"read in Blender's source at v4.4.0: makesrna/intern/rna_fluid.cc defines both with `RNA_def_property_int_funcs(... rna_Fluid_cache_startframe_set / rna_Fluid_cache_endframe_set ...)` and no `RNA_def_property_update`; measured 2026-09-21 on 4.4.0 (scratchpad probe_fire6.py: the domain read [1, 240], rangeNotHeld framesStored 50)","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B200","fact":"**`ptcache.bake_all` bakes a Mantaflow domain only as far as the scene's POINT caches reach, and a domain's own point cache is 1-250 and not in RNA: a REPLAY GAS domain whose range was 1-300 stored 250 frames, and MODULAR and ALL domains store none.** bake_all steps from the lowest to the highest point-cache range it finds; FluidDomainSettings has no point_cache property to move; a LIQUID escapes because its FLIP particle system carries the scene's range. Mantaflow's own `fluid.bake_*` operators bake the domain's cache_frame_start..end, synchronously in exec - which is how `bake_physics` finishes a fluid now (`_bake_fluid_domains`).","source":"read in Blender's source at v4.4.0: blenkernel/intern/pointcache.cc `BKE_ptcache_bake` and `BKE_ptcache_id_from_smoke`, editors/physics/physics_fluid.cc `fluid_bake_exec` / `fluid_bake_sequence`, rna_fluid.cc (no point_cache); measured 2026-09-21 on 4.4.0: tools/test_blender_fire_reach.py F114 stored 250 of 300 before `_bake_fluid_domains` and 300 after; F116 (ALL) and F117 (MODULAR with noise) baked by the fluid operators","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B201","fact":"**A fluid domain's cache range may start before frame 1, and a pre-roll works: a domain made with cacheStart -119 baked -119..180, and frame 1 renders a fire already burning.** Frames before 1 are written with the minus sign inside the frame field of the file name.","source":"RNA range -MAXFRAME..MAXFRAME read in rna_fluid.cc at v4.4.0; measured 2026-09-21 on 4.4.0: scratchpad probe_fire7.py (three domains, frames [-119, 180], 300 stored each, frame 1 shows developed flames) and tools/test_blender_fire_reach.py F115 (-30..12: 43 frames, the first on disk -30)","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B202","fact":"**EEVEE draws volumes on a coarse grid by default - `eevee.volumetric_tile_size` 8 - which renders a baked flame as a soft blob; at 2 (volumetric_samples 128) the same frame looks close to a Cycles still at about 1 s against 20-44 s.** In EEVEE the floor under the fire stayed unlit where Cycles lit it orange, so an EEVEE fire needs a light of its own.","source":"measured 2026-09-21 on 4.4.0, 1280x720: scratchpad probe_fire4c.py (tile 8, tile 2 and Cycles on one frame; the floor lit in Cycles only), probe_fire5.py (Cycles 24 spp 19.8 s at frame 180), probe_fire7.py (Cycles 32 spp 43.7 s; EEVEE tile 2 1.0-1.4 s)","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B203","fact":"**A Principled Volume reading the `temperature` attribute lit a fire's whole smoke plume as an orange cloud; reading `flame` (temperatureAttribute \"flame\") lit only where it burned - dim red at a Temperature of 1400 K, orange-yellow at 2500 K.**","source":"measured 2026-09-21 on 4.4.0: scratchpad probe_fire4c.py (\"temperature\", 1400 K: a glowing cloud filling the plume), probe_fire5.py (\"flame\", 1400 K: dim red at the base only), probe_fire7.py (\"flame\", 2500 K)","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B205","fact":"**A REPLAY fluid domain simulates as a side effect of ANY bake that steps the scene - another domain's `fluid.bake_all`, or `ptcache.bake_all` - from that bake's first frame; a pre-rolled REPLAY domain stepped afterwards reads those frames back instead of simulating them.** So bake_physics steps REPLAY domains first, each from its own start, and judges every domain complete or not once, before any bake runs.","source":"measured 2026-09-21 on 4.4.0: tools/test_blender_fire_reach.py F122 before the reorder - a REPLAY domain (1..6) reached after an ALL domain's fluid.bake_all (1..10) already held 6 fresh frames and was reported kept; after it, stepped. REPLAY gating by cache range read in fluid.cc `fluid_modifier_processDomain` (v4.4.0)","engine":"4.4 (measured), 3.6-5.2 (suite)","ue":[],"bl":[],"section":""},{"id":"B206","fact":"**A REPLAY domain pointed at a folder that holds an earlier bake keeps those frames - unless it is first evaluated AT its cache start frame, where an outdated point cache frees the folder.** `cache_directory`'s update is `rna_Fluid_update`, which frees nothing. A pre-rolled domain (start -119) is first evaluated at frame 1, not its start, so a rebuilt showcase pointed at its previous build's folders replayed the old fire.","source":"read in rna_fluid.cc v4.4.0 (cache_directory -> rna_Fluid_update) and fluid.cc `fluid_modifier_processDomain` (\"If 'outdated', reset the cache here\", at is_startframe); measured 2026-09-21 on 4.4.0: scratchpad probe_rebake.py (current frame 3: the new domain's plain bake kept the old folder, keptExistingBake ['Twin:fluid']) against tools/test_blender_fire_reach.py F121 at frame 1 (the folder freed and re-simulated); the firetemps second preview (built in 47.6 s, cache files dated from the first build)","engine":"4.4","ue":[],"bl":[],"section":""},{"id":"B207","fact":"**EEVEE does not deliver Particle Info's Age and Lifetime to particle instances: an Age / Lifetime -> ColorRamp emission renders every instance at the ramp's FIRST stop, while Cycles colours each by its own age.** Embers meant to cool from white-yellow to dark red came out uniformly cream in EEVEE and cooled correctly in Cycles from the same file and frame.","source":"measured 2026-09-21 on 5.2.1: scratchpad probe_embers2.py (EmberMat: Particle Info -> Math DIVIDE -> ColorRamp -> Principled Emission Color, built by add_group_node / link_group_nodes / set_color_ramp) rendered at frame 60 in EEVEE (all instances one colour) and in Cycles (colour by age), probe_embers2_cycles.py","engine":"5.2","ue":[],"bl":[],"section":""},{"id":"U161","fact":"**`describe_endpoint` reports a parameter's CANONICAL spelling only; the alias-inclusive accepted set is `RejectUnknownParams`' AcceptedKeys list in the handler source, and matching there is case-INSENSITIVE.** The header states the contract: *\"Fails Out (and returns true) naming EVERY key in In that is not accepted, and listing the accepted set. Matching is case-INSENSITIVE, mirroring how the accessors above find fields, so a key that WOULD be honoured is never rejected.\"* Measured across the handler source: **449 handlers carry a guard list, naming 2,510 accepted spellings**, while describe_endpoint exposes only the canonical one of each alias group - so `to_index`, `include_objects` and `instancename` are all honoured by the running code and all invisible to a caller who trusts describe_endpoint alone. Two further counts from the same scan: **552 distinct input names are read from payloads and NOT ONE is spelt `bFoo`** (so a b-prefixed CamelCase token in guidance names a C++ field, never a bridge parameter), and of the brace-pairs that look like a commonMistakes entry, **1,148 have a sentence as their second element and 258 have a bare identifier** - the 258 are alias and accepted-key lists, which are the same shape in C++ as a mistakes table and cannot be told apart by length. **[Reviewed 2026-09-21 against the code - STALE in part: acceptedParams lists aliases too, with aliasGroups and distinctParams (MifBridgeDescribe.cpp:882, 887, 2972-2983)]**","source":"contract read 2026-09-20 from `Source/MifBridge/Public/MifBridgeParams.h:118-132` (comment quoted verbatim, signature `RejectUnknownParams(In, Out, AcceptedKeys, AcceptedSummary, KeyNotes)`). Counts re-runnable via `guard_keys()` in `tools/audit_hint_targets.py`, which parses the guard lists from `Source/MifBridge/Private/*.cpp`; the b-prefix and sentence/identifier counts by grep over the same tree. NOT claimed: that every alias is DOCUMENTED - only that the guard honours it.","engine":"n/a - this bridge's own contract","ue":[],"bl":[],"section":"","status":"superseded"},{"id":"U169","fact":"**The two landscape edit-layer API changes happen at 5.7, NOT 5.6, and MifBridge's guard is off by one engine version.** Read from the engines' own shipped sources: **`ALandscapeProxy::CanHaveLayersContent()`** is `return true;` - a genuine constant - at `UE_5.7/.../Private/LandscapeEditLayers.cpp:7735`, but at `UE_5.6/.../Private/LandscapeEditLayers.cpp:12188` it has real logic beginning `if (HasAnyFlags(RF_ClassDefaultObject **[Reviewed 2026-09-21 against the code - STALE in part: the guard is #if !MIF_ENGINE_AT_LEAST(5, 7) (MifBridgeLandscape.cpp:716-722, 735); the guard is #if !MIF_ENGINE_AT_LEAST(5, 7) (MifBridgeLandscape.cpp:733-754)]** | RF_ArchetypeObject)) { return false; }`. **`ALandscape::ToggleCanHaveLayersContent()`** is an empty `{ }` at `UE_5.7/.../LandscapeEditLayers.cpp:1372`, but at `UE_5.6/.../LandscapeEditLayers.cpp:2657` it is a working implementation that guards against being called during `UpdateLayersContent` and logs `LogLandscapeBP` if it is. `MifBridgeLandscape.cpp:534` compiles the disable-edit-layers call out under `#if !MIF_ENGINE_AT_LEAST(5, 6)`, citing both of those behaviours - which are 5.7's, not 5.6's. **Measured consequence on a live UE 5.6.1 editor**, in one level: the template map's editor-created `Landscape` reports `editLayers` of 2 and `bCanHaveLayersContent: True`, while a landscape made by `create_landscape` reports **`editLayers: []` and `bCanHaveLayersContent: False`**. So on 5.6 a bridge-created landscape has NO sculpt edit layers, the `editLayersNote` warning never fires for it (it is gated on `MadeEditLayers.Num() > 0`), and the claim that every 5.6+ landscape has edit layers is false for them. The behaviour is currently right by accident - there are no layers to disable, so skipping the toggle costs nothing - but the REASON recorded in the source is another engine's.","source":"measured 2026-09-20 on UE **5.6.1-44394996**, port 8806, a scratch project. Engine sources read from `UE 5.6` and `UE_5.7`, line numbers as cited. Flag read via `get_property {propertyPath:'bCanHaveLayersContent'}` on both actors in the same level. NOT measured: 5.8, and whether a 5.6 landscape that DOES have layers discards a merged write.","engine":"5.6 vs 5.7","ue":["5.6","5.7"],"bl":[],"section":"","status":"superseded"},{"id":"U170","fact":"**`count` is the TOTAL THAT MATCHED, not the number of rows in the response, on 11 of the 63 endpoints that emit it - and the two are only equal when no cap bit.** Every `count` site set on a response was classified: **49 are rows-returned, 11 are NOT, 3 are a literal 0 on an empty-result path**, and 2 more set `count` on a nested per-row object, which answers a different question. The 11 reach the same contract by two mechanisms, both read in the source: **capture-before-cap** - `X.Num()` stored, then `X.SetNum(limit)` truncates, and the STORED value is emitted (`MifBridgePCG.cpp:137/146/154`, `MifBridgeStateTree.cpp:110/119/127`, `MifBridgeWater.cpp:313/322/330`, `MifBridgeSpatial.cpp:275/284/291`) - and **count-past-the-cap**, where `++Matched` keeps running while row-building stops with `continue; // keep counting, build no more rows` (`MifBridgeDataTables.cpp:372-390`, `MifBridgeIntrospect.cpp:165-213`). `MifBridgeIntrospect.cpp` states the convention outright in its own comment: *\"count is the project and returned is the page\"*. **So a caller reading `count` as the length of the array it just received is wrong on those 11 whenever `limit` bites** - the row count is `returned`, or the array's own length.","source":"measured 2026-09-20 by `tools/audit_count_semantics.py`, which now exits 0 with *\"Every site is classified. Nothing is being guessed at.\"* Each of the 11 was read against the cited lines before its verdict was written into HAND_READ. Re-runnable, and it refuses to print at all if its hand-read ANCHORS disagree with the classifier.","engine":"n/a - this bridge's own contract","ue":[],"bl":[],"section":""},{"id":"U171","fact":"**`landscape_info`'s zero Z span reproduces on 5.6, AND the fix the spec item proposes would not work on a World Partition landscape, because `get_actor_bounds` on the PARENT is zero too.** Measured on live UE 5.6.1 against the template map's landscape after importing a 0..6000 uu ramp into it: `landscape_info` reported **`worldMin.z = worldMax.z = 100`** - a span of ZERO - while `trace_ground` returned real relief at the same time (139.5 and 4668.6 uu at two of five sample points). So the defect is not 5.3-specific. **The new half**: that landscape is World-Partitioned, `landscape_info` reports **1** landscape while the level holds at least **5 `LandscapeStreamingProxy` actors**, and `get_actor_bounds` on the parent `Landscape` returns **min.z 0, max.z 0, size.z 0**. The real bounds live in the PROXIES and differ between them: `LandscapeStreamingProxy_3_2_0` size.z **1994.53**, `_3_5_0` size.z **1285.16**, and `_3_3_0` / `_4_3_0` / `_3_4_0` all size.z **200** (-100..100, near-flat). The item proposes *\"take Z from the same bounds `get_actor_bounds` uses\"*; on a partitioned landscape that source is zero as well, so the fix has to walk the proxies and union their bounds. **[Reviewed 2026-09-21 against the code - STALE in part: max_value_chars goes to 100000 and clipping is flagged (MifBridgeNodes6.cpp:133, 168-188); landscape_info takes Z from the union over proxies (U172's fix) (MifBridgeLandscape.cpp:2121-2168)]**","source":"measured 2026-09-20 on UE **5.6.1-44394996**, port 8806, a scratch project, against the template map's `Landscape` (2017 verts a side, 2 sculpt edit layers). Ramp imported via `import_landscape_heightmap {minZ:0, maxZ:6000}`. Proxy bounds each read individually by `get_actor_bounds`. NOT established: whether the import reached every proxy - three read near-flat afterwards, which is consistent with an uneven write AND with those cells being flat by design, and the two are not distinguished here.","engine":"5.6, and 5.3 in the original row","ue":["5.3","5.6"],"bl":[],"section":"","status":"superseded"},{"id":"U173","fact":"**Audio is fully authorable through the bridge's GENERIC verbs - 8 of 8 types - and three of them hide every field inside one struct member.** Measured on UE 5.6.1, each created or placed and then written and READ BACK changed: `SoundAttenuation` via `set_property Attenuation.FalloffDistance` (3600 -> 3723, read back 3723); `SoundSubmix` via `OutputVolume`; `ReverbEffect` via `ReflectionsDelay`; `SoundClass` via `Properties.Volume` (1.0 -> 124); `SoundConcurrency` via `Concurrency.VolumeScale` (1.0 -> 124); `SoundCue` via `VolumeMultiplier`; `AmbientSound` and `AudioVolume` placed by `spawn_actor_in_level`. **The nested members, read from 5.6's headers**: `SoundAttenuation.h:448 FSoundAttenuationSettings Attenuation`, `SoundClass.h:215 FSoundClassProperties Properties`, `SoundConcurrency.h:167 FSoundConcurrencySettings Concurrency`; the floats at `Attenuation.h:95 FalloffDistance`, `SoundClass.h:60 Volume`, `SoundConcurrency.h:109 VolumeScale`. **Supersedes the claim** in `docs/02_GOTCHAS.md` and the parity spec that audio 'cannot be authored at all'.","source":"measured 2026-09-20, UE **5.6.1-44394996**, port 8806, a scratch project, by `tools/verify_audio_authoring.py` - 8/8 WORKS. Assets created UNSAVED under `/Game/_MifAudioProbe` in scratch write mode.","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U174","fact":"**Landscape grass is fully authorable through generic verbs, 7 of 7 - and a new Landscape Grass Output node ships with one default entry, so 'add then set [0]' leaves a dangling empty one.** Measured on UE 5.6.1: `create_asset {class:LandscapeGrassType}`; `edit_container {propertyPath:GrassVarieties, operation:add}`; `set_property GrassVarieties[0].GrassMesh` (read back the Cube mesh); `set_property GrassVarieties[0].GrassDensity.Default` (wrote 250, read back 250 - an FPerPlatformFloat); `add_material_expression` for the Grass Output node; and its `GrassTypes[0].GrassType` pointed at the grass type by TWO routes, both read back through `list_material_expressions` - `set_property` on the subobject path `<Mat>.<Mat>:<expressionName>`, and `properties` passed at creation. **The default entry, read from source**: the constructor runs `new(GrassTypes)FGrassInput(ConstructorStatics.NAME_Grass)` with `NAME_Grass(\"Grass\")`, in `UE_5.6/Engine/Source/Runtime/Landscape/Private/LandscapeGrass.cpp`. Route A's read-back showed exactly the consequence: two entries after one `add`. **Supersedes** the spec item's claim that landscape grass is 'entirely unreachable'.","source":"measured 2026-09-20, UE **5.6.1-44394996**, port 8806, a scratch project, by `tools/verify_grass_authoring.py` - 7/7. Also: `add_material_expression`'s response keys read from `MifBridgeMaterials.cpp:1297-1460` - expressionName, expressionIndex, expressionClass, propertiesApplied, x, y; no object path.","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U175","fact":"**`ui_scenario_capture` now includes the UMG layer, and captures the game viewport alone.** Measured on UE 5.6.1 in one PIE session by `tools/verify_ui_scenario_capture.py`: capture A with no widget on screen read **0.000** of pixels magenta; capture B, after a Widget Blueprint whose root is a Border at `BrushColor (R=1,G=0,B=1,A=0.6)` was created and `AddToViewport`'d, read **1.000**, and its `topLevelWidgets` listed `/Game/MifUICapture/WBP_MifCaptureTint.WBP_MifCaptureTint_C`. Both captures 1280x722 (the PIE viewport), `includesUI:true`, `captureMethod` slate; both images inspected by eye - the game view alone, no editor panels. Mechanism: `FSlateApplication::TakeScreenshot(const TSharedRef<SWidget>&, TArray<FColor>&, FIntVector&)` on `UGameViewportClient::GetGameViewportWidget()`, both declared on 5.3-5.8; `FViewport::ReadPixels`, the previous path, reads the scene render target, which Slate draws UMG over afterwards.","source":"measured 2026-09-20, UE **5.6.1-44394996**, port 8806, a scratch project, write mode full; built clean on 5.3 and 5.6","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U176","fact":"**UE's text importer refuses a hard reference to a PRIVATE object in another package, and every level actor is private - so `call_function` could pass no actor to a static function until it imported full paths with a null owner, as Epic does.** Engine source: `FObjectPropertyBase::FindImportedObject` sets the result to null when `!Result->HasAnyFlags(RF_Public) && OwnerObject && !OwnerObject->HasAnyFlags(RF_Transient)`, the outermost objects and packages differ, and the property does not `AllowCrossLevel()`, logging \"Illegal TEXT reference to a private object in external package\" (the log line: `CoreUObject/Private/UObject/PropertyBaseObject.cpp:612` on 5.6, `:573` on 5.3). `UObject::CallFunctionByNameWithArguments` passes **nullptr** as the owner to `ImportText_InContainer` at both parameter-import sites (`ScriptCore.cpp:1401,1417` on 5.3). Measured on 5.6.1: BEFORE, `call_function {objectPath:/Script/UMG.Default__WidgetBlueprintLibrary, function:Create}` refused `WorldContextObject=/Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.PlayerController_0` - the path `list_pie_actors` had just returned - as \"did not resolve to a loaded object\"; AFTER, it returned `/Engine/Transient.UnrealEdEngine_0:GameInstance_0.WBP_MifCaptureTint_C_0`, and `AddToViewport` on that path succeeded.","source":"`Runtime/CoreUObject/Private/UObject/PropertyBaseObject.cpp:612` and `Runtime/CoreUObject/Private/UObject/ScriptCore.cpp:1475` on 5.6 (the 5.3 lines, :573 and :1417, are in the fact); measured 2026-09-20, UE **5.6.1-44394996**, port 8806, by `tools/verify_ui_scenario_capture.py`","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U177","fact":"**Three engine APIs this plugin calls are UE_DEPRECATED(5.5) and still present on 5.6, so a guard keyed to 5.6 or 5.7 compiles the deprecated spelling - and prints C4996 - on 5.5 (and 5.6).** `ALandscape::GetLayer(const FName&) const`: `UE_DEPRECATED(5.5, \"Use GetLayerConst\")` at `Runtime/Landscape/Classes/Landscape.h:416` (5.5) and `:454` (5.6) - the FName overload; the int32 and FGuid ones sit two and four lines above - absent on 5.7; its body is `return GetLayerConst(InLayerName);` (`LandscapeEditLayers.cpp:12336` on 5.5). The pointer form of `ALandscapeProxy::Import`: `UE_DEPRECATED(5.5, \"Use the new method prototype which takes a TArrayView parameter for import layers\")` at `LandscapeProxy.h:1368` (5.5) and `:1393` (5.6); it builds a TArrayView and forwards (`LandscapeEdit.cpp:3246` on 5.6). `ICollectionManager::GetLastError`: `UE_DEPRECATED(5.5, \"Deprecated for thread safety reasons...\")` at `Developer/CollectionManager/Public/ICollectionManager.h:378` (5.5), where CreateCollection, DestroyCollection, AddToCollection and RemoveFromCollection gained a trailing `FText* OutError` (:121-123, :162, :174-179, :196-201); 5.4 has neither. MEASURED by full rebuild: 5.5 printed C4996 at `MifBridgeCooked.cpp:2186, 2270, 2394`; the 5.6 build at 23:37 printed the two landscape ones.","source":"engine headers read on 5.4, 5.5, 5.6, 5.7; measured 2026-09-20 by `tools/sweep_build_warnings.py`, full rebuilds of a scratch project and a scratch project","engine":"5.4-5.7","ue":["5.4","5.5","5.6","5.7"],"bl":[],"section":""},{"id":"U178","fact":"**describe_live_widget returns the whole live tree - panel children AND a nested UserWidget's own content - on UE 5.6.1 and 5.8.** A UserWidget built through the bridge (CanvasPanel_0 > Column VerticalBox > Title TextBlock, Frame Border > FrameText TextBlock, and InnerInst, a NESTED Widget Blueprint whose InnerRoot VerticalBox holds InnerText) was created in PIE with `call_function WidgetBlueprintLibrary::Create` and `AddToViewport`; `describe_live_widget` on the instance path `list_live_widgets` returned reported all nine names, each UserWidget's content under `userWidgetContent` and panel children under `children`. The walker recurses `UPanelWidget::GetChildAt` and `UUserWidget::WidgetTree->RootWidget`, unchanged since bfd8b7c8.","source":"`Source/MifBridge/Private/MifBridgeLiveWidgets.cpp`, DescribeWidgetTree; measured 2026-09-20/21 by `tools/verify_describe_live_widget.py` on UE 5.6.1-44394996 (a scratch project, port 8806) and UE 5.8 (a scratch project, port 8807) - 9 of 9 on both","engine":"5.6-5.8","ue":["5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U180","fact":"**What `stat unit` reads, where it is written, and Epic's TWO different 'bound' rules - the contract a frame-timing endpoint must copy.** `FStatUnitData::DrawStat` converts `GGameThreadTime`, `GRenderThreadTime`, `GRHIThreadTime` (each `extern RENDERCORE_API uint32`, RenderTimer.h) and `RHIGetGPUFrameCycles(GPUIndex)` with `FPlatformTime::ToMilliseconds`. They are written every frame with no stat command: `GGameThreadTime` in `FViewport::Draw`, `GRenderThreadTime` in the Slate RHI renderer. **Per-frame bound** (`FPerformanceTrackingChart`): only a frame longer than `FEnginePerformanceTargets::GetTargetFrameTimeThresholdMS()` counts, and EVERY thread whose time reaches that target is flagged - several can be at once. **Hitch cause** is a different rule: the thread at the maximum, `EpsilonCycles = 0.250f` compared against CYCLE counts (so effectively the maximum itself), tie order game, render, RHI, GPU. Same rules on 5.3 (`ChartCreation.cpp:1240`, `:1266-1287`; `UnrealClient.cpp:456-480`; `RenderTimer.h:108-117`). `FEnginePerformanceTargets`' accessors are `static ENGINE_API` on 5.3-5.8. NOT yet measured live.","source":"`Runtime/Engine/Private/UnrealClient.cpp:381-410` (DrawStat) and `:1844` (GGameThreadTime written); `Runtime/RenderCore/Public/RenderTimer.h:8-17`; `Runtime/RHI/Public/DynamicRHI.h:1250`; `Runtime/SlateRHIRenderer/Private/SlateRHIRenderer.cpp:1359`; `Runtime/Engine/Private/ChartCreation.cpp:1327` (target rule), `:1299` and `:1378` (hitch rule); `Runtime/Engine/Public/Performance/EnginePerformanceTargets.h:12` - read 2026-09-20/21","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U181","fact":"**`StaticFindObjectSafe` changed signature at 5.7.** 5.3-5.6 declare `(UClass* Class, UObject* InOuter, const TCHAR* Name, bool ExactClass=false)`; 5.7 and 5.8 declare `(UClass* Class, UObject* InOuter, FStringView Name, EFindObjectFlags Flags = EFindObjectFlags::None)`, plus an `ExactClassType` overload. A three-argument call with a `const TCHAR*` compiles on all six - it converts to FStringView - and a fourth `bool` argument does not carry across the boundary. Checked in all six installed UObjectGlobals.h.","source":"`Runtime/CoreUObject/Public/UObject/UObjectGlobals.h:408` on 5.8 (`:391` on 5.6) - read 2026-09-20; exercised by DiagnoseRefusedObjectImport, built on 5.3-5.8 by the recorded sweep of 2026-09-21","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U183","fact":"**In UE 5.8.2 PIE (editor, level viewport, D3D12, RTX 4080 SUPER, 1280x722): frames are HELD at the editor's SmoothedFrameRateRange ceiling of 120 fps with vsync and t.MaxFPS off; GRenderThreadTime reads ~0 on every frame; GPU time follows load; and GetMaxTickRate is not the cap.** Shortest of 360 frames exactly 8.3334 ms; `[/Script/UnrealEd.EditorEngine]` in BaseEngine.ini sets `bSmoothFrameRate=false` and `SmoothedFrameRateRange` 5-120 (lines 2059-2060). `renderThreadMs` 0.0001-0.0042 ms on all 360 frames beside game 6.2-11.4 ms. GPU p50 2.57-2.77 ms rose to 10.45-10.86 ms at `r.ScreenPercentage 300` (four runs), `busiestThread` game -> gpu. `UEditorEngine::GetMaxTickRate` returned 119.999 at the cap and 86.4 for ~11.6 ms frames: in the editor it is the last frame's rate clamped to the range.","source":"`Editor/UnrealEd/Private/EditorEngine.cpp:2523` (GetMaxTickRate); `Runtime/SlateRHIRenderer/Private/SlateRHIRenderer.cpp:1359` (GRenderThreadTime); measured 2026-09-21 by `tools/test_frame_capture.py` on a scratch project, port 8807, against `Source/MifBridge/Private/MifBridgeFrameStats.cpp`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U184","fact":"**On a landscape WITH sculpt edit layers, a write to the MERGED heightmap through `FLandscapeEditDataInterface(Info)` is thrown away by the next layer composite - on UE 5.6.1 and 5.8.2 alike - and an import made straight after the landscape is created never reaches the surface at all.** Measured before the fix, one session per engine, a 127x127 landscape per case made by `create_landscape {editLayers:true}` (layers `[\"Layer\"]`), a 0..4000 ramp imported, five witness vertices read with `trace_ground`: L1, import IMMEDIATELY after creation - all five `0.0` against a predicted 634.9-3174.6 (lost: the landscape's own first composite ran after it), although the import answered `ok:true, verified`. L2, import after ~5 s settle - `634.3, 1269.5, 1904.7, 2539.0, 3174.2` (landed, within 0.6); a sculpt raise of 500 landed at `4245.3`; then ONE `apply_spline_to_landscape {editLayer:\"Layer\"}` along row 90, far from both - which writes through the layer and forces a composite - returned all five witnesses to `0.0` and the sculpt centre to `0.0`. Identical digits on both engines. Supersedes the 2026-09-06 5.7 reading that \"a merged write survives\": that run waited ten seconds and requested no composite.","source":"measured 2026-09-21 on a scratch project (UE 5.6.1) and a scratch project (UE 5.8.2), both built from Source at 46693af1 plus the `create_landscape editLayers` change, with the L1/L2 phases of `tools/probe_landscape_layer_discard.py` (its v3, before `sample_landscape` existed)","engine":"5.6, 5.8","ue":["5.6","5.8"],"bl":[],"section":""},{"id":"U185","fact":"**Writing THROUGH the layer fixes it on both engines: with `FLandscapeEditDataInterface(Info, LayerGuid)` + `RequestLayersContentUpdate` + `ForceUpdateLayersContent`, the immediate import lands, and import, sculpt and PAINT all survive a layer composite.** Same probe, after the fix, UE 5.6.1 and 5.8.2: L1 immediate import `634.3 .. 3174.2` - landed; import reports `editLayer: \"Layer\"`, `surfaceDiffering: 0`, `surfaceMaxDelta: 0`; after the same spline composite the witnesses still read `634.3 .. 3174.2` and the sculpt centre `4245.3 -> 4245.3`; a paint of weight 1 at radius 800 (197 vertices, through `Layer`) reads back `1.000` from `sample_landscape` and `1.000` again after a spline composite. `sample_landscape`'s heightmap z agrees with `trace_ground`'s collision z to 0.03 at all five witnesses. An unknown `editLayer` is refused with the real layer listed. On 5.6.1, where a landscape WITHOUT edit layers can still be made: naming `editLayer` on it is refused (\"its heightmap is the landscape itself\"), and a raise of 300 without it reads back exactly `300` from `sample_landscape`.","source":"measured 2026-09-21 on the same two probes rebuilt from this commit's Source (six engines built, 0 warnings), with `tools/probe_landscape_layer_discard.py` - 12 of 12 verdicts on 5.8.2, 14 of 14 on 5.6.1 (the two unlayered checks cannot apply on 5.8, which has no such landscape)","engine":"5.6, 5.8","ue":["5.6","5.8"],"bl":[],"section":""},{"id":"U186","fact":"**`FLandscapeEditDataInterface` has a constructor that targets ONE edit layer explicitly, and its one-argument constructor does not mean \"the merged buffer\": it follows the landscape's SHARED editing layer.** `FLandscapeEditDataInterface(ULandscapeInfo*, const FGuid& InEditLayerGUID, bool)` sets `bUseSharedLandscapeEditLayer(false)`; `GetEditLayer()` returns `LocalEditLayerGUID` then, and otherwise `LandscapeInfo->LandscapeActor->GetEditingLayer()` - which the editor's Landscape mode sets to the selected layer while it is open. An invalid GUID reads and writes the composited result. Every height and weight read/write goes through `GetEditLayer()`. Epic's own Ramp tool uses the explicit form and then `RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_Heightmap_All)`. Identical in the 5.3.2 source fork and in 5.8.","source":"`Engine/Source/Runtime/Landscape/Private/LandscapeEditInterface.cpp:49` (constructor) and `:71` (GetEditLayer); `Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeRampTool.cpp:545` (the explicit construction) and `:617` (the update request)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":""},{"id":"U187","fact":"**Reading a landscape's heights through `FLandscapeEditDataInterface` calls `Modify()` on every heightmap texture read, with `bShouldDirtyPackage` defaulting to TRUE - so a \"read\" dirties the level, and inside a transaction records the textures for undo.** `FLandscapeTextureDataInfo`'s constructor runs `Texture->Modify(bShouldDirtyPackage)`, beside Epic's own comment that the better fix \"would be not to call Modify when we don't intend to actually modify the texture\"; the interface initialises `bShouldDirtyPackage(true)`. MEASURED on 5.8.2 against a level saved a moment before: one `export_landscape_heightmap` (then in a transaction) took dirty world packages `0 -> [\"/Game/_MifReadProbe/L_ReadProbe\"]` and the undo queue `0 -> 1`, entry `\"Mif Bridge: export_landscape_heightmap\"` holding the level and `Heightmap_0`; that entry then left `delete_asset` reporting 0 assets removed. After `SetShouldDirtyPackage(false)` and moving the endpoint out of the transaction: `0 -> []`, `0 -> 0` on 5.8.2 and 5.6.1, and the same for `sample_landscape`.","source":"`Engine/Source/Runtime/Landscape/Private/LandscapeEditInterface.cpp:4041` (Modify) and `:4038` (the comment), `:89` (the default); measured 2026-09-21 by `tools/probe_landscape_read_side_effects.py`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U188","fact":"**On UE 5.3 `ProfileGPU` prints a legacy tree whose percent column is `%4.1f` - padded with a space below 10 - and whose root and children share one indent.** The header is `Perf marker hierarchy, total GPU time %.2fms`, and each node `%s%4.1f%%%5.2fms   %s %s` - indent `LeftPad(EffectiveDepth * 3)`, then name and a stats suffix (`<n> draws <n> prims <n> verts`, `<n> dispatches`). So leading whitespace is indent PLUS the pad a value under 10 gets, and the dump collapses the top level: the FRAME root and its children print at the same indent. The header total is the whole GPU span: measured 215.14 ms on 5.5.4 for a frame whose root pass was 1.6 ms. The same code runs through 5.5 (UnrealEngine.cpp dispatches PROFILEGPU there).","source":"`Engine/Source/Runtime/RHI/Private/GPUProfiler.cpp:335` (the node line) and `:477` (the header); measured by `tools/probe_profilegpu_output.py` on 5.3.2 and 5.5.4","engine":"5.3.2","ue":["5.3"],"bl":[],"section":""},{"id":"U189","fact":"**UE 5.6 switched `ProfileGPU` to the rewritten GPU profiler: one report per QUEUE, headed `GPU Profile for Frame N - <Queue> <i> - GPU <g>`, and always a box-drawn table - 5.6 has no `r.ProfileGPU.TableFormatting` at all.** `RHI_NEW_GPU_PROFILER` defaults to 1 there; the header is assembled from `Frame %d - %s %d - GPU %d`; the table carries exclusive AND inclusive draws, dispatches, prims, verts, percent and time, name last, indented three spaces per level, with three different vertical rules (U+2502, U+2503, and U+250A between a percent and its bar). Its \"- Frame Time\" line is busy time only; 5.8's adds wait.","source":"`Engine/Source/Runtime/RHI/Public/GpuProfilerTrace.h:12`; `Engine/Source/Runtime/RHI/Private/GPUProfiler.cpp:1914` and `:1923`; measured on 5.6.1","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U190","fact":"**UE 5.8 adds `r.ProfileGPU.TableFormatting` (default true); off, the report is a tree of `%s%4.1f%% %5.2fms%s   %s` with two decimals and no draw counts.** The legacy profiler is gone (`RHI_NEW_GPU_PROFILER` is deprecated and always 1); the header is `GPU Profile for Frame %d - %s` with \"Graphics pipeline 0 - GPU 0\". A capture that wants per-pass draws and three decimals must pin the table on.","source":"`Engine/Source/Runtime/RHI/Private/GPUProfiler.cpp:58` (the cvar), `:1289` (the tree line), `:1397` (the header)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U191","fact":"**`profile_gpu` ranks the passes a frame really spent its GPU time on, on all six engines: the probe map's volumetric-cloud pass ranks with clouds on, vanishes with `r.VolumetricCloud 0`, and returns.** `tools/test_gpu_profile.py`, 12 of 12 on each of 5.3.2, 5.4.4, 5.5.4, 5.6.1, 5.7.4 and 5.8.2 (formats legacy-tree x3, table x3 as predicted). Cloud pass with clouds on / off / back on: 5.8.2 CloudView 0.532 ms / none / 0.609 (busy 2.626 -> 2.083 -> 2.714 ms); 5.7.4 0.539 / none / 0.545; 5.6.1 VolumetricCloud 3.248 / none / 1.518; 5.5.4 0.470 / none / 0.430; 5.4.4 0.320 / none / 0.300; 5.3.2 1.120 / none / 0.410. On 5.3.2 busyMs went UP with clouds off (8.70 -> 9.19): the legacy root holds time no marked pass accounts for, so per-pass exclusive times are what compare between captures. The root's own share is reported as unaccountedMs - after it was made the frame node's exclusive time plus any \"Other Children\" row (the legacy dump prints that row only sometimes): 2.43 of 5.74 ms on 5.3.2, 1.14 of 3.17 on 5.5.4, 0.04-0.22 ms on 5.6-5.8. Also checked on every engine: ranking by exclusive time, no pass over the busy time, a second profile refused while one waits, and every pinned r.ProfileGPU cvar restored.","source":"measured 2026-09-21 on a scratch project..58, each built from this commit's Source","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U192","fact":"**`UInstancedStaticMeshComponent` derives from `UStaticMeshComponent`, so a census that casts to the static-mesh component and reads LOD0 counts an instanced component ONCE, whatever its instance count - and MifBridge's two censuses both did.** Measured on UE 5.8.2 before the fix (`tools/probe_instance_census.py`, an empty level, standalone instanced actors of `/Engine/BasicShapes/Cube`): one cube added +48 triangles to `get_perf_stats` `scene.lod0TriangleEstimate` and to `perf_heavy_actors` `totals.triangles`; a second actor with 1000 cubes added +48 to each again. The engine cube's LOD0 is 48 triangles, not the 12 a box needs - the probe's first version assumed 12 and had to be rewritten to measure one instance first.","source":"`Engine/Source/Runtime/Engine/Classes/Components/InstancedStaticMeshComponent.h:158` (the class) and `:435` (`GetInstanceCount`); measured 2026-09-21 on a scratch project at Source 63b66ff3","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U193","fact":"**After the fix the censuses count every instance, on 5.3.2, 5.6.1 and 5.8.2 with identical numbers, and `get_perf_stats` and `scene_cost` report the same total because they are one function.** Same probe: one cube +48, the thousand-cube actor +48000 in `get_perf_stats`, `perf_heavy_actors` and `scene_cost`; the `/Engine/BasicShapes/Cube` row reads components 2, instances 1001, trianglesPerInstance 48, triangles 48048; `get_perf_stats` `scene.meshInstances` 1001 and `lod0TriangleEstimate` 48048 = `scene_cost` `census.lod0TriangleEstimate` 48048. `scene_cost {compare}` against a baseline saved on the empty level returned `lod0TriangleEstimate` change +48048, `meshInstances` +1001, `instancedMeshComponents` +2 and the cube under `meshesAdded`; a missing baseline was refused with the saved names listed.","source":"measured 2026-09-21 by `tools/probe_instance_census.py` on a scratch project, a scratch project and a scratch project rebuilt from this commit's Source (six engines built, 0 warnings)","engine":"5.3, 5.6, 5.8","ue":["5.3","5.6","5.8"],"bl":[],"section":""},{"id":"U194","fact":"**A landscape component renders with Nanite only when `ALandscapeProxy::UpdateRenderingMethod` finds all of: `landscape.RenderNanite` on and Nanite components present, `UseNanite()` true for the platform, the Nanite mesh up to date, and the materials passing `AuditNaniteMaterials()` - and it records the answer per component with `SetNaniteActive` (`IsNaniteActive()` reads it back, public and inline).** On 5.3 the platform checked is `GShaderPlatformForFeatureLevel[GMaxRHIFeatureLevel]`; from 5.4 the editor checks the WORLD's feature level instead. `AuditNaniteMaterials()` is public but has no `LANDSCAPE_API`, so a plugin cannot call it.","source":"`Engine/Source/Runtime/Landscape/Private/Landscape.cpp:7963` (UpdateRenderingMethod), `:7980` (UseNanite, world feature level), `:7989` (IsNaniteMeshUpToDate), `:7995` (AuditNaniteMaterials)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U195","fact":"**Every MifProbe editor here runs D3D12 at feature level SM5, where `UseNanite()` is false - so a landscape whose Nanite mesh is built and up to date stays non-Nanite, on 5.3.2, 5.5.4, 5.6.1 and 5.8.2 alike; and the flag alone builds nothing.** Measured 2026-09-21 by `tools/probe_landscape_nanite.py` (a 2x2-component landscape in an empty level): `set_property bEnableNanite true` on 5.8.2 left the frame's base pass drawing the landscape (1 draw, 7938 prims) and no Nanite pass; `set_landscape_nanite {enabled:true}` then built in 0.06-0.16 s, `proxiesUpToDate` 1 of 1, `naniteComponentsWithMesh` 1, and `componentsRenderingNanite` 0 of 4 with `shaderPlatform` PCD3D_SM5, `platformUsesNanite` false, `landscape.RenderNanite` 1 - the same with WorldGridMaterial assigned, ruling out the material. The editor log reads \"Creating D3D12 RHI with Max Feature Level SM5\". Relaunching the 5.8 probe with `-sm6` produced \"GPU timeout: A payload ... has not completed after 5.000427 seconds\" within its first frames.","source":"measured on a scratch project, 55, 56, 58; the SM6 timeout in a scratch project/Saved/Logs/MifProbe.log","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U196","fact":"**`ULandscapeSubsystem::BuildNanite` changed signature at 5.6: `(TArrayView<ALandscapeProxy*>, bool bForceRebuild)` through 5.5, `UE_DEPRECATED(5.6)` beside a new `(UE::Landscape::EBuildFlags, TArrayView<ALandscapeProxy*>)`, and only the flags form remains at 5.8.** Both forms are `LANDSCAPE_API` and both WAIT for every Nanite build they start (5.3 drains `NaniteBuildsInFlight` under a slow-task dialog; 5.8 calls `FinishAllNaniteBuildsInFlightNow`, which is not exported). `EBuildFlags` has None, WriteFinalLog and ForceRebuild.","source":"`Engine/Source/Runtime/Landscape/Public/LandscapeSubsystem.h:159` and `:165` on 5.6, `:148` on 5.8, `:86` on 5.3","engine":"5.6","ue":["5.6"],"bl":[],"section":""},{"id":"U197","fact":"**On 5.3 and 5.4 a static mesh's Nanite data is BUILT and then STRIPPED when the running target platform targets no Nanite-capable shader format - and the engine default targets PCD3D_SM5 only, whose `bSupportsNanite` is false; 5.5+ keep the data in the editor.** `FStaticMeshBuilder::Build` receives `bTargetSupportsNanite = DoesTargetPlatformSupportNanite(TargetPlatform)`, runs the Nanite build from LOD0 anyway (\"so that it generates the fallback\"), then `NaniteResources = Nanite::FResources();` under \"Strip the Nanite bulk from this target platform\". On 5.4 the flag is StaticMesh line 3148 and the strip StaticMeshBuilder lines 474-477; 5.5's PrepareNaniteStaticMeshBuild (StaticMeshBuilder line 185) has no strip, and 5.5 strips at cook instead (NaniteResources line 241). The default target is the same on 5.5 (BaseEngine.ini line 3317) and 5.8 (line 3411); SM5's bSupportsNanite=false is DataDrivenPlatformInfo.ini line 53 on 5.3, SM6's true at line 104. Measured 2026-09-21 by `tools/test_nanite_settings.py`, same scratch copy of /Engine/BasicShapes/Cube, same call: `hasValidNaniteData` false on 5.3.2 and 5.4.4, true on 5.5.4, 5.6.1, 5.7.4 and 5.8.2, every project reporting `targetShaderFormats` ['PCD3D_SM5'] and `targetSupportsNanite` false.","source":"`Engine/Source/Runtime/Engine/Private/StaticMesh.cpp:3108` (the flag), `Engine/Source/Developer/MeshBuilder/Private/StaticMeshBuilder.cpp:407` (build anyway), `Engine/Source/Developer/MeshBuilder/Private/StaticMeshBuilder.cpp:444-447` (strip); `Engine/Config/BaseEngine.ini` line 2994 (`+D3D12TargetedShaderFormats=PCD3D_SM5`)","engine":"5.3","ue":["5.3"],"bl":[],"section":""},{"id":"U198","fact":"**`set_nanite_settings` writes each key to this engine's `FMeshNaniteSettings` field, refuses a missing field and a mistyped value with nothing changed, and rebuilds once - on all six engines.** `tools/test_nanite_settings.py` 15/15 on 5.3.2, 5.4.4, 5.5.4 and 14/14 on 5.6.1, 5.7.4, 5.8.2 (5.6+ accept `voxelLevel`, so the nothing-changed-after-refusal check runs only before 5.6). `preserveArea true` read back as `bPreserveArea=True` on 5.3-5.6 and `ShapePreservation=PreserveArea` on 5.7-5.8; `voxelLevel` refused on 5.3-5.5 naming `VoxelLevel` with the engine's field list, and `keepPercentTriangles 0.25` sent beside it did not land; `shapePreservation Voxelize` refused before 5.7 (\"no equivalent before 5.7\"), accepted on 5.7+; `fallbackTarget NoSuchTarget`, `keepPercentTriangles \"0.5\"`, `enabled 1` and `positionPrecision 2.5` each refused. The set call returned in 0.02-0.07 s for the cube.","source":"measured 2026-09-21 on a scratch project..58, each built from this commit's Source (six engines, 0 warnings)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U199","fact":"**`FJsonValue::TryGetBool` returns true for a NUMBER (`Value != 0.0`) and for ANY string (`FString::ToBool`, which reads \"abc\" as false), and the string overloads of `TryGetNumber` for int32/uint32/int64/uint64 return true for any string (`LexFromString`, 0 for garbage) - so neither is a type check.** Only null, array and object values say no (the base class). The first build of set_nanite_settings asked bool-first and wrote `keepPercentTriangles 0.5` as \"True\", which the float import stored as 0, under ok:true on every engine; caught by the verifier before commit. Unchanged in substance at 5.8, where the string reader is the templated TJsonValueString (JsonValue.h line 493, integer overloads lines 465-490) and the number reader moved to JsonValue.cpp (line 440).","source":"`Engine/Source/Runtime/Json/Public/Dom/JsonValue.h:139` (string TryGetBool), `Engine/Source/Runtime/Json/Public/Dom/JsonValue.h:157` (number), `Engine/Source/Runtime/Json/Public/Dom/JsonValue.h:135-138` (string TryGetNumber integer overloads), `Engine/Source/Runtime/Json/Public/Dom/JsonValue.h:72` (base returns false)","engine":"5.3","ue":["5.3"],"bl":[],"section":""},{"id":"U200","fact":"**`FJsonValueBoolean::TryGetNumber` sets 1 or 0 and returns TRUE, so a JSON `true` reads as the number 1 through TryGetNumber / TryGetNumberField; and `FJsonValue::AsNumber` returns 0.0 after only an `ErrorMessage` log line when TryGetNumber fails (AsBool likewise false).** At 5.8 the boolean's TryGetNumber is JsonValue.cpp lines 476-480, same body.","source":"`Engine/Source/Runtime/Json/Public/Dom/JsonValue.h:201` (FJsonValueBoolean::TryGetNumber), `Engine/Source/Runtime/Json/Private/Dom/JsonValue.cpp:6-16` (AsNumber)","engine":"5.3","ue":["5.3"],"bl":[],"section":""},{"id":"U201","fact":"**Sixteen endpoints wrote a wrong-typed NESTED caller value under ok:true until 2026-09-21, and refuse it by name since.** `tools/verify_strict_nested_values.py` (16 endpoints, one wrong-typed value each plus a correctly typed control): on 5.8.2 at Source 1e8f7463, 24 passed and 35 failed with 0 cases lacking a verdict - every wrong-typed value accepted, every control accepted; after the fix 59 of 59 on each of 5.3.2, 5.4.4, 5.5.4, 5.6.1, 5.7.4 and 5.8.2, no case without a verdict. Specific before-fix outcomes measured: remove_foliage_instances `indices:[1.7]` and `sphere:{radius:true}` each removed an instance (3 became 1 - the radius-1 sphere at the origin took instance 0); generate_lods `screenSizes:[1.0,true]` rebuilt the cube with 2 LODs; create_material_instance `scalars:{Speed:true}` created the asset.","source":"measured 2026-09-21 on a scratch project..58, each built from this commit's Source (six engines, 0 warnings)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U202","fact":"**After the offline-gate fixes, the new and renamed suites pass on all six engines: test_nanite_settings 15/15 on 5.6-5.8 and 16/16 on 5.3-5.5 (its new check: the rebuild kept every written field - `verified` true, no `changedByEngine`), test_gpu_profile 14/14 (topPassesTotal/Truncated and topLevelTotal/Truncated consistent with the rows listed), test_scene_cost 15/15, test_landscape_sample_nanite 20/20, verify_strict_nested_values 59/59.** scene_cost and get_perf_stats agreed on each probe's own map - lod0TriangleEstimate 104320 with 65 mesh instances on 5.5-5.8, 6016 with 65 on 5.4, 3968 with 1 on 5.3 - and top=1 listed 1 of 65 meshes with meshesTruncated true (1 of 1, false, on 5.3). Every probe map carries one 256-component landscape, sampled read-only at the centre of the extent sample_landscape itself reports. The suites that need the harness's ownership check ran with MIF_PROJECT_MARKER=MifProbe.uproject, as the sweep sets it; without it wait_for_bridge refuses a probe editor's port, correctly.","source":"measured 2026-09-21 on a scratch project..58, each built from this commit's Source (six engines, 0 warnings)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U203","fact":"**Nanite DRAWS through set_nanite_settings and set_landscape_nanite once the project TARGETS SM6 - measured on UE 5.8.2 in a scratch project, a copy of a scratch project whose DefaultEngine.ini adds `-D3D12TargetedShaderFormats=PCD3D_SM5`, `+D3D12TargetedShaderFormats=PCD3D_SM6` and `DefaultGraphicsRHI=DefaultGraphicsRHI_DX12`.** The editor log read \"Creating D3D12 RHI with Max Feature Level SM6\" and logged no GPU timeout (the U195 hang was -sm6 forced onto an SM5-targeted project). set_nanite_settings reported shaderPlatform PCD3D_SM6, platformUsesNanite true, targetShaderFormats ['PCD3D_SM6','PCD3D_SM5'], targetSupportsNanite true, valid Nanite data. One ProfileGPU frame per state, same camera, `match` \"nanite\", exclusive times summed over the LISTED matches - `matched` lists at most 200, so the ON sums cover 200 of 225 (matchedTruncated true) and the OFF sum all 7: cube Nanite ON 225 passes 0.714 ms listed, OFF 7 passes 0.057 ms, and the ordinary DepthPassParallel went 67 draws / 358168 prims -> 68 / 358216 - the cube's 48 triangles leaving the Nanite pipeline. set_landscape_nanite {enabled:true} built in 36.96 s and the engine reported 256 of 256 components rendering with Nanite; the listed Nanite exclusive time went 0.740 -> 1.157 ms (200 of 225, then 200 of 242 - the per-component answer and the raster passes appearing are the primary evidence) with HW Rasterize (Triangles) 0.139 ms and ShadeGBufferCS 0.185 ms appearing; off again, 0 of 256. Re-run at the probe's final revision, 7 of 7 again: 225 / 7 passes, 0.699 / 0.062 ms, 256 of 256 (built in 1.50 s from cache), 0.713 -> 1.172 ms.","source":"measured 2026-09-21 by tools/probe_nanite_draws.py (7 of 7) on a scratch project, plugin binaries copied from a scratch project at Source 74bb2039","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U204","fact":"**JNum / JInt / JBool record a wrong-typed parameter and return the default without stopping the handler, so a transacted call refused for one has already written with the default - and until 2026-09-21 RunEndpoint Cancel()led that transaction, discarding the undo entry.** Measured on 5.8.2 at Source af5b3bb1: set_actor_transform {location:{x:100}, relative:2} on an actor at x=500 answered ok:false naming 'relative', and the actor was at x=100 afterwards. After the change (a violation-only failure keeps its transaction, identified by the TransactionId captured from GUndo while open): the same call says the change STANDS and names 'Mif Bridge: set_actor_transform', and undo_transactions {count:1} returns the actor to x=500; a call that also fails on its own (no transform + relative:2) is still CANCELLED, the next undo stays the previous clean move, and undoing it restores that move's start. tools/test_violation_undo_record.py 10/10 on 5.3.2, 5.4.4, 5.5.4, 5.6.1, 5.7.4 and 5.8.2.","source":"`Engine/Source/Editor/UnrealEd/Classes/Editor/Transactor.h:583` (GetUndoContext), `Engine/Source/Runtime/Core/Public/Misc/ITransaction.h:103` (ITransaction::GetContext), `Engine/Source/Runtime/Core/Public/CoreGlobals.h:97` (GUndo); measured 2026-09-21 on a scratch project..58 built from this commit's Source","engine":"5.3","ue":["5.3"],"bl":[],"section":""},{"id":"U205","fact":"**A Mesh Terrain cannot be CREATED through reflection or the console on 5.8 - every creation path runs C++ that is neither a UFUNCTION nor a console command - while its modifiers are reflected components.** All three creation paths (`MeshPartitionRectangleGenerator.cpp`, `MeshPartitionHeightmapImporter.cpp`, `MeshPartitionConvertTool.cpp`) do the same four steps: `SpawnActor<AMeshPartition>`, `NewObject<UMeshPartitionEditorComponent>`, `AMeshPartition::SetMeshPartitionComponent` (plain `UE_API void`, not reflected) and `RegisterComponent`, then build the sections in C++ (`FRectangleGeneratorUtils`, or `UMeshPartitionEditorComponent::SpawnBaseModifier(FDynamicMesh3, ...)` in the test utilities). The plugin's only console commands are `MegaMesh.Cache.Clear`, `MegaMesh.Cache.Usage`, `MegaMesh.GenerateNewModifierOpGuids`, `MeshPartition.Channels.CaptureNextBatch` and `MeshPartition.ReportSectionStatus` - none creates. `AMeshPartition` is `UCLASS(NotPlaceable)`; `PostActorCreated -> InitializeDefinition` assigns `MegaMeshDefinition` from `USettings::GetDefaultDefinition()`, but nothing attaches the component. The EDIT half is reflected: the modifiers are components (`UNoiseModifier : UModifierComponent`, `meta=(BlueprintSpawnableComponent)`, likewise Patch, Lattice, Boolean, MeshProject, ProjectSculptLayers, MeshProvider), `UModifierComponent` has BlueprintCallable `SetAffectedMeshPartition` and `BindToNearestMeshPartition`, and their settings are EditAnywhere UPROPERTYs.","source":"read 2026-09-21 from Engine/Plugins/Experimental/MeshPartition/Source` (`MeshPartition/Private/MeshPartition.cpp` constructor, `PostActorCreated`, `InitializeDefinition`; the three creation .cpp files at their `SetMeshPartitionComponent` calls; every `FAutoConsoleCommand*` in both plugins)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U206","fact":"**Measured live on 5.8.2: the generic endpoints build a Mesh Partition actor with no terrain component, and the next `BindToNearestMeshPartition` by any modifier CRASHES THE EDITOR.** On a copy of a scratch project with MeshPartition and MeshTerrainMode enabled: the classes resolve by plain path (`/Script/MeshPartition.MeshPartition`, `/Script/MeshPartitionEditor.ModifierActor`, `.NoiseModifier`, `.MeshPartitionEditorComponent` - Epic's C++ namespace does not appear in them); `spawn_actor_in_level` spawns the `NotPlaceable` AMeshPartition; its `MegaMeshDefinition` reads `/MeshPartition/DataAssets/MPD_Default.MPD_Default` (assigned in PostActorCreated) and `MegaMeshComponent` reads **None**; `add_component` puts a NoiseModifier on a ModifierActor and a MeshPartitionEditorComponent on the partition; `call_function BP_SetAffectedMegaMesh` returns called:true; `call_function BP_BindToNearestMeshPartition` then killed the editor: EXCEPTION_ACCESS_VIOLATION reading 0x0000000000000650 in `UMeshPartitionEditorComponent::ForAllPreviewSections` (MeshPartitionEditorComponent.cpp:945) from `UModifierComponent::BindToNearestMeshPartition` (MeshPartitionModifierComponent.cpp:623). The engine source shows why: the bind iterates EVERY AMeshPartition in the world and calls `Cast<UMeshPartitionEditorComponent>(MegaMesh->GetMeshPartitionComponent())->ForAllPreviewSections(...)` with no null check - one component-less partition anywhere in the level takes the editor down. No compiled, preview or interactive section actor ever appeared. **[Reviewed 2026-09-21 against the code - STALE in part: NotPlaceable classes are refused unless allowNotPlaceable (MifBridgeCommon.cpp:2235-2255)]**","source":"measured 2026-09-21 06:58-07:01 on a scratch project (scratch `probe_mesh_terrain.py`, `probe_mesh_terrain2.py`); crash report `Saved/Crashes/UECC-Windows-E3D9A2F646F768A3B1DDAF906DCFC7FA_0001` (CrashType Crash, symbolised call stack); source read at MeshPartitionModifierComponent.cpp:600-630","engine":"5.8","ue":["5.8"],"bl":[],"section":"","status":"superseded"},{"id":"U207","fact":"**These Engine actor classes are `notplaceable` in their UCLASS on BOTH 5.3 and 5.8 - AGameNetworkManager, AGameStateBase, APlayerState, AGameModeBase (also Transient), ADefaultPhysicsVolume (also transient) and ALevelScriptActor - and AGameNetworkManager is a concrete AInfo, which makes it the inert test subject for the spawners' NotPlaceable guard.** `UCLASS(config=Game, notplaceable, MinimalAPI)` / `class AGameNetworkManager : public AInfo` at line 27 on both. AWorldSettings carries notplaceable on 5.8's first UCLASS line and not on 5.3's (its 5.3 UCLASS spans lines - unread past the first).","source":"read 2026-09-21 from `UE 5.3|UE_5.8/Engine/Source/Runtime/Engine/Classes/GameFramework/{GameNetworkManager,GameStateBase,PlayerState,GameModeBase,DefaultPhysicsVolume}.h` and `Engine/LevelScriptActor.h`, the UCLASS line of each","engine":"5.3, 5.8","ue":["5.3","5.8"],"bl":[],"section":""},{"id":"U208","fact":"**Before 2026-09-21 MifBridge's editor-level spawners placed NotPlaceable classes: on the pre-guard build spawn_actor_in_level {actorClass: /Script/Engine.GameNetworkManager} spawned one (GameNetworkManager count 0 -> 1, undo head 'Mif Bridge: spawn_actor_in_level') and spawn_many placed two more (-> 3); with the guard both refuse, naming NotPlaceable and allowNotPlaceable, spawn nothing and leave no undo entry.**","source":"measured 2026-09-21: `tools/test_spawn_many.py` on a scratch project (5.8.2, MifBridge DLL of 05:30, before the guard) 42/47 - T548, T549, T549c, T551 fail; on the six gate probes rebuilt with the guard 47/47 each","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U229","fact":"**UE 5.8 imports OpenVDB `.vdb` files as Sparse Volume Textures through an engine editor module, not a plugin; a separate Interchange route ships as an experimental plugin that is off by default.** The factory in the `SparseVolumeTexture` editor module registers `vdb;OpenVDB Format` and branches on `Extension == \"vdb\"`; `InterchangeOpenVDB` carries `\"IsExperimentalVersion\": true` and `\"EnabledByDefault\": false`. Read on 5.8 only - earlier engines not checked.","source":"read in UE 5.8 `Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureFactory.cpp:289` (`Formats.Add(TEXT(\"vdb;OpenVDB Format\"))`) and `:345` (`if (Extension == TEXT(\"vdb\"))`); `Engine/Plugins/Interchange/Extensions/OpenVDB/InterchangeOpenVDB.uplugin`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U230","fact":"**`FNiagaraStackGraphUtilities::GetOrderedModuleNodes`, the call that returns an emitter stage's modules in stack order, has no `NIAGARAEDITOR_API` in any engine 5.3-5.8, so an editor module outside NiagaraEditor cannot link it; `GetStackFunctionInputs` is exported only from 5.6, and only its two `FCompileConstantResolver` overloads.** A module-stack reader in MifBridge has to walk the output node's graph itself. Same trap as `KillSystemInstances` (Source/MifBridge/Private/MifBridgeNiagara2.cpp:986-990, found by the linker).","source":"read in each installed engine's `Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Public/ViewModels/Stack/NiagaraStackGraphUtilities.h`: `GetOrderedModuleNodes` 5.3 :52, 5.4 :54, 5.5 :54, 5.6 :55, 5.7 :55, 5.8 :55, all unexported; `GetStackFunctionInputs` 5.3 :117/:120/:121 and 5.4, 5.5 :123/:126/:127, all unexported; 5.6, 5.7, 5.8 :131 unexported, :134 and :135 `NIAGARAEDITOR_API`","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U231","fact":"**A Niagara system's emitters are sub-objects of the system asset, and each handle carries its own emitter's object path, which is not the handle name.** `EmitterHandles[i]` reads `VersionedInstance=(Emitter=\"/Script/Niagara.NiagaraEmitter'<system object path>:<object name>'\")`, and on the system measured a handle named `X` pointed at the object `X_0`. Read the path off the handle rather than building it from the name.","source":"measured 2026-09-21 on 5.8.2 in the MifVFX scratch project: `list_object_properties {objectPath:<a NiagaraSystem>}`, the EmitterHandles value; reproducible on any system (the source asset is not named here)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U232","fact":"**UE 5.8's OpenVDB importer reads a sequence frame number as the digits after the LAST non-digit character, so a minus sign is dropped: Blender's pre-roll frame `fluid_data_-0001.vdb` and `fluid_data_0001.vdb` both parse as frame 1; and sequence membership is every `.vdb` in the folder whose name starts with the chosen file's base name, so a Mantaflow cache folder's pre-roll frames join the sequence.** Import a baked fire from a folder holding only the frames to keep, renumbered from 0 - never straight from a cache folder with a negative-start pre-roll (B208).","source":"read in UE 5.8 `Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureOpenVDBUtility.cpp:569-610` (`FindOpenVDBSequenceFileNames`: `StartsWith` filter :594, `GetFilenameNumberSuffix` via `FindLastCharByPredicate` :606)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U233","fact":"**`UHeterogeneousVolumeComponent::EndFrame` starts at 0, and the automatic reset to `NumFrames - 1` runs only in editor code or through `SetMaterial`: so a volume component whose asset never stored an EndFrame plays frame 0 only in a packaged build.** The constructor sets `EndFrame = 0` (:265); `OnSparseVolumeTextureChanged` (:476-496) sets `StartFrame 0, EndFrame = GetNumFrames() - 1`, and its callers are `PostEditChangeProperty` (:541, inside `#if WITH_EDITOR` :516-585), `TickComponent` only inside `#if WITH_EDITOR` (:655-661), and `SetMaterial` (:628). So a tool that places one must write EndFrame explicitly. Read from source, not run in a packaged build.","source":"read in UE 5.8 `Engine/Source/Runtime/Engine/Private/Components/HeterogeneousVolumeComponent.cpp:265, :476-496, :516-585, :628, :655-661`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U234","fact":"**The OpenVDB import options - which VDB grid feeds which texture channel - are a plain C++ struct with no reflection, so neither Python nor a generic property setter can set them; the only UObject beside them holds preview data.** `struct FOpenVDBImportOptions` (:29) and its component-mapping structs (:17, :23) carry no USTRUCT; `UOpenVDBImportOptionsObject` (:84-91) holds only `FOpenVDBPreviewData`. A custom channel map on import needs C++.","source":"read in UE 5.8 `Engine/Source/Editor/SparseVolumeTexture/Public/OpenVDBImportOptions.h:17-91`","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U235","fact":"**`UNiagaraComponent::AdvanceSimulation` does nothing while the component is PAUSED: the system instance's `AdvanceSimulation` only runs its manual ticks `if (TickCountToSimulate > 0 && !IsPaused())`, so 'pause the effect, then step it N ticks' cannot drive a frame-exact capture.** The alternative for stepping an effect by exact time is the desired-age path - `SetAgeUpdateMode`, `SetDesiredAge` / `SeekToDesiredAge`, `SetSeekDelta`, all `UFUNCTION(BlueprintCallable)` on the component - whose determinism is still to be measured here.","source":"read in UE 5.8 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraSystemInstance.cpp:987-989` (5.3: `:980-982`, same guard); `NiagaraComponent.cpp:1098-1104` (5.8) forwards to it; setters `Public/NiagaraComponent.h:300-371`","engine":"5.3, 5.8","ue":["5.3","5.8"],"bl":[],"section":""},{"id":"U236","fact":"**A placed Niagara component steps by exact time only when it is SOLO: `SetForceSolo(true)` first, then `SetAgeUpdateMode(DesiredAge)`, `SetSeekDelta(1/fps)`, `SetLockDesiredAgeDeltaTimeToSeekDelta(true)` and `SetDesiredAge(t)` through `call_function`, and the age read back (`get_niagara_component_stats` `age`) before a capture. Without solo the desired age is never sought: the component is batched, the world manager ages it in real time, and SetDesiredAge only records a number - SetDesiredAge(0.5) read 1.08 and ran on at 1.05 s per wall second, while with solo it read 0.49999 and held; 61 steps at 1/30 s landed within 22 us of i/30 (27 ms per step), and SetForceSolo(false) with TickDeltaTime puts it back to real time.** TickComponent seeks through the solo system simulation only, and a component that does not require solo mode has its own tick disabled. THIS ROW SAID THE OPPOSITE until 2026-09-22: two unsolo'd runs of 90 steps agreeing to 0.48/255 per pixel measured a capture loop running at a steady pace twice, not exact steps. The empty-stage finding it carried stands (the open-world default level's sky flooded every brightness mask; `new_level` plus `r.DefaultFeature.AutoExposure 0` fixed it). The enum argument is accepted by name (`\"DesiredAge\"`).","source":"read: 5.3 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraComponent.cpp:801` (check(IsSolo()) in TickComponent), `:845` (the DesiredAge branch), `:858` (GetSoloSystemSimulation), `:4054` (SetComponentTickEnabled(bRequireSoloMode)) - the 5.3.2 source fork; 5.4 `NiagaraComponent.cpp:841`, `:885`, `:898`; 5.5 `NiagaraComponent.cpp:863`, `:907`, `:919`; 5.6 `NiagaraComponent.cpp:948`, `:960`; 5.7 `NiagaraComponent.cpp:980`, `:993`; 5.8 `NiagaraComponent.cpp:995`, `:1008`, `:4717` (RequiresSoloMode), `:4733`; SetForceSolo is a UFUNCTION on all six (5.3 `Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraComponent.h:299`, 5.8 `NiagaraComponent.h:307`). Measured 2026-09-22 on 5.8.2 (a scratch project, port 8858): verify/solo_check.py (batched vs solo) and verify/solo_step_test.py (61 steps, idle hold, jump, reset, back to real time) on a placed campfire","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U237","fact":"**Niagara's scalability culling in an editor world measures from the editor's own viewports, never from a scene capture: `FNiagaraWorldManager` reads `World->CachedViewInfoRenderedLastFrame` when the level editor's viewport is on that world, and only the game viewport, the editor engine's viewport update and an editor viewport's draw ever fill that array.** NiagaraWorldManager.cpp:1747-1765 picks the world's cached views (`bUseWorldCachedViews`, forced on under `GCurrentLevelEditingViewportClient`); the array is filled at GameViewportClient.cpp:1913 (`AddStreamingViewInfo`, UnrealClient.cpp:1771-1786), EditorEngine.cpp:2694 (a viewport that is not redrawing still caches its view) and EditorViewportClient.cpp:4929 - no scene-capture path adds to it. `DistanceCull` (:2374-2430) takes the nearest of THOSE view origins, and the frustum test (:2255-2293) uses THOSE frustums, starting the sim anyway only when there is no cached view at all. So an effect with distance or visibility culling (EmitterState's `Enable Distance Culling`, or an effect type's cull) is judged by where the editor camera is, even while `capture_camera` films it from a metre away: before a capture, put the editor viewport (`set_viewport_camera`) where the capture camera is.","source":"read 2026-09-22 from the installed UE 5.8 source (Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraWorldManager.cpp; Engine/Source/Runtime/Engine/Private/UnrealClient.cpp, GameViewportClient.cpp; Engine/Source/Editor/UnrealEd/Private/EditorEngine.cpp, EditorViewportClient.cpp)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U238","fact":"**A Niagara system saved by UE 5.7 loads in 5.8.2 with `compiledDataCurrent` false and `compilePending` true, and that pending compile only advanced while a NiagaraComponent held the system: polled through `describe_niagara_system` alone, a never-placed system stayed `compilePending` for over five minutes; after `SetAsset` on a placed component it was `readyToRun` 2.0 s later.** All 27 systems placed in the first pass reported `compilePending` on load and `readyToRun` after their run. A capture or measurement of a freshly loaded system must therefore place it FIRST and wait for `readyToRun` and `compiledDataCurrent` on the placed system - stepping before that renders what the previous compile left, which for a 5.7 asset is nothing.","source":"measured 2026-09-22 on 5.8.2 (MifVFX scratch editor, port 8852): verify/behaviour_pass.py, the same six-emitter fire system timed out at 120 s waiting unplaced and was ready in 2.0 s placed","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U239","fact":"**From UE 5.4 an unattended Sparse Volume Texture import takes the WHOLE `FOpenVDBPreviewData` - grids, sequence file list AND import options, i.e. the grid-to-channel map - from `UAssetImportTask::Options` when that is a `UOpenVDBImportOptionsObject`, so C++ can import a VDB with a chosen channel map; on 5.3 it cannot.** SparseVolumeTextureFactory.cpp:436-452 (5.8): unattended (`IsAutomatedImport`, `FApp::IsUnattended`, a commandlet or `GIsRunningUnattendedScript`) + a task whose Options casts to `UOpenVDBImportOptionsObject` -> its `PreviewData` is used as is. `LoadOpenVDBPreviewData`, which fills that struct from a file, is `SPARSEVOLUMETEXTURE_API` in the Public header from 5.4 (SparseVolumeTextureFactory.h:61 on 5.4, :64 on 5.8); on 5.3 the module's headers are Private and nothing in it carries `SPARSEVOLUMETEXTURE_API`. Refines U234: the options struct is still unreflected, so Python and a generic property setter still cannot reach it.","source":"read 2026-09-22 from the installed UE 5.3, 5.4 and 5.8 sources (Engine/Source/Editor/SparseVolumeTexture)","engine":"5.4-5.8","ue":["5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U240","fact":"**Niagara Fluids 3D gas systems (the `Grid3D_Gas_*` modules) and the distance-field collision/avoidance modules do not COMPILE when mesh distance fields are off (`r.GenerateMeshDistanceFields`, off in a bare project), and the system then runs nothing while every status reads healthy; a script compiled that way stays broken after the setting is turned on.** The failure is logged only at compile time - `LogNiagaraEditor: The mesh distance field generation is currently not enabled, please check the project settings.` (402 lines in the scratch editor's log) - and afterwards `describe_niagara_system` says readyToRun and the component bIsActive while nothing draws: all 11 gas systems and one distance-field-avoidance system measured blank in capture_camera AND in the editor viewport after 10 s of real-time play. After enabling distance fields and restarting, a system first compiled with them off logged `Could not create script runtime data ... compiled DI count 15 and resolved DI count 17` and a recompile returned in 0.0 s from cache; the stock gas templates compiled fresh in a distance-field-on session drew. So: turn distance fields on BEFORE a gas system is first compiled in a project.","source":"measured 2026-09-22 on 5.8.2: the MifVFX scratch editor (8852, distance fields off: log count, verify/vol_probe.py viewport + capture), and a separate 5.8.2 probe with distance fields enabled (the vfx_volumes build's gas-preset suite, pixel-counted)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U241","fact":"**A Volume-domain material compiles only with the Additive blend mode, on every engine 5.3-5.8: any other blend mode is the compile error `Volume materials must use an Additive blend mode.`** The check is `Domain == MD_Volume && BlendMode != BLEND_Additive`, so a HeterogeneousVolume's material is created Additive or it does not compile.","source":"read in each installed engine's `Engine/Source/Runtime/Engine/Private/Materials/`: 5.3 `HLSLMaterialTranslator.cpp:1316-1318`, 5.4 `:1768`, 5.5 `:1901`, 5.6 `:2099`, 5.7 `MaterialShared.cpp:6105`, 5.8 `MaterialShared.cpp:6398-6400`","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U242","fact":"**A Sparse Volume Texture Sample with nothing wired into Coordinates samples ONE POINT on UE 5.3 and the whole volume from 5.4: 5.3 compiles the UVW as the constant (0,0,0), so every pixel reads the texture's minimum corner, while 5.4-5.8 fall back to (LocalPosition - ObjectLocalBounds Min) / ObjectLocalBounds Extents.** A volume material that must work on 5.3 wires that UVW itself. ObjectLocalBounds' outputs, in pin order on 5.3-5.8: `Half Extents`, `Extents` (the full size, 2 x Half Extents), `Min`, `Max`, all in local space.","source":"read in each installed engine's `Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressions.cpp`, `UMaterialExpressionSparseVolumeTextureSample::Compile`: 5.3 `:26803` in the 5.3.2 source fork, `:26787` in the Epic 5.3.2 install (`CompileWithDefaultFloat3(Compiler, Coordinates, 0.0f, 0.0f, 0.0f)`); the fallback at 5.4 `:28170`, 5.5 `:27625`, 5.6 `:23058`, 5.7 `:21812`, 5.8 `:22244-22258` (`ObjectLocalBounds(1)` is Extents, `ObjectLocalBounds(2)` is Min); the ObjectLocalBounds outputs 5.3 `:13093-13099`, 5.8 `:12401-12407`","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U243","fact":"**EXTENDING U232 - the OpenVDB importer lays a sequence out as the array Lowest..Highest of the parsed frame numbers and CUTS it at the first empty slot, so every file after a gap is dropped without a message; two files that parse to the same number share one slot, and the one the folder listing returns later overwrites the other.** Measured: a Blender Mantaflow cache folder holding frames -9..40 (50 files) imported through the stock importer as 41 frames, and nothing reported the nine files dropped - the pre-roll files -0001..-0009 alias frames 1..9. The same code on every engine from 5.3 (in the factory on 5.3-5.5, in the OpenVDB utility from 5.6).","source":"read in UE 5.8 `Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureOpenVDBUtility.cpp:634-644` (the slot assignment `SequenceFilenames[Index - LowestIndex] = Path / ItemFilename`) and `:646-654` (`// Chop off any items after finding the first gap`); the same blocks at 5.6 and 5.7 `:634/:646`, and in `SparseVolumeTextureFactory.cpp` at 5.3 `:214/:226`, 5.4 and 5.5 `:207/:219`. Measured 2026-09-22 on 5.8.2 with `import_asset` of a raw cache folder, in a separate 5.8.2 probe project","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U244","fact":"**The OpenVDB importer's DEFAULT channel map, the same on 5.3-5.8: when a grid is named exactly `density` and the other grids' components number at most 4 and not 3, density goes alone into A.r as 8-bit unorm and every other component into B as float16; otherwise every component, in the file's grid order, fills A then B four at a time as float16, and anything past eight is dropped.** A Blender Mantaflow fire file (velocity x3, density, shadow, temperature, flame: six components besides density) therefore imports as A = velocity.x, velocity.y, velocity.z, density and B = shadow, temperature, flame, so its density sits in A.a and a second texture carries the shadow grid. An unattended import given no options takes this map; from 5.4 a caller's map can replace it (U239).","source":"read in each installed engine's `Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureFactory.cpp`, `ComputeDefaultOpenVDBGridAssignment`: 5.3 `:45` (the condition `:64`), 5.4-5.6 `:38` (`:57`), 5.7 `:40` (`:59`), 5.8 `:40-109` (`:60`). The Mantaflow map measured 2026-09-22 on 5.8.2 as `import_vdb_as_volume_texture {dryRun:true}` `defaultMap`, which matches the rule","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U245","fact":"**A Niagara system's `IsReadyToRun` means \"nothing is still compiling\", not \"it compiled\": neither the system's check nor an emitter's ever asks whether a script SUCCEEDED, so a system whose scripts failed to compile reads ready to run.** The emitter check returns false only while a spawn, update or event script (CPU) or the GPU compute script is still pending, on every engine 5.3-5.8; the system check adds missing system scripts, outstanding compile requests and a compiled-data count mismatch. From 5.5 an emitter is also never ready when it is a GPU sim with any event handler (\"event scripts on GPU emitters are not currently supported\"); on 5.3 and 5.4 that emitter reads ready, and its event read or write node fails the translation instead, which it does on every engine (\"Cannot use an event read node on GPU sims!\"). `describe_niagara_system`'s `readyToRun` is this call (MifBridgeNiagara2.cpp:143), so a compile verdict has to come from each script's own status. Measured: a GenerateLocationEvent module on an emitter without persistent IDs failed both particle scripts (\"Before the Particles.ID parameter can be used, the 'Requires persistent IDs' option has to be activated\") while readyToRun read true.","source":"read: the emitter check `FVersionedNiagaraEmitterData::IsReadyToRunInternal` in `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraEmitter.cpp` - 5.3 `:1494` (the 5.3.2 source fork), 5.4 `:1505`, 5.5 `:1491` (the GPU-event refusal `:1523`), 5.6 `:1511` (`:1543`), 5.7 `:1504` (`:1536`), 5.8 `NiagaraEmitter.cpp:1515-1557` (`:1547`); the system check 5.8 `NiagaraSystem.cpp:1731`, `UNiagaraSystem::IsReadyToRunInternal`; the node errors in `Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraNodeReadDataSet.cpp:110` and `NiagaraNodeWriteDataSet.cpp:94`, the same lines on all six. Measured 2026-09-22 on 5.8.2 with `compile_niagara_system` beside `describe_niagara_system`, in a separate 5.8.2 probe project","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U246","fact":"**A Niagara compile waited on from inside a bridge handler - a loop on the game thread - makes no progress unless the loop pumps the asset and shader compiling managers: `PollForCompilationComplete` only QUERIES (`QueryCompileComplete(false)`, no wait) and advances nothing itself.** Polling it alone for 90 s timed out on a two-emitter stock system; calling `FAssetCompilingManager::Get().ProcessAsyncTasks()` and `GShaderCompilingManager->ProcessAsyncResults(...)` between polls compiled the same systems in 0.2-1.9 s. This is how `compile_niagara_system` waits. Complements U238, where a compile advanced only once a placed component held the system.","source":"read in UE 5.8 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraSystem.cpp:3282-3290` (`UNiagaraSystem::PollForCompilationComplete` -> `QueryCompileComplete(false)`, :3532). Measured 2026-09-22 on 5.8.2 (`compile_niagara_system`, in a separate 5.8.2 probe project)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U247","fact":"**An OpenVDB read in the editor crashes the editor, instead of failing, when the machine runs out of memory: the c-blosc 1.21.0 that every engine 5.3-5.8 links for OpenVDB's compressed grids does not check its temporary buffer - `my_malloc` returns NULL after a printf when `_aligned_malloc` fails, and the serial decompress path uses the result unchecked (`tmp = my_malloc(...)`, `tmp2 = tmp + blocksize`) - so decompression writes to address 0.** Unreal's own allocator never sees it, so the crash is not reported as out-of-memory (`bIsOOM` 0). Triage signature: EXCEPTION_ACCESS_VIOLATION writing 0x0 in UnrealEditor_SparseVolumeTexture, blosc frames (`BLOSC_LZ4_decompress_safe`, `blosc_decompress_ctx`) under `openvdb::io::bloscFromStream`, reached from `LoadOpenVDBPreviewData` or the factory - look at system memory before looking at the file. Measured: an `import_vdb_as_volume_texture` preflight read crashed 5.8.2 one second after Windows logged event 2004 (a low virtual memory condition; the top consumers three editors at 17.4, 6.8 and 6.7 GB); the faulting instruction was `movups [rdi], xmm0` with rdi 0 and r13 0xc000, the grid's blocksize; the same file had read cleanly twice moments before, and the same suite passes with memory free.","source":"read: c-blosc v1.21.0 `blosc/blosc.c:206-228` (`my_malloc`) and `:814` (the unchecked use), github.com/Blosc/c-blosc at tag v1.21.0 - the version in every installed engine's `Engine/Source/ThirdParty/Blosc/Deploy/c-blosc-1.21.0` (5.3 fork, 5.4-5.8). Crash measured 2026-09-22 on 5.8.2 (the minidump under a probe's Saved/Crashes, read with cdb `.ecxr`; the System event log)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U248","fact":"**A Niagara GPU script's shader messages come back as ONE list of warnings and errors, and the engine counts a message as an error only when it contains \"err0r\" (its own rewrite of \"error\"): read that list as errors and every warning becomes a failed compile.** `UNiagaraScript::GetLastCompileStatus` walks `ScriptResource->GetCompileErrors()` and sets Error only for an entry containing \"err0r\", Warnings otherwise. Measured: the first 5.8.2 compile of the stock RecycleParticlesInView emitter listed three X3571/X3078 messages, all warnings by that rule; a shader map loaded from the DDC listed none.","source":"read in `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraScript.cpp`, `UNiagaraScript::GetLastCompileStatus`: 5.3 `:4238` (the 5.3.2 source fork), 5.8 `:4334`. Measured 2026-09-22 on 5.8.2 with `compile_niagara_system`, in a separate 5.8.2 probe project","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U249","fact":"**EXTENDING U246 to GPU shaders - a Niagara GPU shader map waited on from a bridge handler also needs the Niagara shader compilation QUEUE and the Niagara shader compilation MANAGER pumped: `INiagaraModule::ProcessShaderCompilationQueue()` (NIAGARA_API; the queue's own tickable is not exported) and `GNiagaraShaderCompilationManager.ProcessAsyncResults()` (NIAGARASHADER_API).** Without them a fresh stock GPU emitter's shader compile made no progress in 300 s on 5.3.2; with them it finished in 0.9 s.","source":"read: 5.3 `Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraModule.h:70` (the 5.3.2 source fork), 5.8 `NiagaraModule.h:77`; the manager 5.8 `Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShaderCompilationManager.h:44` (`ProcessAsyncResults`) and `:59` (`GNiagaraShaderCompilationManager`), the same lines on the 5.3 fork. Measured 2026-09-22 on 5.3.2 with `compile_niagara_system {includeGpuShaders}`, in a separate probe project","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U250","fact":"**An editor-world Niagara component stops ticking while the editor is held non-realtime - unfocused with \"Use Less CPU when in Background\" on - so a placed effect sampled then shows its age and tick count standing still.** Measured: a stock Fountain ticked five times and then stood still for 30 s while `get_viewport_camera` reported realtime false; with a realtime viewport the same placement ticked and spawned. A measurement of a placed effect (particle counts, ticks, frames) has to check the viewport is live first. Relates to the throttled-editor tick quantum.","source":"measured 2026-09-22 on 5.3.2 (`get_niagara_component_stats` beside `get_viewport_camera`, in a separate probe project)","engine":"5.3 (measured)","ue":["5.3"],"bl":[],"section":""},{"id":"U251","fact":"**No stock Niagara module has an object input - a UObject input that is not a data interface - so the stack's object-asset binding has no stock target to exercise.** Searched: every module script under /Niagara on 5.8.2 (244) plus 179 dynamic inputs, and on 5.3.2 (200 module scripts).","source":"measured 2026-09-22 on 5.8.2 and 5.3.2 (`tools/test_niagara_stack_writes.py` T12129's search, in separate probe projects)","engine":"5.3, 5.8 (measured)","ue":["5.3","5.8"],"bl":[],"section":""},{"id":"U252","fact":"**A Niagara system as the engine LOADS it differs from its saved bytes in three ways: duplicate function nodes are renamed unique (InitialMeshOrientation -> InitialMeshOrientation001), rapid-iteration constants a stage script no longer holds are dropped, and modules are upgraded to newer versions.** So a reader of the saved bytes and a reader of the loaded asset agree everywhere else and differ exactly there. In the second run every dropped constant equalled its module's declared default (0.0 against 0, Lifetime 1.0 against 1), so the effect's behaviour does not change.","source":"measured 2026-09-22 on 5.8.2 in two agreement runs of `describe_niagara_emitter_stack` against an offline reader of the saved packages, both read-only in separate probe projects: 44,045 of 44,105 items agree (the 60: 55 dropped constants, 4 renamed nodes, 1 version upgrade), and 24 of 24 systems agree with 16 such changes (1 renamed node, 15 at-default constants)","engine":"5.8","ue":["5.8"],"bl":[],"section":""},{"id":"U253","fact":"**On UE 5.5 a Niagara Fluids gas system activated as it is placed can run and stay BLANK: the explosion template - its source fires once and its particles live 0.1-0.2 s - compiled clean, ticked and completed while drawing nothing, and once the fire template did the same; the same component RESET once its system was ticking drew.** Measured with a control: the engine's own Grid3D_Gas_Explosion placed as it ships drew nothing on 5.5 and a rising puff by 2 s on 5.6 under the same camera and wait; a template copy drew 268-502 changed pixels on 5.5 where 5.3/5.4/5.6/5.7/5.8 drew thousands, and 9,024 after a reset once it ticked; a fire copy once drew 185 while a stock control beside it, reset while it was waited on, drew 23,961. So a tool that places gas restarts it once the system ticks (mif_create_niagara_fluid_effect does); the cause - a first-frame splat lost before the grid simulates - is consistent with every measurement but not read in source. Not reproduced on 5.5.4 on 2026-09-25 (VL61): started by its Asset alone, as it was placed, the burst drew in 3 trials of 3.","source":"measured 2026-09-22 on 5.5.4 and 5.6.1 (capture_viewport frames and get_niagara_component_stats, `tools/test_vfx_composites.py` V343 with its stock control, in separate probe projects with mesh distance fields on)","engine":"5.5 (measured; 5.6 the control)","ue":["5.5","5.6"],"bl":[],"section":""},{"id":"U254","fact":"**UE 5.3 and 5.4 ship no stock `Minimal` emitter template; their `Empty` template holds the same stack.** `/Niagara/DefaultAssets/Templates/Emitters/` has Empty.uasset on 5.3 and 5.4, and Minimal.uasset with no Empty on 5.5-5.8. Read back from a scratch system built from each: EmitterState (emitter update), InitializeParticle V2 (particle spawn), ParticleState (particle update) and one enabled sprite renderer, on 5.3 Empty, 5.4 Empty and 5.5 Minimal alike; the emitter properties differ only by fields the older engine does not have (EmitterDependencies, SimStageExecutionLoopEditorData). So a build that names Minimal fails at its first add_niagara_emitter on 5.3 and 5.4 (\"not an asset that could be loaded\"), and naming Empty there gives the same starting stack.","source":"listed from each install's Engine/Plugins/FX/Niagara/Content/DefaultAssets/Templates/Emitters; measured 2026-09-22 on 5.3.2, 5.4.4 and 5.5.4 (add_niagara_emitter + describe_niagara_emitter_stack with includeRenderers, in separate probe projects) (the listing covers 5.3-5.8; measured on 5.3, 5.4, 5.5)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U255","fact":"**A Niagara renderer's `bIsEnabled` is a plain `UPROPERTY()` on 5.3-5.7 and `UPROPERTY(EditAnywhere)` only on 5.8, while `SetIsEnabled(bool)` is `NIAGARA_API virtual` on all six.** So a property write of bIsEnabled that works on 5.8 is refused as not editable on 5.3-5.7 (measured on 5.5.4, 5.6.1 and 5.7.4: \"NiagaraSpriteRendererProperties has no editable property 'bIsEnabled'\"), and the setter is the portable way to turn a renderer on or off.","source":"read: 5.3 `Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraRendererProperties.h:428-429` (setter :380), 5.7 `NiagaraRendererProperties.h:469-470` (setter :428), 5.8 `NiagaraRendererProperties.h:482-483` (setter :433). Measured 2026-09-22 through set_niagara_renderer on 5.5, 5.6 and 5.7 (read on 5.3-5.8; measured on 5.5, 5.6, 5.7)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U256","fact":"**Changing a Niagara emitter handle's enabled state under a RUNNING component killed the 5.8.2 editor: the next end-of-frame render update hit `ensure(RendererIndex == NumEmitterRenderers)` and the frame after it the fatal `check(DataSetIdx < ExecCtx->DataSets.Num())`.** `FNiagaraEmitterHandle::SetIsEnabled` invalidates the compile results and nothing resets the live instances, which still carry render data built for the old emitter set. Tearing every component that uses the system down before the change and reinitialising it after - `FNiagaraSystemUpdateContext` with destroy-on-add, exported on all six - is the guard (set_niagara_emitter uses it).","source":"read: 5.8 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraSystemRenderData.cpp:226` (the ensure), 5.8 `Engine/Source/Runtime/VectorVM/Private/VectorVMRuntime.cpp:421` (the check), 5.8 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraEmitterHandle.cpp:135-165` (SetIsEnabled), the update context 5.3 `Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraCommon.h:970`, 5.8 `NiagaraCommon.h:1033`. Crashed 2026-09-22 on 5.8.2 (two crash reports, one event: an active placed two-plus-emitter system, set_niagara_emitter enabled:false without recompile) (the crash measured on 5.8 only; the guard read on 5.3-5.8)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U257","fact":"**A Niagara system compile requested over N emitters that finishes after emitter N+1 was added kills the editor at the next world tick: UNiagaraSystem::QueryCompileComplete, reached from the world-tick poll, hands the result to NiagaraEditor code that indexes past the set (\"Array index out of bounds: 3 into an array of size 3\", then \"2 into 2\" in a second run). Aborting the running compile before the handle change (UNiagaraSystem::KillAllActiveCompilations, NIAGARA_API on 5.3-5.8 - what the Niagara editor calls before a structural change, e.g. FNiagaraSystemViewModel::PostUndo) removes it: the next compile is built from the new set.** A build that adds emitters one by one while other edits request compiles hits the window without trying.","source":"read: 5.4 `Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraSystem.cpp:3102` (QueryCompileComplete), 5.4 `NiagaraSystem.cpp:2708` (KillAllActiveCompilations), 5.8 `NiagaraSystem.cpp:3532` and 5.8 `NiagaraSystem.cpp:3125`. Crashed 2026-09-22 on 5.4.4 in tools/test_vfx_look_live.py L105 (the muzzle flash's emitters added in turn), 2 of 2 runs, two crash reports; with add_niagara_emitter / remove_niagara_emitter aborting first (MifBridge a7a72006) the same suite ran to its end on 5.4.4 (85 of 86; the one failure a faint smoke). Not reproduced by a one-off compile request followed by an add (T6104) - the window needs the compile still running","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U258","fact":"**Removing an emitter handle leaves that emitter's two nodes in the system graph (a UNiagaraNodeEmitter per handle for the system spawn and update scripts, keyed by its EmitterHandleId UPROPERTY), and every later compile of the system fails while the emitter list reads back correctly.** The editor's own delete (FNiagaraEditorUtilities::RemoveEmittersFromSystemByEmitterHandleId) rebuilds those nodes through FNiagaraStackGraphUtilities::RebuildEmitterNodes; neither is exported from NiagaraEditor, and UNiagaraNodeEmitter's own methods are not either, so a caller outside it destroys the removed handle's nodes by reflection and relinks their neighbours (the rebuild's first half).","source":"read: 5.3 `Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraEditorUtilities.cpp:2137`, 5.8 `NiagaraEditorUtilities.cpp:2197`; 5.3 `Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/ViewModels/Stack/NiagaraStackGraphUtilities.cpp:3395` (the 5.3.2 source fork; stock 5.3.2 has it at line 3384), 5.8 `NiagaraStackGraphUtilities.cpp:3342`; 5.3 `Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraNodeEmitter.h:85`, 5.8 `NiagaraNodeEmitter.h:86` (FGuid EmitterHandleId). Measured 2026-09-22 on 5.4.4: a Fountain system with an emitter added and then removed through remove_niagara_emitter compiled with 18 errors and no events (9 when the removal followed a recompile request); with the nodes removed (MifBridge a7a72006) T8100, T8102 and T6104 compile clean (tools/test_niagara_add_remove.py, tools/test_niagara_emitter.py)","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":""},{"id":"U271","fact":"**Enabling MetaHumanCharacter in a clean Blueprint-only project mounts 30 more engine plugins and logs 29 more startup warnings on UE 5.7.4 (MetaHuman Creator's optional content not installed), and 40 more plugins and 3 more warnings on 5.8.2 (optional content installed).** One project, one MifBridge DLL, the .uproject differing only by `{\"Name\": \"MetaHumanCharacter\", \"Enabled\": true}`. 5.7 adds USDCore, USDImporter, Mutable, OpenColorIO, ChaosClothAsset (+Editor), ChaosOutfitAsset, AppleARKit (+FaceSupport), LiveLink, LiveLinkControlRig, LiveLinkHub, TextureGraph, DNACalib, CaptureManagerCore/Editor, PerformanceCaptureCore, MetaHuman, MetaHumanCoreTech and more; its warnings are \"MetaHuman Optional Content folder not found. MetaHuman Creator plugin initialized with limited features\", 8 missing MetaHumanSDK Intro_1/2.png reads, 8 missing VisionOS launcher icons, 9 missing Slate images, 2 unknown Slate widget styles and one TextureGraph \"serialize Var ... NOT FOUND\". 5.8 adds the Electra media player family, ControlFlows, NNERuntimeBasicCpu and ScriptableToolsFramework besides; its warnings are two unknown Slate styles 'MetaHumanAuthenticationMenu' and the TextureGraph line. A `create_metahuman_character` on 5.7 then logs \"Failed to initialize texture synthesis with default models, skin editing will be disabled\" - and still returns a character with `valid: true`.","source":"measured 2026-09-23 ~00:00; warnings counted up to `(Engine Initialization) Total time`; mounted plugins diffed by `Mounting Engine plugin` lines. Requiring the plugin in MifBridge.uplugin would put this into every 5.6+ project - rejected for that reason (MifBridge.Build.cs).","engine":"5.7.4, 5.8.2","ue":["5.7","5.8"],"bl":[],"section":"UE plugin loading - optional dependencies, target receipts and delay-loaded modules"},{"id":"U272","fact":"**A UE module can DELAY-LOAD another plugin's module DLL, and the calls resolve at runtime once that module is loaded - but UBT drops a misnamed `/DELAYLOAD` without a word.** `PublicDelayLoadDLLs` with `UnrealEditor-MetaHumanCharacter.dll` and `UnrealEditor-MetaHumanCharacterEditor.dll` moved both from MifBridge's import table to its delay-import table on 5.6, 5.7 and 5.8 (69 static imports -> 67 plus 2 delayed). With MetaHuman Creator enabled, `create_metahuman_character` (GetPrivateStaticClass, InitializeMetaHumanCharacter, IsCharacterValid) and `spawn_metahuman_actor` (TryAddObjectToEdit, SpawnMetaHumanActor) returned ok through the thunks on 5.7.4 and 5.8.2; without it an `FModuleManager::IsModuleLoaded` check refuses first and the editor stays up. The name must be UBT's exactly - `{AppName}-{Module}[-{Platform}-{Config}].dll`, AppName `UnrealEditor` in a shared build environment and the target name in a unique one, engine modules built as Development under DebugGame, the suffix only when that configuration is not `UndecoratedConfiguration`. UBT links with `/ignore:4199`, so a name that matches no import is ignored silently and the import stays static; delayimp.lib is always linked. No engine Build.cs delay-loads an `UnrealEditor-*.dll`.","source":"source: UE_5.7 `Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs:1834-1838` (/ignore:4199) and `:3102-3104` (/DELAYLOAD), `UEBuildWindows.cs:1986` (delayimp.lib), `Configuration/UEBuildTarget.cs` `CreateDynamicLibraryForModule` (~4916-4929) and `MakeBinaryFileName` (5002-5030; 5.8 5352-5397); tools/audit_plugin_imports.py reads PE data directory 13, cross-checked against dumpbin on the D3D12RHI, Core and MifBridge DLLs; launches 2026-09-23.","engine":"5.6 - 5.8","ue":["5.6","5.7","5.8"],"bl":[],"section":"UE plugin loading - optional dependencies, target receipts and delay-loaded modules"},{"id":"U273","fact":"**UE 5.8 has no MassEntity PLUGIN at all - MassEntity is only an engine runtime module there - so detecting Mass by its plugin descriptor switches it OFF on 5.8.** 5.3 and 5.4 ship MassEntity as a plugin module (Plugins/Runtime/MassEntity/Source/MassEntity). 5.5-5.7 moved the code to `Engine/Source/Runtime/MassEntity` and keep an EMPTY descriptor (\"This plugin is now DEPRECATED. All of MassEntity code has been moved to the engine\", no modules). 5.8 deletes the descriptor. MifBridge's `AddPluginModules(\"MIF_WITH_MASSENTITY\", \"MassEntity\", ...)` finds the shell on 5.5-5.7 and links the engine module; on 5.8 it finds nothing, prints \"plugin 'MassEntity' not found under Engine/Plugins - its endpoints will compile as unavailable-on-this-engine refusals\", and the Mass endpoints refuse although the module exists.","source":"source: the installed engines' trees (`MassEntity.uplugin` and `MassEntity.Build.cs` located per version) and the 5.8 build log a scratch project, 2026-09-22.","engine":"5.3 - 5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE plugin loading - optional dependencies, target receipts and delay-loaded modules"},{"id":"U281","fact":"**The Live Link client registers when the LiveLink PLUGIN's module loads, not because another module links it: with MifBridge neither linking nor enabling Live Link, `tools/test_livelink.py` passes 21/21 - push, exact read-back, replace-in-place, the 0.5 s staleness rule, and push/read during PIE - on 5.3, 5.7 and 5.8 in projects that enable Live Link themselves.** This settles the one open question in the 2026-08-31 spec item on idle plugin dependencies (\"linking LiveLink from here may be what causes that load\"): it was not - the DLL never imported the module. `MifBridgeLiveLink.cpp` compiles against LiveLinkInterface (an engine module) and resolves `ILiveLinkClient` through `IModularFeatures` at call time.","source":"measured 2026-09-23: suite logs ll53_suite / ll57_suite / ll58_suite (session scratchpad) on a scratch project, whose .uproject enables MifBridge and LiveLink, built from the tree that removed the dependency.","engine":"5.3, 5.7, 5.8","ue":["5.3","5.7","5.8"],"bl":[],"section":"UE plugin loading - optional dependencies, target receipts and delay-loaded modules"},{"id":"U450","fact":"**A required plugin brings its own required plugins with it, so a module DLL reached through that chain loads though the plugin that links it never names it in its descriptor: UE 5.8's StateTree lists PropertyBindingUtils with `\"Enabled\": true` and no Optional, and 5.3's StateTree and PCG list StructUtils the same way, while PropertyBindingUtils and StructUtils are both EnabledByDefault false - so enabling StateTree (or PCG on 5.3) enables them, and their Binaries join the DLL search path (U270).** MifBridge 1.1's binaries import PropertyBindingUtils on 5.8 and StructUtils on 5.3 (its Build.cs links them from StateTree's own descriptor) and load for this reason; audit_plugin_imports follows such chains since ab05c45c and names them.","source":"read: 5.8 `Engine/Plugins/Runtime/StateTree/StateTree.uplugin:52`, 5.8 `Engine/Plugins/Runtime/PropertyBindingUtils/PropertyBindingUtils.uplugin:13`, 5.3 `Engine/Plugins/Runtime/StateTree/StateTree.uplugin:36`, 5.3 `Engine/Plugins/Experimental/PCG/PCG.uplugin:40`, 5.3 `Engine/Plugins/Experimental/StructUtils/StructUtils.uplugin:13`; measured 2026-09-28: the import tables of MifBridge's 5.8.2 and 5.3.2 DLLs (probes a scratch project, a scratch project) read by audit_plugin_imports - PropertyBindingUtils through StateTree, StructUtils through PCG and StateTree","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE plugin loading - optional dependencies, target receipts and delay-loaded modules"},{"id":"PH1","fact":"The asset registry keeps, for each package its reader could open, the file and licensee versions, the size and `ImportedClasses` - the classes of the package's exports that live in another package, as full object paths - so an AtmosphericFog or a Cascade component saved inside a map or a Blueprint is visible without loading it. It keeps no saving engine version: the package reader copies only `IsLicenseeVersion()` out of the summary's `SavedByEngineVersion`, so the version comes from each file's `FPackageFileSummary`, whose `operator<<` is exported and reads the header alone. A file the reader cannot open - not a package, unversioned, too old, too new, a custom version the engine lacks - is not in the registry at all (`ReadAssetFile` returns false), so a folder of content saved by a newer engine is EMPTY to a registry-only scan. `import_health_report` walks the folder on disk and names every such file with the loader's reason; test_import_health_report IH111 plants two non-packages and reads both back as notIndexed on 5.8.2 and 5.3.2.","source":"5.3 `Runtime/CoreUObject/Public/AssetRegistry/AssetData.h:910-911`, 5.3 `Runtime/AssetRegistry/Private/PackageReader.cpp:613`, 5.3 `Runtime/AssetRegistry/Private/PackageReader.cpp:735`, 5.3 `Runtime/CoreUObject/Public/UObject/PackageFileSummary.h:335`, 5.3 `Runtime/AssetRegistry/Private/AssetDataGatherer.cpp:3814-3842`, 5.8 `Runtime/CoreUObject/Public/AssetRegistry/AssetData.h:964-965`, 5.8 `Runtime/AssetRegistry/Private/PackageReader.cpp:1126`, 5.8 `Runtime/AssetRegistry/Private/PackageReader.cpp:1261`, 5.8 `Runtime/CoreUObject/Public/UObject/PackageFileSummary.h:365`, 5.8 `Runtime/AssetRegistry/Private/AssetDataGatherer.cpp:5100-5109`; measured by tools/test_import_health_report.py IH111 (--allow-disk) on a scratch project (5.8.2, port 8871) and a scratch project (5.3.2, port 8872), 2026-09-26","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE package headers and the asset registry - what a reader knows about a package without loading it"},{"id":"PH2","fact":"A package's registry dependencies include some its import map and soft lists never show: dependencies it records for its cook, marked `EDependencyProperty::Build` (\"used in the transformation of the source asset during cooking in addition to being required in the game or editor\"), which a `Soft` dependency query returns together with the soft references. Measured on 5.8.2: /Engine/EngineMaterials/EnergyConservation/SheenLegacy_Energy carries an editor-only soft Build edge to /Engine/Transient (`get_dependencies` includeProperties: hard false, game false, build true) that a header reader cannot see, so over all of /Engine the references to the transient package read 21 live against 20 in tools/import_health_offline.py. `import_health_report` asks for `Soft` plus `NotBuild` for the file's soft references and judges Build edges apart (kind build); test_import_health_report IH104 then reads 20 = 20 on 5.8.2 and 0 = 0 on 5.3.2.","source":"5.3 `Runtime/CoreUObject/Public/Misc/AssetRegistryInterface.h:98`, 5.3 `Runtime/CoreUObject/Public/Misc/AssetRegistryInterface.h:130-131`, 5.8 `Runtime/CoreUObject/Public/Misc/AssetRegistryInterface.h:76`, 5.8 `Runtime/CoreUObject/Public/Misc/AssetRegistryInterface.h:126-128`; measured with get_dependencies and tools/test_import_health_report.py IH104 on a scratch project (5.8.2) and a scratch project (5.3.2), 2026-09-26","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE package headers and the asset registry - what a reader knows about a package without loading it"},{"id":"PH3","fact":"One package can hold two registry edges to the same asset, a hard editor-only one and a soft build one, so `hard:true` plus `hard:false` counts can exceed the referencer count: /Engine/EngineResources/Black on 5.7 has 172 referencers, 160 hard and 16 soft, four packages in both. Compare package sets against per-edge reads.","source":"5.7 DependsNode.cpp:326 (one FAssetDependency per edge); measured with get_referencers, tools/test_ue_reference_graph_filters.py T9442","engine":"5.7","ue":["5.7"],"bl":[],"section":"UE package headers and the asset registry - what a reader knows about a package without loading it"},{"id":"U455","fact":"**UE 5.8 cannot run a project whose asset-registry cache folder path is long: the editor takes a system-wide mutex named <asset-registry cache root>/AssetRegistryCacheLock, and the Windows system-wide mutex checks that its name is shorter than MAX_PATH (260), so a deep enough project asserts about ten seconds after start, with or without any plugin. 5.3-5.7 take no such lock.** Measured 2026-09-28: a Blueprint-only project 240 characters deep asserted \"InName.Len() < 260\" on 5.8.2 with MifBridge disabled, and the same depth ran 113 s on 5.3.2 and answered 19 bridge calls.","source":"read: 5.8 `Engine/Source/Runtime/AssetRegistry/Private/AssetDataGatherer.cpp:638`, 5.8 `Engine/Source/Runtime/Core/Private/Windows/WindowsPlatformMutex.cpp:17`; no system-wide mutex in AssetDataGatherer.cpp on 5.3 (5.3.2 source) or on 5.4-5.7","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE package headers and the asset registry - what a reader knows about a package without loading it"},{"id":"U567","fact":"**UnrealHeaderTool still parses the headers of a project plugin's modules marked `bUsePrecompiled = true` in their Build.cs, and a plugin with no Source folder at all fails UBT with \"Could not find definition for module\".** A binary plugin whose headers reference another plugin's types that ship without headers therefore breaks every build of the project, Live Coding included: UHT reports \"Unable to find 'class' with name ...\" in the precompiled plugin's headers. What works is the shape binary-only marketplace plugins ship in: each module folder keeps only its `<Module>.Build.cs` with `bUsePrecompiled = true` and no headers or cpps. After that change the project's cached `Makefile.bin` and `<Target>.uhtmanifest` still listed the removed headers (\"File not found\"); renaming both made UBT regenerate them, and the build succeeded.","source":"measured 2026-10-16 on UE 5.8.3 (launcher build): three successive UBT runs, each read from `%LOCALAPPDATA%/UnrealBuildTool/Log.txt` (Source with bUsePrecompiled: UHT errors; no Source: RulesError \"Could not find definition for module\"; Build.cs only plus a regenerated makefile: UHT passed, then a full `Build.bat` editor target Succeeded)","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U631","fact":"**UnrealBuildTool runs of different engine versions under one Windows user share per-user files, so a running build of one engine blocks another's: (1) `%LOCALAPPDATA%\\UnrealBuildTool\\Log.txt` - a second UBT fails at start with `IOException ... being used by another process` in `Log.BackupLogFile` (worked round with `-Log=<own path>`); (2) `%LOCALAPPDATA%\\UnrealEngine\\Intermediate\\Build\\BuildRules\\MarketplaceRules.dll` - the second UBT fails in `DynamicCompilation.CompileAndLoadAssemblyAsync` with the same IOException and no workaround but waiting. `-WaitMutex` does not wait for either (its mutex is per engine).** A UE 5.3 probe build and a UE 5.8 project build cannot overlap.","source":"Live, 2026-10-19: UE 5.8.3 UBT console output (BuildMode.cs:143 / Log.cs:275 for the log; DynamicCompilation.cs:380 for the DLL) while a UE 5.3 Build.bat of another project ran (process command lines read from Win32_Process).","engine":"5.3, 5.8","ue":["5.3","5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"}]