From da2d345d954274368d05f9270efb3bba5ac15dff Mon Sep 17 00:00:00 2001 From: ET Date: Wed, 18 Feb 2026 15:07:42 -0800 Subject: [PATCH] WpfGfx: Fix headless RenderTargetBitmap rendering - Preserve existing default partition-thread render behavior - Allow UI-thread synchronous compose path to render without display devices - Keep compatibility switch behavior unchanged --- .../src/WpfGfx/core/uce/composition.cpp | 6 ++++-- .../src/WpfGfx/core/uce/composition.h | 2 ++ .../src/WpfGfx/core/uce/connectioncontext.cpp | 9 ++++++++- src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partition.h | 1 + .../src/WpfGfx/core/uce/partitionthread.cpp | 9 ++++++++- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.cpp b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.cpp index 28ebeeee9c9..18d4e71afe5 100644 --- a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.cpp +++ b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.cpp @@ -597,6 +597,7 @@ CComposition::EndProcessVideo() //----------------------------------------------------------------------------- HRESULT CComposition::ProcessComposition( + bool canRenderWithoutDisplayDevices, __out_ecount(1) bool *pfPresentNeeded ) { @@ -643,7 +644,7 @@ HRESULT CComposition::ProcessComposition( // auto& compatSettings = g_pPartitionManager->GetCompatSettings(); - doRenderPass = compatSettings.ShouldRenderEvenWhenNoDisplayDevicesAreAvailable(); + doRenderPass = canRenderWithoutDisplayDevices || compatSettings.ShouldRenderEvenWhenNoDisplayDevicesAreAvailable(); // // Make sure that we invalidate all of the render targets and caches, // and notify any listeners that display set is not valid @@ -773,6 +774,7 @@ HRESULT CComposition::ProcessComposition( //------------------------------------------------------------------------------ HRESULT CComposition::Compose( + bool canRenderWithoutDisplayDevices, __out_ecount(1) bool *pfPresentNeeded ) { @@ -802,7 +804,7 @@ CComposition::Compose( // If not zombie, perform the render and optional present passes... // - IFC(ProcessComposition(&fPresentNeeded)); + IFC(ProcessComposition(canRenderWithoutDisplayDevices, &fPresentNeeded)); } else { diff --git a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.h b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.h index f17baeef1bc..513beab1dbb 100644 --- a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.h +++ b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/composition.h @@ -143,6 +143,7 @@ class CComposition : // Runs any necessary updates to the composition - all the resources // participating in the composition, the thread, or device management. override HRESULT Compose( + bool canRenderWithoutDisplayDevices, __out_ecount(1) bool *pfPresentNeeded ); @@ -285,6 +286,7 @@ class CComposition : // Performs the compositor duties by processing any pending batches, // updating the video subsystem, rendering and ticking animations. HRESULT ProcessComposition( + bool canRenderWithoutDisplayDevices, __out_ecount(1) bool *pfPresentNeeded ); diff --git a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/connectioncontext.cpp b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/connectioncontext.cpp index ab2cef68f3b..6cc42c75849 100644 --- a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/connectioncontext.cpp +++ b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/connectioncontext.cpp @@ -503,7 +503,14 @@ CConnectionContext::PresentAllPartitions() { bool fPresentNeeded = false; - MIL_THR(pServerEntry->pCompDevice->Compose(&fPresentNeeded)); + // So far this method is used only to synchronously present on managed UI + // thread when calling RenderTargetBitmap.Render or alike. This can happen + // when an app is running without any monitors, e.g. when Visual Studio + // restarts on a DevBox machine overnight after update. In this case we want + // to allow rendering without any displays. Otherwise RenderTargetBitmap images + // will appear blank when user reconnects to DevBox the next day. + MIL_THR(pServerEntry->pCompDevice->Compose( + /*canRenderWithoutDisplayDevices*/true, &fPresentNeeded)); if (hr != WGXERR_DISPLAYSTATEINVALID) { diff --git a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partition.h b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partition.h index 80301c0536e..b65fc1d8ed8 100644 --- a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partition.h +++ b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partition.h @@ -140,6 +140,7 @@ class __declspec(novtable) Partition : virtual ~Partition() {} virtual HRESULT Compose( + bool canRenderWithoutDisplayDevices, __out_ecount(1) bool *pfNeedsPresent ) = 0; diff --git a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partitionthread.cpp b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partitionthread.cpp index 0c3e1533725..0c445791f33 100644 --- a/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partitionthread.cpp +++ b/src/Microsoft.DotNet.Wpf/src/WpfGfx/core/uce/partitionthread.cpp @@ -131,7 +131,14 @@ CPartitionThread::RenderPartition( { HRESULT hr = S_OK; bool presentThisPartition = false; - MIL_THR(pPartition->Compose(&presentThisPartition)); + + // This code path is for regular WPF rendering. By default we do not allow + // rendering without display devices being present. App authors can still + // allow that with following switch in their app config file: + // + MIL_THR(pPartition->Compose( + /*canRenderWithoutDisplayDevices*/false, &presentThisPartition)); + if (FAILED(hr)) { //