WinUI3에서 Win2D의 CanvasSwapChainPanel 성능 확인 - slog

앞전의 확인에 의해 WinUI3에서 Win2D의 성능이 UWP보다 상당히 떨어진다는 것을 확인했습니다.

만약 자신의 모니터 주사율이 120hz 이라면 충분한 그리기 처리량의 경우 120fps 근처로 나와줘야 하지만, WinUI3에서는 30~55fps 정도로 모니터 주사율에 도달하지 못했습니다.

오늘은 Win2D에서 지원하는 CanvasSwapChainPanel을 이용해 이 문제점을 해결 할 예정입니다.

좋아요 1

CanvasSwapChainPanel 를 이용하면 필요한 시점에서 그리기 및 화면 표시가 가능하다고 합니다.

간단히 상자를 그리도록 하고, 초당 몇회(FPS) 그리기 성능이 나오는지를 확인할 예정입니다.

좋아요 1

먼저 WinUI3 환경 및 Win2D 적용 버전에 대해 살펴봐야 합니다.

WinUI3는 Windows App SDK의 구성품으로 안정화 버젼 0.83 및 프리뷰 버젼 1.0 Preview 1이 있습니다.
Win2D는 WinUI3 용으로 변환 작업이 진행되는 것으로 보이며 가장 최신의 버젼은 1.0-experimental1입니다. 실험버전은 프리뷰 버전 보다 불완전한 버전으로 완전한 기능을 제공하지 않습니다.

WinUI3의 경우 Windows App SDK 프리뷰 버전과 Win2D를 현재 같이 쓸수 없습니다. 정확한 것은 모르겠지만, 프리뷰가 되면서 XAML 처리 방식이 변한것 같으며 그 부분에 충돌하는 것 같습니다.
버전 0.83을 사용해야 합니다.

.NET 6을 사용하기 위해 Visual Studio 2022 Preview 환경에서 환경 세팅을 합니다.

도구-확장 관리에서 Project Reunion을 설치합니다.
이후 새 프로젝트 만들기에서 Blank App, Packaged (WinUI 3 in Desktop)을 선택해서 프로젝트를 시작할 수 있습니다.

image

컴파일 해서 실행하면 다음의 화면처럼 WinUI3 앱이 실행됨을 확인할 수 있습니다.

NuGet에서 Microsoft.Graphics.Win2D를 찾아 설치합니다.

패키지 설치 후 정상적으로 동작하는지를 확인하기 위해 MainWindow.xaml을 다음 처럼 변경합니다.

<Window
    x:Class="App26.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App26"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xaml="using:Microsoft.Graphics.Canvas.UI.Xaml"
    mc:Ignorable="d">

    <xaml:CanvasControl  ClearColor="Blue" />
</Window>

실행화면:

좋아요 1

이제 CanvasControl 대시 CanvasSwapChainPanel를 적용해 봅시다.

<Window
    x:Class="App26.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App26"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xaml="using:Microsoft.Graphics.Canvas.UI.Xaml"
    mc:Ignorable="d">

    <xaml:CanvasSwapChainPanel />
</Window>

현재는 아무것도 표시되지 않습니다.

좋아요 1

CanvasSwapChainPanel은 내부적으로 SwapChain을 이용해 그리고 화면에 표시합니다.
먼저 그려야 할 영역의 사이즈가 결정되거나 변경될 때 이를 감지해 SwapChain을 설정하는 코드를 작성해야 합니다. 다음처럼 XAML을 변경합니다.

...
    <xaml:CanvasSwapChainPanel x:Name="canvas"
                               SizeChanged="canvas_SizeChanged"
                               Loaded="canvas_Loaded"
                               Unloaded="canvas_Unloaded"/>
...

다음으로 xaml.cs을 다음처럼 코딩합니다.

       private void Update(Size size)
        {
            // 사이즈가 없으면 SwapChain 정리
            if (size.Width <= 0 || size.Height <= 0)
            {
                canvas.SwapChain?.Dispose();
                canvas.SwapChain = null;
                return;
            }
            // SwapChain이 없으면 생성
            else if (canvas.SwapChain is null)
            {
                var device = CanvasDevice.GetSharedDevice();
                var swapChain = new CanvasSwapChain(device, (float)size.Width, (float)size.Height, 96);
                canvas.SwapChain = swapChain;
            }
            // 사이즈가 변했을 경우 버퍼 재조정
            else if (canvas.SwapChain.Size != size)
            {
                canvas.SwapChain.ResizeBuffers(size);
            }

            // 그리기
            using (var ds = canvas.SwapChain.CreateDrawingSession(Colors.Blue))
            {

            }

            canvas.SwapChain.Present(1);
        }


        private void canvas_Loaded(object sender, RoutedEventArgs e)
        {
        }

        private void canvas_Unloaded(object sender, RoutedEventArgs e)
        {
            canvas.RemoveFromVisualTree();
            canvas = null;
        }

        private void canvas_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Update(canvas.ActualSize.ToSize());
        }

이제 상자를 그려봅시다.

...
            // 그리기
            using (var ds = canvas.SwapChain.CreateDrawingSession(Colors.Black))
            {
                ds.DrawRectangle(100, 100, 100, 100, Colors.White);
            }
...

좋아요 1

슬로그를 진행하는 목적은 CanvasSwapChainPanel를 이용했을 때 PC의 최대 속도를 내주는지 확인하기 위함입니다. 성능을 측정해 봅시다. 별도의 스레드를 통해 Update() 메소드를 지속적으로 호출하고 1초에 몇번(FPS) 호출되었는지 확인하려고 합니다.

MainWindow.xaml.cs

    public sealed partial class MainWindow : Window
    {
        private Random rand = new();

        private Task drawTask;
        private object updateLock = new object();
        private CancellationTokenSource drawTaskCts;
        private Size canvasSize;
        
        private int drawCount;
        private DateTime beforeTime;

        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void Update(Size size)
        {
            lock (updateLock)
            {
                // 사이즈가 없으면 SwapChain 정리
                if (size.Width <= 0 || size.Height <= 0)
                {
                    canvas.SwapChain?.Dispose();
                    canvas.SwapChain = null;
                    return;
                }
                // SwapChain이 없으면 생성
                else if (canvas.SwapChain is null)
                {
                    var device = CanvasDevice.GetSharedDevice();
                    var swapChain = new CanvasSwapChain(device, (float)size.Width, (float)size.Height, 96);
                    canvas.SwapChain = swapChain;
                }
                // 사이즈가 변했을 경우 버퍼 재조정
                else if (canvas.SwapChain.Size != size)
                {
                    canvas.SwapChain.ResizeBuffers(size);
                }

                // 그리기
                using (var ds = canvas.SwapChain.CreateDrawingSession(Colors.Black))
                {
                    var x = rand.Next(0, (int)size.Width);
                    var y = rand.Next(0, (int)size.Height);
                    var width = rand.Next(0, (int)size.Width);
                    var height = rand.Next(0, (int)size.Height);

                    ds.DrawRectangle(x, y, width, height, Colors.White);
                }

                canvas.SwapChain.Present();

                drawCount++;
                var span = DateTime.Now - beforeTime;
                if (span >= TimeSpan.FromSeconds(1))
                {
                    var fps = drawCount / span.TotalSeconds;
                    fpsTextBlock.DispatcherQueue.TryEnqueue(() =>
                    {
                        fpsTextBlock.Text = $"{fps:0.000} fps";
                    });

                    beforeTime = DateTime.Now;
                    drawCount = 0;
                }
            }
        }

        private void canvas_Loaded(object sender, RoutedEventArgs e)
        {
            beforeTime = DateTime.Now;

            drawTaskCts = new CancellationTokenSource();
            drawTask = Task.Run(() =>
            {
                while (true)
                {
                    if (drawTaskCts.Token.IsCancellationRequested == true)
                        return;

                    Update(canvasSize);
                }
            }, drawTaskCts.Token);
        }

        private void canvas_Unloaded(object sender, RoutedEventArgs e)
        {
            drawTaskCts.Cancel();

            canvas.RemoveFromVisualTree();
            canvas = null;
        }

        private void canvas_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            canvasSize = canvas.ActualSize.ToSize();
            Update(canvasSize);
        }
    }

별도의 스레드에서 계속 Update() 메소드를 호출해주고 FPS를 계산해서 출력했습니다.

그런데 생각보다 FPS가 높게 안나오네요. 이유는 SwapChain의 Present() 메소드의 기본 동작이 모니터 주사율에 맞추기 때문입니다. 이를 무시하려면 Present(0)을 주면 되는데요, 그러자 꽤 만족스러운 결과가 나옵니다.

이번 슬로그를 통해 알아본 것은 아직 WinUI3에서 Win2D의 CanvasControl의 성능이 UWP 처럼 나오지는 않는다는것. 그것을 CanvasSwapChainPanel을 이용하면 해소할 수 있다는 것을 살펴봤습니다.

제 컴퓨터에서는 박스 하나를 그리는데 디버그 시 3500 FPS의 성능이 나왔고 릴리즈 시 3600 FPS를 확인할 수 있었습니다. 이정도면 안정적이기만 한다면 쓸모가 있습니다.

좋아요 1

1000개의 상자를 그리니 177 FPS 정도의 나옵니다.

좋아요 1

안티알리아스를 비활성화 하니 620 FPS 정도가 나옵니다.

좋아요 1