dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.03k stars 1.16k forks source link

Application not scaled with .NET Core 3.1.19 #5375

Open davidegiacometti opened 2 years ago

davidegiacometti commented 2 years ago

The issue is affecting only PowerToys Run and Image Resizer, but I can't believe this is affecting only PowerToys.

Actual behavior:

I can also repro this on an XPS13 1920x1080 @150%

3 1 19

Expected behavior:

After downgrade to .NET Core 3.1.18

3 1 18

Minimal repro: Issue can be reproduced with PowerToys Run and Image Resizer.

ThomasGoulet73 commented 2 years ago

I just tried decompiling PresentationCore.dll from 3.1.19 and the module initializer responsible for loading DirectWriteForwarder, which is responsible for setting DPI awareness, is not there. This is a bug with the ILAsm/ILDasm module initializer injection and is probably very high priority.

ryalanms commented 2 years ago

Thanks, @davidegiacometti and @ThomasGoulet73.

https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-3.1.119-windows-x64-binaries

I don't see the missing module initializer when building release/3.1 locally or when examining the PresentationCore.dll in 3.1.119: dotnet-sdk-3.1.119-win-x64.zip\shared\Microsoft.WindowsDesktop.App\3.1.19. Does the timestamp match the assembly in the SDK (8/17/2021)?

ThomasGoulet73 commented 2 years ago

@ryalanms ModuleInitializer is there but \<Module> (The class that invokes ModuleInitializer.Initialize and is injected by ILDasm/ILAsm) is not there:

Here's the difference using dotPeek: 3.1.18: image

3.1.19: image

EDIT: I decompiled the binaries from https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-3.1.118-windows-x64-binaries and https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-3.1.119-windows-x64-binaries

ThomasGoulet73 commented 2 years ago

I did some testing and I was also able to reproduce the issue in a local build in BOTH 3.1.18 and 3.1.19 so the change is not related to the changes itself but to the version of ILAsm.exe and ILDasm.exe. On my machine, ILAsm fails but it never breaks the build even if ILAsm.exe exited with a code different from 0 (See here).

The only way I was able to inject the module initializer was to hard-code the version of ILDasm to 4.6.2 (I didn't try other versions but it failed with 4.8, the one by default on my machine when using Latest).

My guess is that ILDasm was updated on the build machine, ILAsm failed but didn't break the build and it used the dll without the module initializer.

ryalanms commented 2 years ago

@ThomasGoulet73: Thank you for the analysis and fix.

ThomasGoulet73 commented 2 years ago

@ryalanms Happy to help!

But it still fails on my machine using your fix. I don't know if it's just on my machine but TargetDotNetFrameworkVersion.VersionLatest still returns the ILDasm from .Net 4.8. Would the right fix be to explicitly pin it to .Net 4.6.2 with TargetDotNetFrameworkVersion.Version462 ?

Again, this might just be my machine.

ryalanms commented 2 years ago

Would the right fix be to explicitly pin it to .Net 4.6.2 with TargetDotNetFrameworkVersion.Version462 ?

Yes, good point. I'll update the PR.

ryalanms commented 2 years ago

With the newer version of ILDasm/ILAsm, disassembly succeeds but the re-assembly fails:

src\Microsoft.DotNet.Wpf\src\PresentationCore\MS\Internal\Ink\StrokeNode.cs(843) : 
error : syntax error at token '-' in:     IL_02a5:  ldc.r8     -nan(ind)
ChristophHornung commented 2 years ago

Is there any known workaround for the scaling issue? I have a few WPF applications that are unusable for more and more users due to being too small.

ThomasGoulet73 commented 2 years ago

@ChristophHornung You could try to PInvoke SetProcessDPIAware before showing the first window.

ChristophHornung commented 2 years ago

@ThomasGoulet73 Thank you very much good sir! Adding the PInvoke call in OnStartup worked. Like this:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        SetProcessDPIAware();
        base.OnStartup(e);
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool SetProcessDPIAware();
}

Edit:

For an immediate fix in the field it also works to start the application with the older Microsoft.WindowsDesktop.App version via the command line. E.g.: dotnet --fx-version "3.1.18" MyMainApplication.dll. (Make sure the user installs the old .NET Core 3.1.18 Desktop Runtime)

davidegiacometti commented 2 years ago

@ChristophHornung another workaround is to downgrade .NET Core to 3.1.18. Keep in mind that now updates may be released under Windows Updates.

EDIT: missed previous comment 😄

Any news on a fix? Will be shipped with the October update?

jaimecbernardo commented 2 years ago

@ThomasGoulet73 @ChristophHornung , The workaround you proposed has worked for PowerToys as well. Thank you.

ChristophHornung commented 2 years ago

Is there any risk adding the SetProcessDPIAware native call? Or will it have side effects for other configurations or in the future when the bug in WPF is fixed? Or should the call be safeguarded by checking the Environment.Version first to see if the current runtime is 3.1.19?

jaimecbernardo commented 2 years ago

I'm not sure. I've tested with a previous working .net core installation without the issue and the behavior looked good. It's also not certain when this is to be fixed in .net core. The long term goal is to eventually use WinUI so the workaround should work well for us.

ThomasGoulet73 commented 2 years ago

The bug in 3.1.19 is that this code is not invoked early enough. The workaround I provided is simply to pinvoke the native method earlier and to my knowledge, you can call SetProcessDPIAware multiple times in the same process. So to answer @ChristophHornung, in my opinion, there are little to no risk of calling SetProcessDPIAware manually, even in another version of .Net Core that does not have the bug.

I'm glad it also worked for PowerToys.