解決 WPF 渲染執行緒失敗問題

本文討論 Windows Presentation Foundation (WPF) 轉譯線程中的失敗。 本文著重於SyncFlushNotifyPartitionIsZombie中發生的例外狀況,以及WaitForNextMessageSynchronizeChannel中發生的程式無回應情況。

WPF 應用程式可能有一或多個 UI 線程正在執行自己的訊息幫浦 (Dispatcher.Run)。 每個UI線程都負責處理線程消息佇列中的視窗訊息,並將其分派至線程所擁有的視窗。 每個 WPF 應用程式只有一個轉譯線程。 如果使用軟體轉譯管線,該個別線程會與 Microsoft DirectX D3D 通訊(或 GDI)。 針對WPF內容,每個UI線程都會將詳細指示傳送至要繪製的轉譯線程。 接著,轉譯線程會遵循這些指示來轉譯內容。

適用於: .NET Framework 4.8

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 失敗

開發人員通常會遇到與 WPF 應用程式中發生之轉譯線程失敗相關的問題。 使用者可能會報告他們的應用程式拋出例外狀況,例如:

  • System.Runtime.InteropServices.COMException: UCEERR_RENDERTHREADFAILURE (HRESULT 的例外狀況:0x88980406)
  • System.InvalidOperationException:轉譯線程上發生未指定的錯誤。
  • System.OutOfMemoryException:記憶體不足,無法繼續執行程式。

相關的呼叫堆疊會從 SyncFlushNotifyPartitionIsZombie開始。 例如:

   at System.Windows.Media.Composition.DUCE.Channel.SyncFlush()  
   at System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget, Nullable\`1 channelSet)  
   at System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget)  
   at System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr lParam)  
   at System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)  
   at System.Windows.Media.MediaContext.NotifyPartitionIsZombie(Int32 failureCode)  
   at System.Windows.Media.MediaContext.NotifyChannelMessage()  
   at System.Windows.Interop.HwndTarget.HandleMessage(Int32 msg, IntPtr wparam, IntPtr lparam)  

應用程式可能會在 WaitForNextMessageSynchronizeChannel 中停止回應,並產生呼叫堆疊,例如:

   ntdll.dll!NtWaitForMultipleObjects
   kernelbase.dll!WaitForMultipleObjectsEx
   kernelbase.dll!WaitForMultipleObjects
   wpfgfx_v0400.dll!CMilChannel::WaitForNextMessage
   wpfgfx_v0400.dll!MilComposition_WaitForNextMessage
   presentationcore.dll!System.Windows.Media.MediaContext.CompleteRender
   kernelbase.dll!WaitForSingleObject
   wpfgfx_v0400.dll!CMilConnection::SynchronizeChannel
   wpfgfx_v0400.dll!CMilChannel::SyncFlush
   presentationcore.dll!System.Windows.Media.Composition.DUCE+Channel.SyncFlush
   presentationcore.dll!System.Windows.Media.MediaContext.CompleteRender
   presentationcore.dll!System.Windows.Interop.HwndTarget.OnResize
   presentationcore.dll!System.Windows.Interop.HwndTarget.HandleMessage

這些呼叫堆疊是轉譯線程失敗的徵兆。 這是診斷的一個具有挑戰性的問題,因為例外狀況和呼叫堆疊是泛型的。 轉譯線程失敗會產生此處所列的其中一個呼叫堆疊(或呼叫堆棧的次要變化),而不論根本原因為何。 因此,難以診斷問題,或辨識不同非回應事件有相同根本原因的情況。

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 失敗的原因

如果 WPF 轉譯線程發生嚴重錯誤,軟體會停止在 UI 線程中回應的例外狀況和情況。 這些錯誤有數個可能的原因,但轉譯線程不會與UI線程共用該資訊。 因為這些錯誤不涉及單一的根本錯誤或問題,因此沒有特定的解決方法。

WPF 的轉譯線程會在呼叫另一個元件時檢查傳回值是否成功或失敗,例如 DirectX D3D、User32 或 GDI32。 偵測失敗時,WPF 會將渲染分區「殭屍化」,並在兩個執行緒同步後通知 UI 執行緒該失敗情況。 轉譯線程會嘗試將它收到的失敗對應至適當的Managed 例外狀況。 例如,如果 WPF 轉譯線程因為記憶體不足狀況而失敗,則會將失敗對應至 System.OutOfMemoryException。 該例外狀況會顯示在UI線程上。 轉譯線程只會在少數位置與UI線程同步處理。 因此,上一節中提及的呼叫堆疊通常會出現在您注意到問題徵兆的位置,而不是問題實際發生的地方。 同步處理最常發生在窗口設定更新的位置(大小、位置等)或 UI 線程處理轉譯線程中的「通道」訊息的位置。

根據設計,UI 線程上的例外狀況和呼叫堆疊並不是有用的資源,可協助您診斷問題。 在擲回例外狀況時,渲染線程已經到達失敗點。 轉譯線程的重要狀態可協助您了解失敗發生的位置和原因,但它已經遺失。 由於這種情況,WPF 應用程式的作者無法得知發生失敗的原因,或如何避免失敗。 我們不會分析例外狀況和呼叫堆疊,而是在事後使用者傾印檔案中對問題進行偵錯。 雖然這個方法只是略微有用,但渲染線程會保留失敗呼叫堆棧的循環緩衝區。 我們可以使用專屬調試程式延伸模組和私人偵錯符號,在內部重新建構緩衝區,以顯示近似的初始失敗點。 不過,我們無法存取失敗時的關鍵狀態,例如局部變數、堆疊變數和堆物件。 我們通常會再次執行應用程式,以尋找我們懷疑涉及的呼叫失敗。

視訊硬體或驅動程序失敗

最常見的 WPF 轉譯線程失敗貯體與視訊硬體或驅動程式問題相關聯。 當 WPF 透過 DirectX 查詢視訊驅動程式是否有功能時,驅動程式可能會誤報其功能。 此動作會讓 WPF 採取導致某些 DirectX D3D 失敗的程式代碼路徑。 驅動程式可能也未被正確地實作。 大部分的渲染線程失敗是因為 WPF 嘗試以某種方式使用硬體渲染管線,而暴露了驅動程式中的一些缺陷。 使用新式圖形裝置和驅動程式的新式 Windows 版本可能會發生此狀況,但與 WPF 早期的情況一樣常見。 基於這個理由,當您開始測試或解決轉譯線程失敗時,建議您先在 WPF 中停用硬體加速。

如果應用程式要求讓驅動程式 (或 DirectX) 轉譯太複雜場景,也可能會發生失敗。 新式驅動程式並不常見這種情況。 不過,每個裝置都有可超過的限制。

另一個轉譯線程失敗的歷史來源是 WPF 中的 Window.AllowsTransparencyPopup.AllowsTransparency 屬性。 這些屬性會導致使用 分層視窗 。 分層視窗在舊版 Windows 中造成問題。 不過,大部分的問題都是在 Windows Vista 中引進桌面視窗管理員 (DWM) 來解決的。

如果轉譯線程失敗顯示為 System.OutOfMemoryException,則此錯誤通常表示進程耗盡了某些資源。 在此情況下,轉譯線程呼叫至 Win32/DX API,而該 API 嘗試分配某些資源未成功。 WPF 映射會傳回值,例如 E_OUTOFMEMORYERROR_NOT_ENOUGH_MEMORYSystem.OutOfMemoryException。 雖然例外項目是指「記憶體」,但此指稱可以指任何類型的資源,例如 GDI 物件控制代碼、其他系統控制代碼、GPU 記憶體、標準 RAM 記憶體等等。

資源配置失敗的備註

下列備註適用於 System.OutOfMemoryException 失敗以及任何資源分配失敗:

  • 根本原因可能與發生失敗的程式代碼無關。 進程中的其他程式代碼可以過度取用資源,以致後續程式代碼無法成功運行。

  • 如果要求非常大,即使資源看似豐富,仍可能會發生失敗。 即使系統有足夠的記憶體,大量(連續)記憶體的要求仍可能會造成 System.OutOfMemoryException 。 以下是真實世界的範例:Visual Studio 外掛程式準備將其視窗從先前工作階段儲存的狀態進行還原。 外掛程式針對先前和當前監視器之間的 DPI 差異進行了不正確的調整。 由於此錯誤是由數層 WPF、WindowsForms 和 VS 視窗承載元件的調整所複合,因此附加元件將視窗大小設定為正確大小的 16 倍。 然後,轉譯線程會嘗試配置大於必要 256 倍的後端緩衝區。 因此,即使預期的配置有足夠的可用記憶體,進程仍會失敗。

一般建議

  1. 停用硬體轉譯。 使用停用硬體加速選項中討論的 DisableHWAcceleration 登錄值。 此動作會影響您電腦上的所有 WPF 應用程式。 請只執行此步驟來測試您的問題是否與圖形硬體或驅動程序有關。 如果是,您可以透過以程序設計方式在更細微的層級停用硬體加速來解決此問題。 此步驟可透過HwndTarget.RenderMode 屬性,以個別視窗為基礎來完成,或使用 RenderOptions.ProcessRenderMode 屬性,以每個進程為基礎完成。

  2. 更新您的視訊驅動程式,或在問題計算機上嘗試不同的視訊硬體。

  3. 升級至適用於您目標平臺的最新版本和服務包層級的 Microsoft .NET Framework。

  4. 升級至最新的操作系統。

  5. 停用在應用程式中使用 Windows.AllowsTransparencyPopup.AllowsTransparency 的功能。

  6. 如果 System.OutOfMemoryExceptions 被報告,請在效能監視器中監控程序記憶體使用量。 特別是,監視 Process\Virtual Bytes、Process\Private Bytes 和 .NET CLR Memory\# Bytes in All Heaps 這三個計數器的數據。 同時監視 Windows 任務管理器中進程的用戶物件和 GDI 物件。 當您發現某個特定資源正在被耗盡時,請對應用程式進行疑難排解,以修復過度的資源消耗問題。 作為指導方針,請遵循上一節中關於資源配置問題的兩個備註。

  7. 如果您有跨平台或不同影音硬體或驅動程式組合中出現的重現性問題,這可能是 WPF 的錯誤。 請確定您收集足夠的資訊,以啟用調查,再向Microsoft報告問題。 呼叫堆疊本身是不夠的。 我們需要更詳細的資訊,例如:

    • 完整的 VS 解決方案,包含重現問題的步驟,包括環境的描述(OS、.NET 和圖形)。
    • 問題的 「時間移動偵錯」追蹤
    • 完整崩潰記憶體傾印檔案。