Back to Blog
    Last Week in Unreal

    Last Week in Unreal: The Week Epic Made Its Failures Talk (July 27 to August 2, 2026)

    Venkat S38 min read
    Last Week in Unreal: The Week Epic Made Its Failures Talk (July 27 to August 2, 2026)

    Unreal Engine Weekly: The Week Epic Made Its Failures Talk

    Week of July 27 – August 2, 2026 | 822 commits on ue6-main, 254 on ue5-main | Branch: ue6-main

    Overview

    A stabilization week with two large refactors running underneath it. 77 of the 165 commits worth reading on ue6-main are crash, hang, deadlock, or teardown fixes, and what connects them is diagnostics rather than volume. Epic kept taking failures that were silent, misattributed, or undiagnosable and making them loud and specific. The crash reporter learned to report its own death (394ead63). Epic built a commandlet that fingerprints Nanite builder output so two changelists can be diffed, because it suspects the builder is not deterministic (10496625). A test harness converted an 1800-second silent hang into a bounded failure that reports in about 240 seconds (e36e274c). UBT stopped printing only the two endpoints of a broken plugin dependency and started printing the whole chain (cc39075c). The counterexample is a one-file commit nobody would stop on: enabling the HTTP server discarded the connection origin filter, leaving the allowed-origins allowlist unenforced on the PixelStreaming2 players signalling server (a62fabd2). A live auth bypass that existed because a failure was quiet.


    FScene decomposition has escalated at every full-volume week since early June, and it is no longer additive. Last week it broke public API. This week it changed the storage model itself. 24640f93 converts FScene’s packed parallel arrays (transforms, bounds, occlusion, visibility ids, proxies, scene infos, flags, component ids) to TDataArrayDirect keyed by persistent primitive index inside FScenePrimitiveDataRenderer. Frustum culling now iterates the id-allocator chunk mask, with 64-id chunks mapping to two visibility-map words, and per-view HiddenPrimitivesMap and ShowOnlyPrimitivesMap bit arrays replace per-primitive TSet lookups in the cull loop. Epic carved out two more extensions the same week: reflection environment data into FReflectionEnvironmentSceneExtension (a04f63cf) and the indirect lighting cache plus precomputed light volumes into FIndirectLightingSceneExtension, gated on IsStaticLightingAllowed() (4b44255b). Epic describes the reflection environment move as carrying minimal behavioral change; the indirect lighting move makes no behavior claim either way. The deletions tell you the direction of travel: unused scene proxy APIs (8919180a), unused AddStaticMeshes/UpdateStaticMeshes parameters that “would now lead to unexpected state if used” (8feb62fc), and deprecated HWRT code in FPrimitiveSceneInfo (07f66585).

    Build and toolchain work has been running for months: modernization, then correctness under failure, and this week outright deletion of dead layers. The BuildGraph bytecode VM is gone: the compiler and interpreter plus the graph builder path on July 28 (06683fd7, 010bbd83), then the entire expression tree in a 27-file cut two days later (187ddf19). Separately, the bundled DirectX SDK is retired in favor of the Windows SDK across 76 files (9d6d218d). Neither adds capability. Both remove a layer Epic no longer wants to carry, and that only happens late in a modernization program.

    The agent toolset arc is now four distinct phases deep: prototype, then crash hardening, then escaping the editor into cooked runtime, and this week a permission model. Twelve commits across July 30 and 31 tagged the toolset families with an AIAccessMode of read, write, or execute, after the ToolsetRegistry schema gold values were updated for annotations.accessMode days earlier (ccd0297c). Nothing this week enforces those access modes yet. What did land is a separate restriction layer, a shared property-write gate backed by FPropertyEditorPermissionList (98a0cf13), and a per-conversation undo manager with a dedicated transaction buffer (969825c7). Typed access levels and rollback semantics show up when someone expects the toolset pointed at a real project.

    On volume: the weeks of June 29 and July 6 were Epic’s summer shutdown, not a slowdown in the program. Exclude both weeks when you read the multi-week curves, or you will misread a holiday as a deceleration.


    TL;DR

    • The PixelStreaming2 players signalling server was silently ignoring its AllowedOrigins allowlist whenever the HTTP server was enabled, which is a live auth bypass and the single cheapest thing to fix this week (a62fabd2).
    • FScene converted its packed parallel arrays to persistent-primitive-index keyed storage and removed FSceneInterface::GetPrimitiveSceneProxies and GetScenePrimitiveComponentIds, so renderer forks will not compile without work (24640f93).
    • The C# BuildGraph expression API is gone: if you wrote C# BuildGraph scripts against the BgExpr family, they are dead on ue6-main (187ddf19, 010bbd83). The XML path survives, but it is not untouched. The legacy <Notify> and <Report> elements and the -ReportName and -ShowNotifications switches were removed the same week (301f225b).
    • Metal 4 landed as a second RHI backend alongside Metal 3, selected once at device init and gated by rhi.Metal.EnableMetal4 and rhi.Metal.ForceMetal3, with the third-party bring-up shipping the same week (8a469fb3, 51da534b).
    • On ue5-main, cook validation found that roughly 800 ControlRig and RigVM packages were being re-flagged on every incremental cook, and healed values got re-flagged with new values on the next run (acf33841).

    Highlights

    A live auth bypass in the PixelStreaming2 signalling server

    What changed. Enabling the HTTP server discarded the connection origin filter, so the AllowedOrigins allowlist went unenforced on the players signalling server (a62fabd2, UE-386542). The fix is small. The exposure was not. Separately, 715723eb hardens the MP4 box parsers against malformed box data across 8 files.

    Why this is important. If you run pixel streaming with a public endpoint and configured an origin allowlist, you may have been trusting a control that was not running. Nothing failed loudly. The filter ran on every connection, and its rejection was discarded before it could take effect.

    Who should care. Anyone shipping PixelStreaming2 with a public or semi-public signalling server. Media pipelines ingesting untrusted MP4 content should pick up 715723eb as well.

    Urgency: Act now.

    FScene’s scene data is being rebuilt underneath you

    What changed. 24640f93 touches 52 files and converts transforms, bounds, occlusion bounds, visibility ids, proxies, scene infos, flags, and component ids to TDataArrayDirect keyed by persistent primitive index in FScenePrimitiveDataRenderer. Frustum culling iterates the id-allocator chunk mask instead of a dense array. Per-view hidden and show-only bit arrays replace TSet lookups inside the cull loop. FSceneInterface::GetPrimitiveSceneProxies and GetScenePrimitiveComponentIds are removed outright. The change reaches Lumen, Nanite, PCG, HairStrands, and the culling paths. Two scene extensions split out the same week (a04f63cf, 4b44255b), 76539c2a adds an IsDrawnChanged dirty array explicitly staged for downstream use, and ae5556ad routes transient renderer arrays through SceneRenderingAllocator to cut per-frame malloc churn.

    One real correctness fix hides inside the refactor. ea0392bf replaces inherited task-access-context propagation in SceneData with an explicit copyable FTaskAccessHandle captured via CaptureTaskAccess() and opened with FTaskAccessScope. Under the old inherited model, any task launched from a managed task body could transitively capture the external access table and outlive the contexts it pointed at, producing a dangling-context crash in FBaseAccessContext::BeginReadAccess.

    Why this is important. Two removed public accessors on FSceneInterface are enough to break a fork on sync. The storage-model change is deeper than that: if you have custom culling, custom primitive iteration, or anything that walked FScene’s parallel arrays by dense index, the assumption that index N in one array corresponds to index N in another no longer holds the way it did.

    Who should care. Anyone maintaining a renderer fork, custom scene extensions, or engine-side culling modifications. Note that c2f51354 moved scene data headers to Engine and RenderCore public to unblock a Renderer refactor and calls the arrangement explicitly temporary, so do not build against it.

    Urgency: Act now if you maintain a renderer fork. Track otherwise.

    The C# BuildGraph VM is deleted and the DirectX SDK is retired

    What changed. BuildGraph’s bytecode virtual machine is gone. 06683fd7 deletes BgCompiler.cs and BgInterpreter.cs. 187ddf19 removes the entire expression tree across 27 files: BgExpr, BgBool, BgInt, BgString, BgList, BgEnum, BgObject, BgFileSet, BgFunc, BgThunk, BgType, BgAgent, BgNode, BgAggregate, BgLabel, BgAnnotation, BgGraph, plus BgOpcode.cs, BgBytecodeWriter.cs, BgObjectDef.cs, and the bytecode-only serializers. 010bbd83 removes the BgGraphBuilder path. C# BuildGraph scripts now construct a BgGraphDef directly through IBgGraphDefBuilder.CreateGraphDef(). The XML path survives, but it is not untouched. 301f225b removes legacy <Notify> and <Report> support, inert under Horde because ExportForHorde() never serialized them, with node-level NotifyOnWarnings surviving. aaed92dc collapses BgOptionDef to its string variant.

    Separately, 9d6d218d retires the bundled DirectX SDK in favor of the Windows SDK: 76 files, 117,916 lines, deleting every bundled DirectX import library and the whole Engine/Source/ThirdParty/Windows/DirectX/Include/ header tree, switching DX11, DX12, DirectSound, and Text3DEditor to PublicSystemLibraries. Commit.gitdeps.xml drops 77 lines. XInput.lib stays bundled, with a stated reason: SDKs older than 10.0.28000.0 ship an import library whose exported DllMain symbol ld-lld resolves incorrectly. Related cleanup landed in 08151c83 (DX11Audio third-party module removed), ea46e9da (unused dxerr.lib linkage dropped from Electra media modules), 2f39ad72, and 921dfcf3 (writable DirectX path properties marked obsolete).

    Why this is important. Both change a contract you build against. The BuildGraph deletion breaks source compatibility for anyone who invested in C# graph authoring. The DirectX change alters what a Windows build machine has to have installed, and which version.

    Who should care. Build engineers, anyone maintaining custom BuildGraph automation, and anyone who owns Windows build machine provisioning.

    Urgency: Act now for build engineers.

    UBT and the toolchain keep tightening, with a concrete compile-time win to copy

    What changed. 8a5771cb is the item worth copying directly. Two include-hygiene cuts identified from ClangBuildAnalyzer captures of UnrealGame Win64 Development rebuilds under clang 20.1.8. PBDRigidsSolver.h dropped from 2455ms to 1210ms average parse, a 43 percent machine-normalized improvement across 29 unchanged consumers, and 71.2 seconds to 35.1 seconds in total. PBDRigidsEvolutionGBF.h fell out of the top-ten most expensive headers. Both removed includes stay behind UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_9, the first clear marker of where the next deprecation gate sits.

    Around it: target rules moved onto source-generated readonly wrappers (cf9576a9, c5183cac, the second noting Horde preflights were not rebuilding UAT and were “leading to spurious failures”), duplicate target names now detected with a best-fit pick or an early error (98b0f444), full plugin dependency chains reported in sealed, disallowed, and deprecated validation errors instead of just the two endpoints (cc39075c), full paths allowed in ModuleRules.PublicDelayLoadDLLs on Windows (ff421d8d), and the old UBT solution file deleted (a4d3107e). On Linux, a6887a39 fixes hot reload failing on the import-library stub response file by routing stub creation through a LinkImportLibrary() override, so the stub becomes a declared module output. a0243626 fixes three separate LinuxServer problems: merged modules needing the lib<exe>.so soname form, the merged-module check for EOSSDK missing the Server target type, and a shutdown crash from global destructor ordering.

    Cook and packaging picked up two items. 67274713 adds an IoChunkAssigner encapsulating IoChunk splitting, so game teams can route bulkdata payloads to arbitrary containers without deep IoStore knowledge, and it auto-updates the package header’s BulkDataMap. 16646740 downloads previous chunk assignments and writes a comparison diff of assets against prior cooks.

    The shader preprocessor (not the shader compiler backend, and not the Verse compiler) got real memory-safety fixes in the vendored stb_preprocess C code (0c116a4d): an unbounded reverse whitespace walk that ran off the front of the buffer into the preceding array header, int length and capacity fields against a size_t API producing near-SIZE_MAX allocations, mis-ordered SSE read padding, and copy_argument failing to write back on three EOF paths. 5248e080 adds unit tests derived from live crash-log asserts, and 31a36269 dumps the pipeline job input hash alongside the stage hash in pipeline shader debug info.

    Why this is important. The 43 percent header parse reduction is a template for work you can do yourself today with the same tooling. The UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_9 gate is advance notice.

    Who should care. Build engineers, anyone with long iteration times on Chaos-dependent modules, and Linux server teams.

    Urgency: Act now on the include hygiene technique. Track the rest.

    The crash reporter learned to report its own death

    What changed. When the crash reporting thread crashed while handling a crash, every diagnostic for the original crash vanished and exit code 777003 was the only signal you got. 394ead63 writes a last-resort minidump from the inner exception filter using only Win32 APIs and static storage. Because the original crashing thread is still parked in its own exception filter waiting on the reporting thread, a single dump captures both the original crash and the reason the reporter died. Stack-overflow crashes skip it outright. 8cd588f2 makes monitor-mode CrashReportClient attach that dump to the synthetic AbnormalShutdown report, only accepting dumps timestamped after the monitor started so stale dumps are never picked up, and translates the exit code to 777003 (CrashReporterCrashed). 22378fb0 stops the crash-time thread callstack walk from dereferencing threads destroyed during exit, which was itself crashing the crash handler.

    Teardown ordering and lock inversion got a matching sweep. ae9683b9 fixes a server hang where DumpAllocatorStats grew a 256-element inline array while holding ClassesLocks, letting a layered malloc re-enter and deadlock. The commit notes servers hit this reliably because one size class commonly holds well over a thousand pools. 7409fd9a fixes a POSIX PTY shutdown deadlock in the Terminal plugin with a self-pipe wakeup, because on Linux the reader’s own reference keeps the open file alive, so closing the master descriptor signals nothing. 723ecc38 stops AddConsoleObject() from deleting a replaced console object that a hot-reloaded module’s static FAutoConsoleObject will later pass to UnregisterConsoleObject() at process exit. 52736778 returns ELoaderType::NotInitialized instead of dereferencing a destroyed async loader during the final purge.

    Sequencer got a null-guard sweep across the week from two authors, five of the seven landing on July 30 and 31: Universal Object Locator fragment widgets polling property handles for objects that are gone (0d5ba46e), bound objects with no level where a spawnable template is outered to the UMovieScene (6812bdad), a fatal assert when a track has no registered track editor, seen in the field with a UMovieScenePropertyTrack added through the scripting API (6ac6cecc), AGroupActor::PostLoad dereferencing a null level after conversion to a spawnable (7d8a7f4a), animation with no skeleton (87b69159), keying object-reference array elements read through the enclosing FArrayProperty plus soft object refs being keyed as a null resolved UObject* (ce2539f6), and FValueCache holding its owner as a raw UObject* so every cache built for an actor’s components and instanced subobjects dangled on Blueprint reinstancing (7081461e).

    Why this is important. Exit code 777003 with no dump is the worst crash report you can get. It tells you a crash happened and nothing else. If your telemetry has a bucket of unexplained abnormal shutdowns, this change turns part of that bucket into readable data.

    Who should care. Anyone operating crash telemetry at scale, dedicated server teams, and Sequencer-heavy productions.

    Urgency: Track. Act now if the Sequencer fixes match crashes you are already seeing.

    The AI editor toolset gets a permission model and per-conversation undo

    What changed. Around 30 commits touch the Python editor_toolset tree, the ToolsetRegistry plugin, and an AIAssistant plugin. About a third of them resolve only to Commit.gitdeps.xml, so the Python-side work is invisible in the public tree. The largest single source-visible change is 969825c7 in Engine/Plugins/Experimental/AIAssistant, which adds FAgentUndoManager with two modes. In global stack mode, agent edits are ordinary transactions and a reject unwinds back to the index at the agent’s first tool call, taking any interleaved user work with it. In per-agent stack mode, each tool call runs inside a capture window with a dedicated transaction buffer swapped over GEditor->Trans, teeing pre-state into a reinstancing-safe tagged record store, with a per-object guard so objects the user hand-edited afterward are skipped on rollback. The commit explains why it cannot simply unwind the buffer: flip-applying engine records can crash on reinstanced objects. It carries two backout tags, ships 1,117 lines of implementation against 1,003 lines of test, and is wired up in 3a4df67e.

    The access-mode sweep ran across July 30 and 31 in twelve commits, tagging AI-callable functions with AIAccessMode values of read, write, or execute: EditorToolset (f53a203a), SlateInspectorToolset (ead4ec67), physics and simulation (f0c64976), Gameplay Ability, Tags, and Features (c18941fc), UMG and MVVM (46e75609), PCG (afcdd21c), Niagara (b70033ad), StateTree, WorldConditions, Conversation, and AIModule (8f4d4ba3), diagnostics and testing (7aaaf7cf), Geometry (0dbb7533), and MetaHumanGenerator (a0969299), with a follow-up tagging a new RewindDebuggerToolset function (01667eaf). The ToolsetRegistry schema gold values were updated for annotations.accessMode (ccd0297c). Two commits give those annotations teeth. 98a0cf13 adds a shared UE::PropertyEditor::CheckCanSetProperty gate with FPropertyEditorPermissionList support and routes both ToolsetRegistry and PythonScriptPlugin property writes through it, and 39628b49 adds a UToolsetLibrary::IsClassAllowed helper gating UMG and MVVM toolset object creation in UEFN.

    The rest is production plumbing. c3107491 rejects scripts that use long-running blocking operations, with the stated goal of steering models toward non-blocking scripts, and PythonScriptingTools.execute_python went async for the same reason (c06ac6a8). execute_tool_script now takes a CodeBlock rather than a plain string (b4a6cbae). GLB and GLTF now route through Interchange in StaticMeshTools.import_file instead of failing on FbxFactory (587b0878). Getters and setters for SCS component variables, function locals, and inherited members can be recreated again, and list_variables gained an explicit VariableScope (85d33e3b). 9b06e4f4 fixes an enum default that broke JSON schema generation at editor startup. And 03839cb5 fixes four automation tests that keyed a node map on str(guid), which fell back to a struct repr embedding the wrapper’s heap address, so pass and fail “came down to allocator luck.”

    The collateral is instructive. cddc9334 backs a no-op guard out of UAssetEditorSubsystem::OpenEditorForAsset and relocates it to the AI toolset entry point, because the subsystem-level guard silently broke Content Browser double-click-to-reload. eb1ad3af fixes a UMG async-load deadlock “when MCP tries to compile and has to sync load data.” 486f5f17 fixes a UMG Toolset crash renaming widgets with a null parent class. And ae501b76 replaces a module-scope import requests with the standard library, after it broke editor Python startup on any machine without a hand-installed package.

    Worth noting without speculating: 0111a4cd adds a TPS and license entry for Meta Llama 3 8B Instruct under a tag reading “[Kimodo]”, and 90d4c03d adds an initial Kimodo plugin for something called the “AnimGenOffline framework.” The commit messages do not say what Kimodo is, and we are not going to guess.

    The same week, 15 commits added 58 distinct README.md files: Core and CoreUObject (ca134812), four Chaos modules (98689430), seven MetaSound modules (1f1c36c0), 21 Concert modules (6034ea0f), Gameplay Cameras (f6b9545b), StateTree (5f2cff5f), Enhanced Input (5b4f69ac), and TaskGraph (91d54540), plus an async-loading doc that landed as AsyncLoading.md rather than a README (284b2991). The Concert commit spells out the format: YAML frontmatter with name, purpose, and status, canonical sections, and content that is novel, durable, specific, and actionable. Machine-parseable per-module documentation landing in the same week the agent surface gets typed access levels is context plumbing for models, whatever else it does for humans.

    Why this is important. Access modes, a property-write permission gate, and per-conversation undo are the three things you need before you let a model touch a real project. Their arrival together is a schedule signal.

    Who should care. Tools teams, anyone planning editor automation policy, and UEFN developers, who are already inside the enforcement path via 39628b49.

    Urgency: Track.

    Metal 4 lands as a complete second backend

    What changed. 8a469fb3 adds an MTL4 backend alongside the existing Metal 3 path across 73 files. FMetalDevice gains bIsMetal4Device, set during feature enumeration for Apple7+ GPUs on macOS 26 or iOS 26 and newer, gated by rhi.Metal.EnableMetal4 and rhi.Metal.ForceMetal3 plus the -metal4rhi and -metal3rhi switches. A new IMetalCompiler interface splits into FMetal3Compiler and FMetal4Compiler. Encoding splits into FMetal3CommandEncoder and FMetal4CommandEncoder, both final non-virtual classes over a shared FMetalEncoderCommon. FMetalRHICommandContext becomes an abstract encoder-less base with TMetalRHICommandContext<TEnc> per encoder and if constexpr (TEnc::bIsMetal4) branching. The commit never states the cvar default, and it is not a pure addition either: MetalFence and the dead Intel-Mac paths are deleted outright.

    The dependency shipped the same week under a title that gives away nothing. 51da534b reads “Metal RHI” with a five-word body, “Upgrading MSC for Metal 4,” and carries 110 files and 24,478 lines: 41 new headers, 34 of them MTL4*.hpp under Engine/Source/ThirdParty/MetalCPP, including MTL4CommandBuffer, MTL4Compiler, MTL4CommandQueue, MTL4AccelerationStructure, and MTL4ArgumentTable. The rest are edits to existing MetalCPP headers. Five more Metal RHI commits landed in the same window: bindless descriptor limits with loud failure on heap exhaustion (0b220fce), a Mac zero-sized viewport backbuffer assert (a87d34fb), an unavailable-drawable assert plus VSM render-target-array validation error (89c6c04b), and re-enabled concurrent dispatch (5211d317).

    Mobile plumbing moved too. e7a4fd6d adds archive-based serialization of Vulkan structures with a Python generator (GenerateVulkanArchive.py) that reads the Vulkan registry, using one shared function for serialize and deserialize to guarantee identical ordering and identical feature structures for device creation in both the app and the service, so the Android PSO compilation service needs less hand-written support for each new extension, though the commit calls itself a first draft. 1a47b49f wires input-latency telemetry through VK_EXT_present_timing and VK_KHR_present_id with a VK_GOOGLE_display_timing fallback into the Android frame pacer, and enables PLATFORM_SUPPORTS_FLIP_TRACKING on Android and Linux. c0b5f55c forces discard to the very end of the GLES mobile base pass pixel shader to dodge Adreno’s “Bary after KILL” crash on a specific character material. 653c7c75 binds a depth-stencil sink in DrawTileMesh‘s translucent path to fix a MetalRHI assert. On Apple platforms, ab401d17 moves LLM SetProgramSize() out of BaseAllocator(), which runs before GMalloc exists, into FApplePlatformMemory::Init(); c0167ef9 wraps MarketplaceKit Swift-task completion delegates so game-thread work does not crash; and 8ac39446 adds iOS GameMode via GCSupportsGameMode and LSSupportsGameMode plist values, enabled by default.

    Why this is important. A second full RHI backend is a multi-release commitment, and the shape of 8a469fb3 (abstract base, templated context, compile-time branching) says Epic intends to keep both alive rather than migrate and delete. If you have Apple platform RHI modifications, you now have two paths to maintain.

    Who should care. Apple platform teams and anyone with Metal RHI forks. Android teams should read e7a4fd6d and 1a47b49f.

    Urgency: Act now for Apple platform engineers. Track otherwise.

    Nanite and MegaLights get shaken out against real content

    What changed. 10496625 adds DumpNaniteFingerprintCommandlet, which builds Nanite data for a UNaniteDisplacedMesh or UStaticMesh and dumps a deterministic text fingerprint (FResources CRCs, per-page CRCs, layout counts, bounds) for A/B comparison across changelists. It is filed under a Nanite non-determinism investigation (UE-381258). One purpose: make a suspected nondeterminism provable.

    CLAS allocation got the same treatment. 044f751c makes AllocateClustersCS report allocation results on both success and failure, tracks the actual required blocks per installed page from CLAS build results, and puts the required CLAS buffer size in the on-screen warning. It also fixes the dispatch running one thread per cluster instead of one per page. 870d608c auto-raises MaxGeometryIndex and MaxUniqueGeometryCount from uploaded segment mappings and logs a cvar recommendation when the growth causes expensive invalidations. fbb53907 renames NumAssemblyParts to NumAssemblyTransforms and fixes an off-by-one in GetMaxEmittedClusters.

    On the lighting side, f2050b2f force-enables cast shadows on first-person primitives because MegaLights needs the flag for screen traces to pick up weapon geometry, and the commit itself says “in the future we should fix it properly.” 7363fa81 adds per-FViewFamily overrides for sample count, guide-by-history, and reference passes for deterministic rendering, opt-in, and byte-identical to before when there is no override and a single view. 1959981c relaxes stochastic-upsampling normal weights so fewer Nanite foliage voxel pixels lose lighting on disocclusion, offsetting the cost with ALU and shared-memory tweaks (0.62ms to 0.59ms at 540p on one console, unchanged on another). 4ad3bfd7 fixes path tracer Substrate refractive slab handling, where IOR of 0 is now treated as IOR of 1, thin-slab transmittance was using mean free path instead of its inverse, and random-walk SSS is now reserved for the opaque case, plus a thin-surface transmittance visualization mode. c8203568 makes SkyAtmosphere LUT resources persistent instead of recreating them on every component modification.

    Three more rendering changes are easy to skim past. 9389850e adds a Substrate Volumetric6AxisBSDF for translucent meshes that works with Lumen and the new material translator, across 19 files including three .ush and one .usf. f57449b7 is a 29-file refactor moving runtime reflection captures under the Ambient Lighting Volume component so they coexist with baked captures, fixing black, flickering, and missing captures. 54abee34 adds a Rendering Quality property (High, Medium, Low) on local lights with per-tier feature fade for mobile deferred (UE-380357).

    Why this is important. CLAS allocation used to just fail. Now it prints the buffer size you needed, which turns a bug report into a config change.

    Who should care. Anyone shipping Nanite at scale, MegaLights adopters, and mobile deferred teams.

    Urgency: Track.

    Epic is building a multi-process test harness inside the engine

    What changed. 31 commits landed in seven days across the ModularTestFramework plugin, its Runner UI, its test plugins, and a new ModularTestHub program, and no single one of them looks like much. Together they are a pre-booted worker pool for networked end-to-end tests: warm slots that drain mid-transition between claims (1768b994), join-in-progress provisioning exempted from the per-test budget with stale connections drained before claim (3f3456c6), per-role wire-flag heartbeats so a wedged slot is visible during a wait rather than after it (97293749), config-driven extra arguments for spawned server workers (800140d8), and async loading thread enabled for server workers (d17619e2). Suites declare and are scheduled by environment (31bf0d6c, b2eeb1db). Test plugin .uplugin files stage into cooked builds with modules denied from Shipping (ae4f1dae). A standalone ModularTestHub program host appeared at Engine/Source/Programs/ModularTestHub/ (460d1d90). Live run status, phase events, and wait heartbeats landed as a 26-file change (d6b2364c), with a matching Runner UI (28162a47).

    Note the failure-mode language running through it. e36e274c converts an 1800-second silent hang into a bounded reset-reaper failure that reports in roughly 240 seconds.

    Why this is important. Multi-process client and server test harnesses are usually something a studio builds itself in an external automation layer. Epic is putting one in the engine, staged into cooked builds. If you have your own, it is worth knowing what the first-party version will cover before you invest another year in yours.

    Who should care. Automation and QA infrastructure owners on networked titles.

    Urgency: Track.

    Networking, replication, and the one-off fixes that will bite specific teams

    What changed. 89c9f3a6 makes Iris NetObjectFactory IDs independent of module load order. Factory registration is now ordered, core factories are pinned to IDs 0 through 2, and the rest are sorted alphabetically, with per-factory logging on the locked table. This matters specifically for anyone registering replication factories from GameFeature plugins, where client and server load order diverges. a0d09021 fixes an Iris client GC leak of replicated entity components on level unload. e825a46d fixes Game Feature plugin unmount not cancelling in-flight preloads, where the imported package survived GC via the loader refcount. b7548e9c adds a bEngineInitSafe gate with replay because external RPC route handlers could be serviced before FEngineLoop::Init returned.

    Four other one-offs are worth naming. ee2a254a fixes Blueprint uber-graph persistent-frame object arrays being kept as hard references, which pinned a level actor and its World and tripped the world-GC-leak fatal on map teardown. It was a side effect of an earlier changelist that stamped all Blueprint object properties as CPF_TObjectPtr, rerouting the frame arrays to an FObjectPtr* collector overload nobody had overridden. 00616fbd moves Material Parameter Collections to persistent shared uniform buffers per ID rather than one per collection instance, fixing crashes when the same collection appears in multiple packages and one gets unloaded, leaving stale references in cached mesh draw commands. 62fa0c6b fixes cook not fully recording shadermap-to-asset info for sibling MICs sharing a configuration, which manifests at runtime as “LibraryInstance is null” when a shaderlib unload takes out a leaf shadermap still in use: a cook-time attribution bug with a runtime-only symptom, which console teams tend to find late. 1504a004 fixes D3D12 reserved-resource LLM tracking to apply the right scope marker on the submission thread and keep reserved buffers on their owner tag instead of RHIMisc and BufferPool, which will change your numbers if you have been chasing unattributed D3D12 memory.

    On Control Rig, c47055e4 changes override owner-structs to load via version-gated dynamic imports instead of a StaticLoadObject workaround that deadlocked the cook, and adds a re-entrancy guard on RecompileModularRig fixing random heap corruption from nested recompiles. It was re-landed after a backout. 7b641d6c fixes ImportBones(USkeletalMesh*) indexing the skeleton-to-mesh lookup table with mesh bone indices, broken for several releases but unreachable until a changelist last week started calling it, “which is why the crash does not reproduce in 5.8.”

    Several concurrency fixes landed quietly, none of them phrased in a way that would catch your eye on a skim: two TSan races in TraceLog block pool acquire and release ordering and in PThread priority initialization racing the child thread (f9dda313), an ASAN fix in CExprTupleElement (469f88a2), a Niagara ASAN type registry slot rewrite (ca04b423), Verse race arms continuing to run after the race resolved and registering awaiters nothing cancelled, so a later signal resumed a continuation reading an unset result slot (c1fb5194), and a DerivedDataCache pak writer shutdown race (6569c1fe, UE-388315).

    Two more with content impact: d65fcac0 fixes an HLOD hash that was order-dependent and dropped caller-accumulated settings, so HLOD setting changes did not trigger rebuilds. Existing HLODs rebuild once (UE-388016). 564735b8 excludes UE_SERVER from UE_ENABLE_DEBUG_DRAWING, so DebugDraw no longer links Renderer into servers (UE-385930). And a63961db fixes ConcertSyncCore.Build.cs adding SQLiteCore unconditionally against its own uplugin deny-list, silently linking it into game clients.

    Why this is important. These are the fixes that will not show up in a release-notes skim but will match a bug you already have open.

    Who should care. Iris and GameFeature users (act now if you use both together), console teams, and anyone tracking D3D12 memory attribution.

    Urgency: Act now for Iris plus GameFeature plugin users. Track the rest.

    Horde, test infrastructure, and the ProceduralVegetation plugin

    What changed. 91b81d1a stops an issue spanning multiple streams from being marked fix-failed in a stream that has since had a successful run, along with the misleading Slack “Not fixed by” messages that followed. The author is explicit that this is a workaround, not a root-cause fix: doing it properly would mean parsing changelist descriptions for RoboMerge tags or querying RoboMerge directly. 606aa83b publishes TestData to a telemetry sink. Outside Horde proper, a4f79d61 adds building low-level tests for a given module set through UnrealBuildTool. and b8300dbb restricts cloud artifact publish and retrieve in the Foundation Cooker test graph to build machines, fixing a race in the Gauntlet incremental cook test.

    The experimental ProceduralVegetation plugin shipped four crash-fix commits and a 255-file preset library in one week. ce587b30 fixes Nanite foliage meshes crashing packaged builds because cooked builds strip the CPU index buffers unless bAllowCPUAccess is set, while GetNumIndices() still returns the cached count, a regression from 5.7.4. 5084a78e fixes an open URange bound asserting during UV generation. 43b603b7 and fa747ac4 add two more. 8352feda adds 255 growth presets converted by script and explicitly notes they are not yet QA’d.

    Why this is important. A fix-failed flag that was wrong, plus Slack notifications amplifying the wrong state. If your team has learned to distrust Horde’s fix-failed signal, this is why.

    Who should care. Build automation owners running Horde, and anyone maintaining low-level or Gauntlet test graphs. ProceduralVegetation is experimental, and the preset library is explicitly unvalidated, so treat it accordingly.

    Urgency: Track.


    The 5.x Stabilization Sidebar

    ue5-main ran 254 commits, 242 of them worth reading after the routine churn comes out, and nearly 50 mention a crash, assert, out-of-bounds, use-after-free, deadlock, or heap corruption. 152 carry an internal #Synced-CL integration tag and 26 explicitly say cherry-pick, so the branch is absorbing fixes from elsewhere far more than it is originating its own work. Commit counts by subsystem understate where the week actually went. Measured by file touches, the week is Control Rig (roughly 370), the experimental UAF plugin (roughly 300), RigVM (over 100), and RoboMerge (85). Two changes landed, were backed out, and re-landed inside the same week (UE-381080 and the RigVM serialization sort), which matters if you sync ue5-main tip. Seven of 242 commits resolve only to Commit.gitdeps.xml.

    One caution on sourcing: no commit message on this branch names ue6-main as a source, so we are not calling any of this a backport. The only literal cross-branch statements are 3d3ff86f (“Bink: Back merge from Main”), the 26 cherry-pick tags, and 1e16ebbe, whose message reads “Avoid usage of UE6 function in cherry picked CL.”

    The cook and derived-data layer got audited, and it was worse than it looked

    What changed. acf33841 is the headline. Cook IncrementalValidate was flagging roughly 800 ControlRig and RigVM packages on every single run, because a 4-byte FRigVMCallableInfo::FunctionHash in serialized rig bytecode differed between the on-disk artifact and a fresh save, and freshly-healed values got re-flagged with new values on the next run. Three cooperating defects sit underneath it, one of which is async load-order dependence deciding, per package and per run, whether the cooked bytecode embeds persisted data or data recompiled in-session. Around it: 441240aa fixes a rig asset minting a random GUID every cook instead of a deterministic one. 06929408 and 145e603a fix map serialization in raw TMap iteration order, which made byte-identical functions serialize differently across cook processes (1c30e76b is the backout of the first attempt). 2c214150 fixes cooks failing outright with “Compilation data for public function has no instructions.” 1cd565db fixes ComputeHLODHash both dropping settings and being source-component-order dependent, so HLOD setting changes were silently not triggering a rebuild. Existing HLODs rebuild once. d94abc9d fixes a shutdown race in the derived-data pak file cache writer.

    Why this is important. If you have been living with incremental cooks that never feel incremental on a Control Rig heavy project, this is a plausible explanation with a fix attached. It also means your incremental cook validation results before this change were not telling you anything real.

    Who should care. Anyone shipping on 5.8 with Control Rig or RigVM content, and anyone who owns cook determinism.

    Urgency: Act now.

    Crashes that only happen in packaged or runtime builds

    What changed. 47105f70 fixes ProceduralVegetationEditor-generated static and skeletal meshes crashing the packaged game on asset load in 5.8 when Nanite Foliage is enabled, explicitly a regression from 5.7.4, caused by reading CPU index buffers that cooked Nanite builds strip unless bAllowCPUAccess is set. It renders fine in the editor, which is why it survived. cc080b6f changes the default memory-mapped bulk data alignment from 1 to 16, because animations are memory-mapped on mobile and alignment of 1 violated the ACL anim sequence codec’s requirement, asserting at decompress time in Debug and Development builds. It touches BaseAndroidEngine.ini, BaseIOSEngine.ini, and SavePackage2.cpp, so it changes cooked layout and adds padding. d4b38151 adds a 2GB estimate-based limit to stop ray tracing acceleration structure builds for groom assets too large for scratch memory. c995dddf prevents a runtime crash transitioning into a linked StateTree asset that was recompiled with different parameters after the parent tree compiled. e742b0e0 null-guards landscape grass data deserialized from packages saved before the LandscapeGrassSingleArray format change. 5473a63e guards GPU crash diagnostic dumps against an invalid diagnostic buffer, so a device-removed event stops replacing your real crash report with a fault inside the crash reporter. That one is speculative with no repro in hand, and the commit says so.

    Why this is important. Every item here is a defect that passes in the editor and fails in a packaged build, which is the worst place on the calendar to find one.

    Who should care. Anyone in cert or approaching it on 5.8. Cherry-pick selectively based on what you actually use, and note that cc080b6f changes cooked layout, so it is not a free pick.

    Urgency: Act now, selectively.

    A systematic editor crash sweep, mostly Sequencer and the property editor

    What changed. A run of near-adjacent Jira tickets (UE-388963, 388964, 388968, 388969, 388971) from two authors over two days, with one pattern throughout: a pointer guaranteed non-null in the common case and not in a Sequencer context. 783795fc (bound objects with no level), 517d4b0f (a track with no registered track editor turning a handled ensure into a hard crash), 966c3146 (a group actor converted to a spawnable, therefore no ULevel outer), 6ac89a27 (animation with no skeleton), e435b5a2 (a locator whose property data went away mid-frame). 8e2c4fa9 is the most structurally interesting: a property change listener held its owner as a raw UObject*, so every cache built for an actor’s components dangled the moment a Blueprint recompile reinstanced them. It is now a TWeakObjectPtr. On Mac, 6075bcee, 3e629390, and 23b2dc0d fix Metal RHI asserts and a129e6f9 raises the bindless descriptor limit. 346641ec fixes buffer stomps and length underflows in the shader preprocessor’s macro argument handling, including an int against size_t mismatch that turned a bad length into a near-SIZE_MAX allocation. That mirrors 0c116a4d on ue6-main.

    Why this is important. These are cheap, low-risk picks for a team spending editor time in Sequencer.

    Who should care. Cinematics teams and Mac-based editor users.

    Urgency: Track.

    Control Rig is still getting new features on this branch

    What changed. a11fab2a is a 104-file commit introducing an entirely new constraints editing interface in the animation editor mode, gated behind the ControlRig.UseNewConstraintsWidget cvar with the old interface left unmodified as a fallback. Roughly a dozen follow-up commits landed the same week polishing it (70eafe2a, da94cded, f422f8a3, 550013e5), and a parallel rework hit the modular rig editor (d71114f9, 7a81b7b4). The UAF plugin accounts for 43 commits and 298 file touches, including Verse exposure (4329276e) and asset registry data versioning (b60200d8).

    Why this is important. Plan around this one. A branch described as post-5.8 maintenance is still receiving a cvar-gated interface replacement and active experimental-plugin development. If your integration policy assumes ue5-main tip equals 5.8 plus safe fixes, that assumption is wrong this week.

    Who should care. Anyone with a standing policy of syncing ue5-main tip, and animation teams evaluating the new constraints widget.

    Urgency: Track.


    Worth Tracking

    Frame pacing driven by task-graph timing, gated off by default. 9599726e pulls from GRHIFrameTimingQueue inside UEngine::UpdateTimeAndHandleMaxTickRate to derive a next-frame work forecast and a target deadline (the next vblank flip), then throttles the game thread through the same path t.MaxFPS uses, and adds a “Slack” field in stat unit. c5bfef96 removes FFrameResult and FRHIFrameTiming, computes work and slack directly on the result node, hands the final node reference back to the game thread, and adds a “continuation” flag on prerequisite links explicitly reserved for separating pipeline stages on the critical path later. d31cd58c adds a DynamicRHI hook for reporting input sample time to platform APIs. The whole game-thread path sits behind UE_TASK_TIMING_ENABLE, disabled by default, and requires platform RHI support (UE-306070). Two of the five commits are build fixes for the enabled case (c1b64712, 1a28752c), which means somebody is compiling it regularly. Worth tracking if you own frame pacing or input latency.

    The new material translator is in legacy-parity cleanup, which means it is being run against production content. Six commits landed in Engine/Source/Runtime/Engine/Private/Materials/MIR/ and surrounding translator files, all about matching what the legacy translator already does: static switches with disconnected inputs (18014ab9), pixel depth offset (580cbccc), terminating the analytic derivative chain at values with no closed-form derivative (29969f64), collapsing shading models on platforms without per-pixel shading model support (022d7a3e), gating SubsurfaceColor and Refraction on platform-filtered shading models (a684c16d), and Preshader statistics plus op byte count in the material editor, which needed separate stat-gathering code because the new translator uses Preshader2 (bf3822cf). 022d7a3e is the one to read: MIR was keeping every shading model the graph reached, which activated extra shading code in the base pass, kept the feeding textures alive, and pushed some character materials past the 16-sampler limit on Android. Parity bugs at that level of specificity do not come from synthetic test content. The commit stream calls it both “New material translator” and “[MIR]”, and the acronym is never expanded anywhere in the data, so we are not expanding it either.

    Convergent scaling work landed across the entity and transactional layers in the same week. 2545a03b replaces EntityFramework’s FExecutionManager full Tarjan rebuild on every register and unregister with incremental insert and remove, then got backed out later the same day (315c94f8). ff707682 adds optional incremental processing-graph invalidation, where previously every new archetype invalidated all graphs. 205e435c replaces processing-queue contention with a lock-free per-chunk 64-bit color mask. 58bec238 makes Mass IsProcessing() reliable across the async pipeline, where on the legacy parallel path (mass.UseProcessingQueue=false) the counter transiently read false between dependent tasks, so callers were picking direct mutation over the command buffer. It was backed out the next day (7845eb56) with no reland inside the week. On the transactional side, f026373a drops a redundant hit-set merge in AutoRTFM nested commit, removing roughly 44 million redundant hash insertions in a single raytracing benchmark run and taking FTransaction::CommitNested from 11.3 percent to 0.6 percent of samples, for 1.05x to 2.06x on named benchmarks. 59a6763d skips AutoRTFM bytecode for single-target client cooks in the Verse compiler, cutting UVerseFunction memory on clients by about 45 percent. And 47f88b39 splits UE_WITH_DSTM and introduces UE_WITH_TRANSACTIONAL_EXECUTION, re-gating ticks, timers, latent actions, RPCs, the control channel, level streaming volumes, async collision, and physics event dispatch across 27 files in Core, CoreUObject, Engine, Chaos, and Iris. That last one changes what a transactionalized build means, so read it before you assume your build configuration is unchanged.

    Also on the radar, at lower confidence: Subsonic shipped 12 commits into Engine/Plugins/Experimental/Subsonic/, building out an event, action, and executor audio model with parameter stores, a custom K2 node for setting executor parameters (cbec3c89), custom source parameter authoring (b1f03a14), collection parameter overrides with a type allow-list (198fc0a7), and editor-tab clipboard shortcuts. That volume of editor polish suggests it is past prototype. The Gaussian Splat plugin split out of Niagara into a standalone Engine/Plugins/GaussianSplat/ (7f267510, 39c0feb2), exposed bUseQuantizedFormat (167e4661), and added import and allocation error handling (6b109377). And 7c096c84 is a 92-file, 5,372-line commit exposing UAF systems to Verse and running skeletal_animations as UAF systems, so a uaf_system can now be played as a skeletal_animation from Verse and each skeletal_animation runs as its own system rather than a composited UUAFAnimGraph. It restructures Engine/Plugins/EntityFramework/Source/EntityModifiers/ along the way, deleting IEntityModifierInterface.h and adding IEntityModifierImplInterface.h, with reflection gated behind Verse.EnableVerseUAFSystemClassReflection.


    What We Ignored

    47 commits on ue6-main were pure noise: content syncs, autogenerated data, and mechanical churn with no reader value. Beyond that, we set aside RoboMerge’s submit-approval work, which ran 19 commits in Engine/Extras/RoboMerge/v3 including a 39-file restructure of changelist filters supporting approval as well as userlist and usertag filter types (f4f95626), an all-or-nothing filter (674e4413), and user tag caching (2373c52b). It is real work, but it is Epic’s internal integration tooling and it does not change anything on your machine. We also set aside the week’s churn of 13 backouts and 13 relands, several of them pairs within days: interesting as a signal about branch churn, not actionable individually. Two other items are worth naming as deliberate omissions rather than oversights. 22 of the 165 commits we read closely resolve to nothing but Commit.gitdeps.xml, and the entire Python editor-toolset and AI-agent workstream lands that way, so much of that section is read from commit messages rather than code. And af12b293 renames “Timing Profiler” to “Timing Insights” and adds a Profile section to the Tools menu, which will confuse exactly one person on your team for exactly one afternoon.


    Closing

    What matters in a stabilization week is which kind of fixes land, not how many. Epic spent an unusual amount of effort this week on making broken things say so: a crash reporter that survives its own crash, a commandlet whose only output is a fingerprint, an allocator that reports the size it actually needed, a hang converted into a timeout. The PixelStreaming2 origin filter is the argument for why that work matters. One file, no symptom, an allowlist left unenforced on a network-facing server. Nobody was ever going to file a bug against it, because it never said a word.

    Commit SHAs are cited inline, in parentheses, next to each claim. They reference the ue6-main branch of EpicGames/UnrealEngine except in the 5.x sidebar, where they reference ue5-main. Some of the most interesting entries resolve only to Commit.gitdeps.xml binary-manifest blobs: the commit message is real, but the code lives in Epic’s internal binaries, which we cannot see.

    Share this article