Skip to content

Keep the confetti canvas usable after it is unloaded and reloaded - #3467

Open
FrayxRulez wants to merge 1 commit into
developfrom
confetti-canvas-reload
Open

FrayxRulez wants to merge 1 commit into
developfrom
confetti-canvas-reload

Conversation

@FrayxRulez

@FrayxRulez FrayxRulez commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Reported by crash telemetry on 12.10.7.

Exception

NativeException: ACCESS_VIOLATION Reading address 0x0000000000000088, on the UI thread:

Microsoft.Graphics.Canvas.dll!CanvasAnimatedControl::ChangedImpl+0x229   CanvasAnimatedControl.cpp:855
Microsoft.Graphics.Canvas.dll!ExceptionBoundary<...>+0x75                ErrorHandling.h:221
Microsoft.Graphics.Canvas.dll!DelegateArgTraits<IDispatchedHandler>::DelegateInvokeHelper<...>::Invoke+0x17
Windows.UI.dll!CDispatcher::ProcessInvokeItem
Windows.UI.dll!CDispatcher::ProcessMessage
...
Windows.UI.Xaml.dll!CJupiterWindow::RunCoreWindowMessageLoop

Cause

Line 855 of Win2D 1.28.3's CanvasAnimatedControl.cpp is m_gameLoop->StopDispatcher();. Disassembled from the shipped Microsoft.Graphics.Canvas.dll (PDB matched): mov rax,[rbx+348h] loads m_gameLoop, then mov rcx,[rax+88h] reads CanvasGameLoop::m_gameLoopThread (the member after the 0x50-byte recursive_mutex and six pointers/bools). So m_gameLoop is null while IsLoaded() - checked a few instructions earlier - is true.

ConfettiView.OnUnloaded calls Canvas.RemoveFromVisualTree(). The canvas is the root of ConfettiView's template, and Win2D's RemoveFromVisualTreeImpl only knows how to detach an element from a Panel, UserControl or ContentControl parent, so the control stays in the tree. What does happen is that CanvasAnimatedControl::RemoveFromVisualTree resets m_canvasSwapChainPanel. When the canvas is later loaded again, CanvasAnimatedControl::Loaded() sees no swap chain panel, takes its designer path (the assert is compiled out) and returns without creating the game loop, but BaseControl::OnLoaded still sets m_isLoaded = true. The next change that needs a draw - a resize, a DPI change - runs ChangedImpl, which checks IsLoaded() and dereferences the null game loop.

MainPage realizes Confetti on the first confetti and never unloads it (the Completed hookup is commented out), so every later unload/reload of the page arms this: the passcode lock does exactly that (WindowContext.Lock swaps the window content, Unlock restores it). The log tails end either on Hiding passcode lock or on a window resize or restore, i.e. the first draw request after an earlier reload. The same path also left confetti silently broken after a reload, since Canvas was nulled.

Change

  • Stop calling RemoveFromVisualTree and stop nulling Canvas; Win2D already destroys the game loop on Unloaded and recreates it on Loaded, as long as it still has its swap chain panel.
  • RemoveFromVisualTree was there to break the cycle Win2D creates by holding Draw handlers strongly. Draw is now subscribed only while the canvas is loaded, with a load count mirroring Win2D's own m_loadedCount, since Loaded for a new parent can arrive before Unloaded for the old one. Loaded/Unloaded stay subscribed, which is what lets the view recover.

Unbuilt: syntax-checked with Roslyn only, not compiled or run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant