Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
CycloneRing opened this issue Mar 16, 2024 · 2 comments
Open

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

CycloneRing opened this issue Mar 16, 2024 · 2 comments
Labels
Investigate Requires further investigation by the WPF team.

Comments

@CycloneRing
Copy link

CycloneRing commented Mar 16, 2024

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.

@lindexi lindexi added the Investigate Requires further investigation by the WPF team. label Mar 17, 2024
@CycloneRing
Copy link
Author

CycloneRing commented Mar 18, 2024

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
Copy link

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 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Investigate Requires further investigation by the WPF team.
Projects
None yet
Development

No branches or pull requests

3 participants