Skip to content

Memory not fully released after high-frequency CanvasBitmap updates and subsequent cleanup on Win2D #996

@WWW4R4E

Description

@WWW4R4E

Library Versions:

CommunityToolkit.Mvvm Version="8.4.0"
Microsoft.Graphics.Win2D Version="1.3.2"
Microsoft.WindowsAppSDK Version="1.7.250606001"

Description

I have a WinUI 3 page that features a real-time preview of a window. This is achieved by capturing the window content at
~30 FPS, creating a CanvasBitmap from each capture, and drawing it onto a CanvasControl.

When the real-time preview is stopped, I perform a thorough cleanup of all resources. However, a significant amount of
memory is never fully released back to the system, and remains allocated for the application's lifetime.

Steps to Reproduce

  1. A CanvasControl is defined in XAML, with its Draw event subscribed (Draw="DesktopCanvas_Draw").
  2. A DispatcherTimer ticks every 30ms, calling a method to refresh the screen capture.
  3. The RefreshCaptureAsync method creates a new CanvasBitmap and disposes the previous one.
    DesktopCanvas.Invalidate() is called to trigger a redraw.
  4. The DesktopCanvas_Draw method draws the latest CanvasBitmap with scaling.
  5. After running the preview for a few seconds, a button is clicked to stop it.
  6. The cleanup logic in CaptureDesktopButton_Click stops the timer, disposes the final bitmap, and attempts a thorough
    cleanup.

Code Snippets

Here is the relevant code from my implementation:

DeskTopCapturePage.xaml:

<Grid>
    <!-- Other elements -->
    <canvas:CanvasControl
        x:Name="DesktopCanvas"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        Draw="DesktopCanvas_Draw" />
    <!-- Other elements -->
    <StackPanel Grid.Row="2">
        <Button
            x:Name="CaptureDesktopButton"
            HorizontalAlignment="Center"
            Click="CaptureDesktopButton_Click"
            Content="开始预览" />
        <!-- Other elements -->
    </StackPanel>
</Grid>

DeskTopCapturePage.xaml.cs:

public sealed partial class DeskTopCapturePage : Page
{
    private DeskTopCaptureViewModel viewModel = new();

    public DeskTopCapturePage()
    {
        this.InitializeComponent();
        viewModel.timer.Interval = TimeSpan.FromMilliseconds(30);
        viewModel.timer.Tick += async (s, e) => await RefreshCaptureAsync();
    }

    private async Task RefreshCaptureAsync()
    {
        using (var softwareBitmap = GetDesktop.CaptureWindow()) // This method provides a capture
        {
            if (softwareBitmap != null)
            {
                viewModel.latestBitmap?.Dispose();
                viewModel.latestBitmap = CanvasBitmap.CreateFromSoftwareBitmap(CanvasDevice.GetSharedDevice(), softwareBitmap);
                DesktopCanvas.Invalidate();
            }
        }
    }

    private void DesktopCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
    {
        if (viewModel.latestBitmap != null)
        {
            // Drawing logic with scaling...
            args.DrawingSession.DrawImage(viewModel.latestBitmap, ...);
        }
    }

    private void CaptureDesktopButton_Click(object sender, RoutedEventArgs e)
    {
        if (viewModel.timer.IsEnabled)
        {
            // --- STOP PREVIEW & CLEANUP ---
            viewModel.timer.Stop();
            CaptureDesktopButton.Content = "开始预览";

            viewModel.latestBitmap?.Dispose();
            viewModel.latestBitmap = null;
            DesktopCanvas.Invalidate();

            // Attempting thorough cleanup
            CanvasDevice.GetSharedDevice().Trim();
            if(DesktopCanvas != null)
            {
                // This cleanup step was tested, but did not solve the final memory retention
                // DesktopCanvas.RemoveFromVisualTree();
                // ((Grid)DesktopCanvas.Parent).Children.Remove(DesktopCanvas);
                // DesktopCanvas = null;
            }
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        else
        {
            // --- START PREVIEW ---
            viewModel.timer.Start();
            CaptureDesktopButton.Content = "停止预览";
        }
    }
}

📊 Key Observations

State Memory Usage
Initial state ~70 MB
During preview ~130–170 MB
After cleanup Still ~130 MB ⚠️

❗ Even after full cleanup, memory does not return to the initial level. This suggests that some internal resources are retained by CanvasDevice or CanvasControl and not being fully released.


🤔 Expected Behavior
After stopping the preview and performing cleanup, the app's memory usage should return close to the initial level (~70–90 MB).

😟 Actual Behavior
Despite calling Dispose(), Trim(), and even removing the CanvasControl from the visual tree, memory usage stabilizes around ~130 MB and does not drop further. Subsequent preview start/stop cycles do not increase memory usage, indicating that no new leaks occur — but also that the initial allocation is never freed.

🧪 Additional Notes
We noticed an odd behavior:

If I subscribe to the Draw event twice, memory usage goes up to ~100–170 MB during preview, and then drops to ~100 MB after cleanup.But after starting the computer the next day, it failed again, but at least this told me that there should be memory space that can be optimized.

🧰 What I’m Asking For
I would like guidance or solutions on how to:

✅ Fully release all Win2D-related resources (especially CanvasDevice, CanvasBitmap)
✅ Ensure that memory returns to the pre-preview baseline
✅ Avoid any hidden references or caches that prevent memory from being reclaimed
Any help with debugging tools, profiling steps, or best practices for resource management in WinUI 3 + Win2D would be greatly appreciated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions