microsoft / microsoft-ui-xaml

Windows UI Library: the latest Windows 10 native controls and Fluent styles for your applications
MIT License
6.17k stars 667 forks source link

The image quality deteriorates on SwapChainPanel(with Direct2D) #9794

Closed sincos2854 closed 3 days ago

sincos2854 commented 1 week ago

Describe the bug

When drawing an image to SwapChainPanel with Direct2D when the display's DPI scaling is set to anything other than 100%, the transfer does not occur pixel-by-pixel, resulting in a degradation of image quality.

Steps to reproduce the bug

Source code SwapChainPanelTest.zip

  1. Save the values of CompositionScaleX and CompositionScaleY of SwapChainPanel
  2. Read and decode the image(Assets\test.png)
  3. Create ID2D1DeviceContext
  4. Create IDXGIFactory2
  5. Create SwapChain(IDXGISwapChain1) using CreateSwapChainForComposition
  6. Get ISwapChainPanelNative from SwapChainPanel and set SwapChain
  7. Create BackBufferBitmap(ID2D1Bitmap1, dpiX = 96 x CompositionScaleX and dpiY = 96 x CompositionScaleY) for the back buffer from IDXGISurface
  8. Create SourceBitmap(ID2D1Bitmap1, dpiX = 96 x CompositionScaleX and dpiY = 96 x CompositionScaleY) for the source image
  9. Call CopyFromMemory to copy the source image to SourceBitmap
  10. Call BeginDraw
  11. Call DrawBitmap to render SourceBitmap onto BackBufferBitmap(The rectangles of both Bitmaps are (0, 0, image width / compositionScaleX, image height / compositionScaleY))
  12. Call EndDraw and Present

When the display's DPI scaling is set to anything other than 100%, SwapChainPanel uses device-independent pixels. To match this, Direct2D also sets the DPI to use device-independent pixels. At first glance, DrawBitmap method appears to perform a pixel-by-pixel transfer because the rectangles of SourceBitmap and BackBufferBitmap are the same. However, scaling is actually performed. This can be confirmed by observing that the image quality changes when D2D1_INTERPOLATION_MODE is changed.

Expected behavior

Display the source image without any modifications.

Screenshots

DPI scaling = 100% 100percent

DPI scaling = 150% 150percent

NuGet package version

WinUI 3 - Windows App SDK 1.5.4: 1.5.240607001

Windows version

Windows 11 (22H2): Build 22621

Additional context

I have read this.
SwapChainPanel scales SwapChain content by DPI scale · Issue #8219

github-actions[bot] commented 1 week ago

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one. Thank you!

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

castorix commented 1 week ago

I cannot test your code (cannot compile C+/WinRT on my PC), but in C# your .png image is correct if I draw it with _D2D1_BITMAP_INTERPOLATION_MODELINEAR (I loaded it with WIC, LoadBitmapFromFile function from WinUI3_Direct2D_Effects that I adapted from MS) (or not great if I resize it very small, but same thing at 100%)

DarranRowe commented 1 week ago

Direct2D's high DPI support is surprising in places, but I can guarantee one thing, it doesn't set anything to match SwapChainPanel. The DPI has always been configurable on the ID2D1RenderTarget interface, and ID2D1DeviceContext allows you to set Direct2D's unit mode to pixels and not DIPs.

One other issue is that it is difficult to see what is going on due to you clearing the swap chain surface to the same colour as the Xaml background. Things get interesting if I make one single modification to your code. If I change your: d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::White)); to d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Pink)); and then run this, everything becomes clear.

Screenshot 2024-07-06 175426

Is what I get if I run the application, but what happens if I resize the window?

Screenshot 2024-07-06 175438

The pink area is the area drawn to by Direct2D, so what your code is doing is taking the source image and then shrinking it down thus resulting in this loss of quality. My system is at 125%, so this is effectively taking the 500x500px image and scaling it down to 400x400px. (width / 1.25 and height / 1.25).

There are three things to remember here. First, it is more than likely that SwapChainPanel is placing a scaling transform on the swapchain surface itself, and after all other drawing occurs. Secondly, SwapChainPanel sizes itself to the size of the swapchain automatically, this means that it will be the width and height scaled by the window scaling factor. Third, it seems as if it doesn't care about the window size, if the window's client rectangle is smaller than the scaled swapchains size, then it will just let some of it go off of the end.

sincos2854 commented 1 week ago

@castorix Thank you for your reply. I couldn't build and run your code on my PC, but I read through it. In your code, you set the dpiX and dpiY of the source image (ID2D1Bitmap) to 96, so I did the same in my code. Specifically, I set the dpiX and dpiY of SourceBitmap to 96. However, I couldn't avoid the degradation in image quality.

@DarranRowe Thank you for your reply. I believe I understand what you are saying. However, I am not sure how to resolve this issue.

castorix commented 6 days ago

However, I couldn't avoid the degradation in image quality.

Did you try to draw the image with D2D1_BITMAP_INTERPOLATION_MODE_LINEAR ? The quality of your .png is better on my PC (I drew it as background of a window to test it)

sincos2854 commented 6 days ago

I did not use D2D1_BITMAP_INTERPOLATION_MODE_LINEAR (ID2D1RenderTarget::DrawBitmap), so I re-tested with it and D2D1_INTERPOLATION_MODE_LINEAR (ID2D1DeviceContext::DrawImage). Certainly, the image quality improved. However, the scaling is still happening, and although it's slight, the image quality does degrade.

I don't want any scaling to be performed in the application. Let me explain what I mean.

In my Win32 + Direct2D application, I was using the following:

SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

By executing this, the client area of a window always operates at 96 DPI regardless of the display's DPI scaling. The parts outside the client area operate according to the display's DPI scaling. Therefore, even if the DPI scaling is at 200%, images can be displayed without any scaling. Since no scaling occurs, the image quality does not change regardless of the D2D1_INTERPOLATION_MODE setting. The original image quality is maintained.

I want to do the same thing with WinUI3 + Direct2D.

castorix commented 6 days ago

I cannot see a quality difference, with or without PerMonitorV2 in the Manifest. I get this at 150%, C#/WinUI3 1.42, Windows 10 22H2, no real graphic card (Intel HD) :

image

DarranRowe commented 6 days ago

@castorix The issue is, the size of your image. The OP wants the window to not scale the image at all. With your screenshot being 795x912, this shows that you are getting the upscaling that occurs. Remember, the source image is 500x500px, the 150% scaled version of this is 750x750px. What @sincos2854 wants is that a 500x500px swapchain will be displayed at 500x500px, not 625x625px (125%), 750x750px (150%) or larger.

castorix commented 6 days ago

Yes, I have let the default 1,1 scaling for the SwapChainPanel To keep the image at 500*500, I can do for example, in C# (scpD2D is the SwapChainPanel name) :

uint nDPI = GetDpiForWindow(hWndMain);
double nScaleX = 96.0f / (double)nDPI;
double nScaleY = 96.0f / (double)nDPI;
scpD2D.RenderTransform = new ScaleTransform { ScaleX = nScaleX, ScaleY = nScaleY };

XAML (scpD2D in a grid) :

      <SwapChainPanel x:Name="scpD2D" Grid.Row="1"
                      Margin="10, 10, 10, 10"
                      HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                      RenderTransformOrigin="0.5 0.5">
          <!--<SwapChainPanel.RenderTransform>
              <ScaleTransform ScaleY="1" ScaleX="1" />
          </SwapChainPanel.RenderTransform>-->
      </SwapChainPanel>

and I get :

image

sincos2854 commented 6 days ago

@DarranRowe Yes, what you are saying is correct.

@castorix The comparison below shows the cropped screenshot you provided (on the right) and the image I uploaded (Assets\test.png) (on the left) using WinMerge. winui3_comparison

The yellow dots indicate the differences. The output is 500x500 pixels, but due to internal scaling within the application, differences can be observed.

On the other hand, the comparison below shows the screenshot of a Win32 application with DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 enabled (taken with the display's DPI scaling set to 150%) (on the right). (The sides were excluded for vertical detection in WinMerge.) win32_comparison

Since no scaling is applied, the image quality remains unchanged. I want to achieve this with WinUI3 and SwapChainPanel. However, it is not possible because SwapChainPanel cannot eliminate the impact of the display's DPI scaling.

castorix commented 6 days ago

Since no scaling is applied, the image quality remains unchanged. I want to achieve this with WinUI3 and SwapChainPanel. However, it is not possible because SwapChainPanel cannot eliminate the impact of the display's DPI scaling.

I get (nearly) the same images from the above test with RenderTransform : 100% vs 150% (the 5 differences are probably from manipulations I did with PSP to copy/save .jpg from screen...) :

image

sincos2854 commented 5 days ago

(I attached the wrong file in my previous post, so I deleted it and am reposting it now.)

I understand what you are saying. You are correct.

When I used RenderTransform of SwapChainPanel, I was able to achieve the correct output without any scaling. Below are the new source code and the results with the display's DPI scaling set to 150%.

Source code: SwapChainPanelTest.zip

RenderTransform_150_percent

RenderTransform_comparison

Here is an important point to note: setting a value less than 1.0 for RenderTransform will reduce the displayable area. For example, when the display's DPI scaling is 150%, the displayable area will be the width and height of SwapChainPanel divided by 1.5. If the size of SwapChainPanel is 150x150, the displayable area will be 100x100.

Therefore, it becomes necessary to change the size of SwapChainPanel in the code. To display an image of 500 x 500 pixels, the size of SwapChainPanel needs to be changed to 750 x 750. The Width and Height of SwapChainPanel are of type double, so it is not a problem if there are decimal points.

The code example is as follows:

MainWindow.xaml

    <SwapChainPanel
        x:Name="swapChainPanel"
        Width="1" Height="1"
        HorizontalAlignment="Left" VerticalAlignment="Top"
        Loaded="swapChainPanel_Loaded"/>

MainWindow.xaml.cpp

//
// Get composition scales of SwapChainPanel
//
auto panel = this->swapChainPanel();
auto compositionScaleX = panel.CompositionScaleX();
auto compositionScaleY = panel.CompositionScaleY();

//
// Set RenderTransform of SwapChainPanel
//
auto scaleTransform = Media::ScaleTransform();
scaleTransform.ScaleX(1.0 / compositionScaleX);
scaleTransform.ScaleY(1.0 / compositionScaleY);
panel.RenderTransform(scaleTransform);

//
// Resize the window size
//
AppWindow().ResizeClient(SizeInt32(imageWidth, imageHeight));

//
// Resize SwapChainPanel size
//
panel.Width(static_cast<double>(imageWidth) * compositionScaleX);
panel.Height(static_cast<double>(imageHeight) * compositionScaleY);

Also, in Direct2D, there is no need to consider DPI. It is perfectly fine to always set all DPIs to 96.0.

With this, I was able to achieve what I was looking for with WinUI3 and SwapChainPanel.

I am truly grateful to @castorix and @DarranRowe.

Thank you very much.

codendone commented 3 days ago

Is anyone here still blocked or have the issues been resolved?