You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
STATUS 2026-09-02 — the fatal attribution is REFUTED
What is confirmed: the stale subclass (observed directly) and the escalation (46 → 2,109 lines across one Play→Stop cycle, measured).
What is refuted: that this mechanism is what kills the editor. A live wedged editor in the exact signature-C symptom state (main window gone, process spinning, log ending on WeaverBaseImpl) was captured with a non-invasive debugger attach across 221 threads: zero CallWindowProc/DispatchMessage/SendMessage frames, and zero SimulatedReality* frames executing anywhere in the process. The main thread was in mono_runtime_invoke — a managed call that never returned.
So this issue is a log flood and a window-subclass leak — both real, both worth fixing — and not, on current evidence, a crash. The editor-killer is mono-side during shutdown and is tracked in #264.
Severity was deliberately not raised, because the reasoning for "fatal on window close" ran through that wedge and the wedge's own stack contradicts it. It goes back up if the open question below resolves toward SR.
Split out of #264. This is the one of that batch with a confirmed mechanism — see the status banner above for what that mechanism is and is not now shown to cause.
Found with a partner integrator (Amazon Lab126 / LeiaViewer port) on plugin v2.16.0.
Confirmed, not inferred
An in-process editor probe on the partner's rig, run 25+ minutes after Lifecycle Shutdown with no session running:
[#264wp] ==== wndproc ownership sweep, pid 45204 ====
[#264wp] HIT class='UnityContainerWndClass' proc=0x7FFC3F9437E0
owner=SimulatedRealityDirectX.dll (base 0x7FFC3F920000, end 0x7FFC3F974000)
[#264wp] windows examined=10
SimulatedRealityDirectX.dll still owns GWLP_WNDPROC on a Unity editor container window, long after the provider went silent (zero DisplayXR-PROV lines after Lifecycle Shutdown). SR modules are all still mapped, so this is the survivable form, not a dangling proc.
Cross-process GetWindowLongPtr returns 0 — Windows blocks it — so this had to be an in-process editor script. Worth knowing before anyone tries to reproduce the probe from outside.
The mechanism, end to end
Our side (displayxr_win32.c, child-glue window creation):
CHILD-GLUE (#740): born as a WS_CHILD of Unity's container window instead of a
parentless top-level. GA_ROOT(child) == the container = the window the SR SDK
resolves as its phase anchor ... this window is OURS: the container is stable
across tab re-hosts, so the child survives them; we recreate it each session.
So on the docked texture path — the editor default since v2.8.0:
The SDK anchors to GA_ROOT, which is the container, not our child.
LifecycleStop destroys our child. The container is Unity's; we neither own nor destroy it.
The subclass therefore survives on a window we were never going to clean up — and we create a fresh child and a fresh weaver against that same container every session.
The property that makes child-glue robust (the container is stable across tab re-hosts) is exactly what lets subclass layers stack.
Teardown contract — three hops, and the failing one has no owner:
xrDestroySession -> destroys the DP runtime VERIFIED unconditional & synchronous
(d3d12_compositor_destroy ->
xrt_display_processor_d3d12_destroy)
DP destroy -> destroys the weaver plug-in contract
weaver destroy -> restores GWLP_WNDPROC SR SDK contract <-- NOT ENFORCED
The runtime's half is satisfied — traced in source, and PR #1012's deferred-destroy graveyard is service-path-only (grep graveyard src/ hits only comp_d3d11_service.cpp), so the in-process D3D12 path this provider uses has no retired-DP window at all and #964's shape cannot reproduce here. The failure is downstream of xrt_display_processor_d3d12_destroy — in the plug-in's DP destroy, or in the SDK's weaver destructor not restoring GWLP_WNDPROC.
Our LifecycleStop comment previously compressed all three hops into "xrDestroySession unhooked the SR weaver subclass". Corrected in-tree.
Observed symptoms
Benign form (reproduced on v2.16.0, non-fatal): after 12 LoadScene cycles with a live session and a normal Play-stop — no scripted resizes, so not a layout artefact:
provider: GfxStop -> Lifecycle Stop -> Lifecycle Shutdown -> 0 further DisplayXR-PROV lines
weaver: "WeaverBaseImpl: no handler for this window" x46 over ~6 min, then STOPPED
pattern: groups of 3, ~3ms / ~3ms / ~100ms within a group, clusters every 5-30 s
editor: alive and healthy throughout
Message-driven rather than frame-driven (an idle editor is not rendering at 60fps), surviving complete provider shutdown, self-limiting when message traffic stops.
Fatal form — REFUTED as an explanation of the observed wedge. The hypothesis was that a new weaver on the same HWND makes the chain self-referencing (SR wndproc -> CallWindowProcA -> SR wndproc -> ...) until the stack is gone. A falsifier was published in advance — "the spinning thread is somewhere else entirely" — and it fired: no SR frames execute anywhere in the wedged process.
The symptom profile was reproduced deliberately (fresh editor → Play → 12 LoadScene cycles → Stop → Play → Stop → CloseMainWindow), and it wedged at 2,174 lines where history showed 5,955 — so the burst count was never a threshold. But the wedge is a managed hang, not a wndproc recursion.
Still open — one structural argument survives, on a different artefact. Dump Unity.exe.23896 shows a repeating 7,344-byte frame pair — and a pair is exactly the unit a self-referencing subclass produces (wndproc -> CallWindowProcA -> wndproc). The mechanism that would produce self-reference rather than a finite chain: every SR layer is the same function at the same address, so weaver 2 subclassing an already-subclassed window reads GWLP_WNDPROC, gets SR's own proc, and stores it as the proc to forward to — arming unbounded recursion at two layers. Three reads settle it, all from that one dump:
Do the repeating frames land inside SimulatedRealityDirectX.dll?
Are the two return addresses in the pair identical (self-reference) or different (finite chain)?
StackBase - StackLimit — ~1 MB implies ~419 B/frame against the observed 3,672 (8.7x off, a different recursion); ~8 MB gives ~2,280 frames (consistent with the recorded ~2500).
Tests, with predictions published in advance
A — docked/child-glue, Play → Stop → Play, then re-probe. Expect the SR wndproc still on UnityContainerWndClass, and burst rate/count escalating per cycle rather than resetting. A reset to baseline, or the SR hit disappearing, kills it.
B — the strong falsifier, DISPLAYXR_PROV_EXTERNAL_WINDOW=1. Undocked PRESENT mode makes our window a visible top-level one, where GA_ROOT == self and we do destroy it at LifecycleStop. Expect no SR wndproc on any Unity window afterwards and no burst. A burst here too means the GA_ROOT/ancestor theory is wrong.
B can destroy the mechanism outright where A can only corroborate, so B is the more valuable run. If B is clean it is also an immediate workaround for affected integrators.
Fix options
Restore GWLP_WNDPROC on weaver destroy (SR SDK / leia-plugin) — the correct fix, upstream.
Anchor the weaver to a window we own and destroy on the docked path, so the subclass cannot outlive the session — removes the accumulation by construction rather than by relying on an unenforced contract.
Refuse to create a weaver against a window that already carries an SR subclass — the neighbouring code already knows this shape: d3d12_split_retire carries the comment "One weaver per HWND — a create against a window that still has one is refused, and the failure is silent weaving."
Related
Editor crash: main-thread stack overflow, repeating 7,344-byte frame pair (dump Unity.exe.23896) #264 — main-thread stack overflow. Whether that dump is this bug is still open: the fatal form predicts ~2500 native SimulatedRealityDirectX frames, but the dump shows a 7,344-byte repeating frame pair (~3,672 B/frame). Those reconcile only at particular stack sizes — a 1 MB stack implies ~419 B/frame (8.7x off, so a different recursion), an 8 MB stack gives ~1,140 pairs ≈ 2,280 frames (consistent). The thread's stack reserve (StackBase - StackLimit) is a required input, not a detail, and must be read alongside the symbol on the repeating frame.
displayxr-leia-plugin#217 — the neighbouring unbounded-burst string.
While searching the wedge capture for SR frames, the investigator found five SimulatedReality hits — and checked before reporting. They were the lm module list, not stack frames.
Recording it because that search would have "confirmed" the fatal attribution to anyone who wanted it confirmed, and it was caught by the person whose own hypothesis it would have supported. Anyone grepping a stack capture for a module name should expect the module list to match first.
Split out of #264. This is the one of that batch with a confirmed mechanism — see the status banner above for what that mechanism is and is not now shown to cause.
Found with a partner integrator (Amazon Lab126 / LeiaViewer port) on plugin v2.16.0.
Confirmed, not inferred
An in-process editor probe on the partner's rig, run 25+ minutes after
Lifecycle Shutdownwith no session running:SimulatedRealityDirectX.dllstill ownsGWLP_WNDPROCon a Unity editor container window, long after the provider went silent (zeroDisplayXR-PROVlines afterLifecycle Shutdown). SR modules are all still mapped, so this is the survivable form, not a dangling proc.The mechanism, end to end
Our side (
displayxr_win32.c, child-glue window creation):So on the docked texture path — the editor default since v2.8.0:
WS_CHILDof Unity's container.GA_ROOT, which is the container, not our child.LifecycleStopdestroys our child. The container is Unity's; we neither own nor destroy it.The property that makes child-glue robust (the container is stable across tab re-hosts) is exactly what lets subclass layers stack.
Teardown contract — three hops, and the failing one has no owner:
The runtime's half is satisfied — traced in source, and PR #1012's deferred-destroy graveyard is service-path-only (
grep graveyard src/hits onlycomp_d3d11_service.cpp), so the in-process D3D12 path this provider uses has no retired-DP window at all and #964's shape cannot reproduce here. The failure is downstream ofxrt_display_processor_d3d12_destroy— in the plug-in's DP destroy, or in the SDK's weaver destructor not restoringGWLP_WNDPROC.Our
LifecycleStopcomment previously compressed all three hops into "xrDestroySession unhooked the SR weaver subclass". Corrected in-tree.Observed symptoms
Benign form (reproduced on v2.16.0, non-fatal): after 12
LoadScenecycles with a live session and a normal Play-stop — no scripted resizes, so not a layout artefact:Message-driven rather than frame-driven (an idle editor is not rendering at 60fps), surviving complete provider shutdown, self-limiting when message traffic stops.
Fatal form — REFUTED as an explanation of the observed wedge. The hypothesis was that a new weaver on the same HWND makes the chain self-referencing (
SR wndproc -> CallWindowProcA -> SR wndproc -> ...) until the stack is gone. A falsifier was published in advance — "the spinning thread is somewhere else entirely" — and it fired: no SR frames execute anywhere in the wedged process.The symptom profile was reproduced deliberately (fresh editor → Play → 12
LoadScenecycles → Stop → Play → Stop →CloseMainWindow), and it wedged at 2,174 lines where history showed 5,955 — so the burst count was never a threshold. But the wedge is a managed hang, not a wndproc recursion.Still open — one structural argument survives, on a different artefact. Dump
Unity.exe.23896shows a repeating 7,344-byte frame pair — and a pair is exactly the unit a self-referencing subclass produces (wndproc -> CallWindowProcA -> wndproc). The mechanism that would produce self-reference rather than a finite chain: every SR layer is the same function at the same address, so weaver 2 subclassing an already-subclassed window readsGWLP_WNDPROC, gets SR's own proc, and stores it as the proc to forward to — arming unbounded recursion at two layers. Three reads settle it, all from that one dump:SimulatedRealityDirectX.dll?StackBase - StackLimit— ~1 MB implies ~419 B/frame against the observed 3,672 (8.7x off, a different recursion); ~8 MB gives ~2,280 frames (consistent with the recorded ~2500).Tests, with predictions published in advance
A — docked/child-glue, Play → Stop → Play, then re-probe. Expect the SR wndproc still on
UnityContainerWndClass, and burst rate/count escalating per cycle rather than resetting. A reset to baseline, or the SR hit disappearing, kills it.B — the strong falsifier,
DISPLAYXR_PROV_EXTERNAL_WINDOW=1. Undocked PRESENT mode makes our window a visible top-level one, whereGA_ROOT == selfand we do destroy it atLifecycleStop. Expect no SR wndproc on any Unity window afterwards and no burst. A burst here too means theGA_ROOT/ancestor theory is wrong.B can destroy the mechanism outright where A can only corroborate, so B is the more valuable run. If B is clean it is also an immediate workaround for affected integrators.
Fix options
GWLP_WNDPROCon weaver destroy (SR SDK / leia-plugin) — the correct fix, upstream.d3d12_split_retirecarries the comment "One weaver per HWND — a create against a window that still has one is refused, and the failure is silent weaving."Related
SimulatedRealityDirectXframes, but the dump shows a 7,344-byte repeating frame pair (~3,672 B/frame). Those reconcile only at particular stack sizes — a 1 MB stack implies ~419 B/frame (8.7x off, so a different recursion), an 8 MB stack gives ~1,140 pairs ≈ 2,280 frames (consistent). The thread's stack reserve (StackBase - StackLimit) is a required input, not a detail, and must be read alongside the symbol on the repeating frame.displayxr-leia-plugin#217— the neighbouring unbounded-burst string.A near-miss worth keeping, verbatim
While searching the wedge capture for SR frames, the investigator found five
SimulatedRealityhits — and checked before reporting. They were thelmmodule list, not stack frames.Recording it because that search would have "confirmed" the fatal attribution to anyone who wanted it confirmed, and it was caught by the person whose own hypothesis it would have supported. Anyone grepping a stack capture for a module name should expect the module list to match first.