dotnet / wpf

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

Regarding WPF BitmapCache Issue + R&D + Solutions #8919

Open CycloneRing opened 6 months ago

CycloneRing commented 6 months ago

Hi,

I've been tackling the well-known BitmapCache issue involving multiple windows in WPF. I've delved deeply into the problem, examining WPF internals down to the native core. I'm actively working on and addressing the issue, You can find the code at this repository.

What is happening, How it's happening

To reproduce this bug, you need to create at least two windows. Let's designate the first window as the "Prime Window" and subsequent windows as "Alternative Windows". Adding a BitmapCache to any Alternative Window created after the Prime Window leads to a visual freeze on those Alternative Windows after pressing Alt + Ctrl + Del, locking the screen, or prompting UAC. Let's refer to this event as the "Display Reset".

Following a Display Reset, hovering over a reactive control (such as a Button or Checkbox) on the Prime Window triggers a WM_PAINT message. However, on the Alternative Windows, the WM_PAINT fails, causing the operating system's graphic manager to continually send (spam) WM_PAINT messages to the Alternative Windows. This incessant messaging is the root cause of the lock and lag experienced on Alternative Windows. It's suspected that the presence of a BitmapCache element causes the painting to fail and return false.

Previous Solutions Provided by Community :

  1. Terminating DWM.exe resolves the lock issue, It's a very ugly and dirty way to fix it, But it works.

  2. Switching to software rendering and disabling Hardware Acceleration has been suggested. However, this is deemed undesirable due to the necessity of GPU usage in 2024.

Current Findings and Proposed Solutions :

  1. Applying a BitmapCache to an element in the Prime Window prevents lock on Alternative Windows after a Display Reset. [Branch]

  2. Hovering over a single reactive control (such as a Button or Checkbox) on the Prime Window with the mouse rectifies the lock on Alternative Windows, without necessitating any BitmapCache on the Prime Window. [Branch]

  3. Closing the Prime Window promotes the second window created after it to the status of Prime Window, resolving any lock situation it may be in.

  4. To detect Display Reset, utilize a D3DImage and listen for the IsFrontBufferAvailableChanged event. Upon Display Reset, switch ProcessRenderMode to software and back to hardware, It will effectively resolve the problem. However, this approach may introduce visual glitches that I personally do not like. [Details]

MediaControl Solution (Cross-Version) :

MediaControl is an internal component of Milcore (WPF Backend Render Engine) which controls rendering options in a running WPF application. Although primarily designed for debugging and profiling, it offers a potential solution to this issue.

My current solution involves hooking MediaControl within your WPF application, manipulating rendering options, and setting the DisableDirtyRegionSupport option to true. By disabling dirty region support, the bitmap cache problem disappears. However, this solution may lead to increased resource usage and impact performance since the entire WPF window will be rendered instead of only the changed areas. [Implementation]

In this solution, we connect to a shared memory path created per process at wpfgfx_v0400-<PID>, then modify the structure to change the DisableDirtyRegionSupport flag. I'm actively working on a fully managed approach to eliminate the dependency on milctrl_v0300_x64.dll.

Note: To make this solution work you need to change HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Avalon.Graphics\EnableDebugControl to 1, If it doesn't be set wpfgfx doesn't create MediaControl. You can also simply hook RegQueryValueEx and return 1 when wpfgfx ask for the registry key.

Real-Time Patcher Solution (Specific-Version) :

In this solution, Instead of using MediaControl to disable Dirty Region Support we directly patch it in wpfgfx. It's stored as g_fDirtyRegion_Enabled inside wpfgfx dll file, To find the offset for installed version you need to download pdb of wpfgfx and use OffsetExtractor to find the static offset of the value.

By using this method we can directly disable Dirty Region Support, Unlock, Restore it back to enabled. [Implementation]

Note: I will update this section with any further discoveries or refinements in solution.

CycloneRing commented 6 months ago

Update 2 : Real-Time Patcher for .NET Framework 4.X

This is a Signature based Patcher I implemented for .Net Framework 4.5-4.8.1, It's working great. Tested on 10 different virtual machines on both Windows 10 & 11.

// Patchers
namespace DirtyRegionEnabledPatcher
{
    // Signature Data
    const char* DirtyRegionEnabledSignature = "45 85 F6 0F 85 ?? ?? ?? ?? 44 39 35 ?? ?? ?? ?? 0F 84";
    const int DirtyRegionEnabledPointerOffset = 12;
    const int DirtyRegionEnabledPointerInstructionSize = 3; // 44 39 35
    const int DirtyRegionEnabledPointerInstructionAndOffsetSize = 7; // 44 39 35 [?? ?? ?? ??]

    // Scanner Cache
    static bool isTargetSignatureFound = false;
    static uintptr_t targetSignatureAddress = 0;
    static uintptr_t targetValueAddress = 0;

    // Patcher
    static bool PatchValue(int targetValue)
    {
        // Scan for Signature If it's Not Cached
        if (!isTargetSignatureFound || !targetSignatureAddress || !targetValueAddress)
        {
            targetSignatureAddress = MemoryScanner::ScanForPattern(uintptr_t(GetModuleHandle(WPFGFX_MODULE_NAME)), DirtyRegionEnabledSignature);
            if (targetSignatureAddress)
            {
                isTargetSignatureFound = true;
                debuglog("DirtyRegionEnabled Signature Found At 0x%p", (void*)targetSignatureAddress);

                // Get Pointer Value
                uintptr_t pointerOffset = targetSignatureAddress + DirtyRegionEnabledPointerOffset;
                int pointerValue = 0;
                memcpy(&pointerValue, (void*)pointerOffset, sizeof(int));
                debuglog("DirtyRegionEnabled Pointer Value : 0x%X (%d)", pointerValue, pointerValue);

                // Calculate Target Value Address
                targetValueAddress = targetSignatureAddress + (DirtyRegionEnabledPointerOffset - DirtyRegionEnabledPointerInstructionSize) 
                    + DirtyRegionEnabledPointerInstructionAndOffsetSize + pointerValue;
                debuglog("DirtyRegionEnabled Memory Address : 0x%p", (void*)targetValueAddress);
            }
        }

        // Validate & Perform The Patch
        if (targetValueAddress)
        {
            // Patch Value
            memcpy((void*)targetValueAddress, &targetValue, sizeof targetValue);

            // Success
            return true;
        }

        // Failed
        return false;
    }
}

It can be implemented directly in managed code as well, Check out this repo.

Here's the versions checked for the signature and flag :

image

Valkirie commented 3 months ago

Hey @CycloneRing we're suffering from this issue at https://github.com/Valkirie/HandheldCompanion where we display a 2nd window usage as quick tools menu. Can you help me understand how to deploy your fix ? Do we need to wait for the issue to occur to look for that wpfgfx_v0400 module and patch it ?