From e23dfb6e2612abce08dbc373b364f0fbfabf6550 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:55:36 +0000 Subject: [PATCH 1/8] perf(windows): prefer Dx12 backend for wgpu on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explicitly request DX12 | Vulkan backends in wgpu InstanceDescriptor on Windows instead of using the default (all backends). DX12 provides optimal D3D11↔D3D12 interop for shared texture handles from the MediaFoundation decoder and is generally the most performant backend on Windows. Non-Windows platforms continue using the default InstanceDescriptor. Co-authored-by: Richie McIlroy --- apps/desktop/src-tauri/src/gpu_context.rs | 6 ++++++ crates/rendering/src/lib.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/apps/desktop/src-tauri/src/gpu_context.rs b/apps/desktop/src-tauri/src/gpu_context.rs index 275cc48e1a2..d9f8ac62217 100644 --- a/apps/desktop/src-tauri/src/gpu_context.rs +++ b/apps/desktop/src-tauri/src/gpu_context.rs @@ -49,6 +49,12 @@ pub struct SharedGpuContext { static GPU: OnceCell> = OnceCell::const_new(); async fn init_gpu_inner() -> Option { + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let hardware_adapter = instance diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index fdf8129fead..5690609fec5 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -979,6 +979,12 @@ impl RenderVideoConstants { .map(|c| XY::new(c.width, c.height)), }; + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let hardware_adapter = instance From 4ded37363391ed7c6fdb489c1b6342de62df6e7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:56:20 +0000 Subject: [PATCH 2/8] perf(windows): enable MediaFoundation decoder with FFmpeg fallback On Windows, try the native MediaFoundation hardware decoder first before falling back to FFmpeg. This mirrors the macOS pattern (AVAssetReader first, FFmpeg fallback). The MF decoder provides: - Native D3D11VA hardware-accelerated video decoding - D3D11 shared texture handles (Y/UV planes) for zero-copy GPU texture sharing - More reliable hardware acceleration than FFmpeg's D3D11VA path If MF initialization fails for any reason, the existing FFmpeg path is used as a fallback, preserving backward compatibility. The force_ffmpeg flag also bypasses MF entirely when set. Co-authored-by: Richie McIlroy --- crates/rendering/src/decoder/mod.rs | 143 ++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 28 deletions(-) diff --git a/crates/rendering/src/decoder/mod.rs b/crates/rendering/src/decoder/mod.rs index 1782b5bd865..07de65e68ce 100644 --- a/crates/rendering/src/decoder/mod.rs +++ b/crates/rendering/src/decoder/mod.rs @@ -667,38 +667,125 @@ pub async fn spawn_decoder( #[cfg(target_os = "windows")] { - let _ = force_ffmpeg; - let (ready_tx, ready_rx) = oneshot::channel::>(); - let (tx, rx) = mpsc::channel(); + if force_ffmpeg { + info!( + "Video '{}' using FFmpeg decoder (forced via experimental setting)", + name + ); + let (ready_tx, ready_rx) = oneshot::channel::>(); + let (tx, rx) = mpsc::channel(); - ffmpeg::FfmpegDecoder::spawn(name, path, fps, rx, ready_tx) - .map_err(|e| format!("'{name}' FFmpeg decoder / {e}"))?; + ffmpeg::FfmpegDecoder::spawn(name, path, fps, rx, ready_tx) + .map_err(|e| format!("'{name}' FFmpeg decoder / {e}"))?; - match tokio::time::timeout(timeout_duration, ready_rx).await { - Ok(Ok(Ok(init_result))) => { - info!( - "Video '{}' using {} decoder ({}x{})", - name, init_result.decoder_type, init_result.width, init_result.height + return match tokio::time::timeout(timeout_duration, ready_rx).await { + Ok(Ok(Ok(init_result))) => { + info!( + "Video '{}' using {} decoder ({}x{})", + name, init_result.decoder_type, init_result.width, init_result.height + ); + let status = DecoderStatus { + decoder_type: init_result.decoder_type, + video_width: init_result.width, + video_height: init_result.height, + fallback_reason: None, + }; + Ok(AsyncVideoDecoderHandle { + sender: tx, + offset, + status, + }) + } + Ok(Ok(Err(e))) => Err(format!( + "'{name}' FFmpeg decoder initialization failed: {e}" + )), + Ok(Err(e)) => Err(format!("'{name}' FFmpeg decoder channel closed: {e}")), + Err(_) => Err(format!( + "'{name}' FFmpeg decoder timed out after 30s initializing: {path_display}" + )), + }; + } + + let mf_result = { + let (ready_tx, ready_rx) = oneshot::channel::>(); + let (tx, rx) = mpsc::channel(); + + match media_foundation::MFDecoder::spawn(name, path.clone(), fps, rx, ready_tx) { + Ok(()) => match tokio::time::timeout(timeout_duration, ready_rx).await { + Ok(Ok(Ok(init_result))) => { + info!( + "Video '{}' using {} decoder ({}x{})", + name, init_result.decoder_type, init_result.width, init_result.height + ); + let status = DecoderStatus { + decoder_type: init_result.decoder_type, + video_width: init_result.width, + video_height: init_result.height, + fallback_reason: None, + }; + Ok(AsyncVideoDecoderHandle { + sender: tx, + offset, + status, + }) + } + Ok(Ok(Err(e))) => Err(format!("MediaFoundation initialization failed: {e}")), + Ok(Err(e)) => Err(format!("MediaFoundation channel closed: {e}")), + Err(_) => Err(format!( + "MediaFoundation timed out after 30s initializing: {path_display}" + )), + }, + Err(e) => Err(format!("MediaFoundation spawn failed: {e}")), + } + }; + + match mf_result { + Ok(handle) => Ok(handle), + Err(mf_error) => { + tracing::warn!( + name = name, + error = %mf_error, + "MediaFoundation failed, falling back to FFmpeg decoder" ); - let status = DecoderStatus { - decoder_type: init_result.decoder_type, - video_width: init_result.width, - video_height: init_result.height, - fallback_reason: None, - }; - Ok(AsyncVideoDecoderHandle { - sender: tx, - offset, - status, - }) + + let (ready_tx, ready_rx) = oneshot::channel::>(); + let (tx, rx) = mpsc::channel(); + + if let Err(e) = ffmpeg::FfmpegDecoder::spawn(name, path, fps, rx, ready_tx) { + return Err(format!( + "'{name}' decoder failed - MediaFoundation: {mf_error}, FFmpeg: {e}" + )); + } + + match tokio::time::timeout(timeout_duration, ready_rx).await { + Ok(Ok(Ok(init_result))) => { + info!( + "Video '{}' using {} decoder ({}x{}) after MediaFoundation failure", + name, init_result.decoder_type, init_result.width, init_result.height + ); + let status = DecoderStatus { + decoder_type: init_result.decoder_type, + video_width: init_result.width, + video_height: init_result.height, + fallback_reason: Some(mf_error), + }; + Ok(AsyncVideoDecoderHandle { + sender: tx, + offset, + status, + }) + } + Ok(Ok(Err(e))) => Err(format!( + "'{name}' decoder failed - MediaFoundation: {mf_error}, FFmpeg: {e}" + )), + Ok(Err(e)) => Err(format!( + "'{name}' decoder failed - MediaFoundation: {mf_error}, FFmpeg channel: {e}" + )), + Err(_) => Err(format!( + "'{name}' decoder failed - MediaFoundation: {mf_error}, FFmpeg timed out" + )), + } } - Ok(Ok(Err(e))) => Err(format!( - "'{name}' FFmpeg decoder initialization failed: {e}" - )), - Ok(Err(e)) => Err(format!("'{name}' FFmpeg decoder channel closed: {e}")), - Err(_) => Err(format!( - "'{name}' FFmpeg decoder timed out after 30s initializing: {path_display}" - )), } } From 47e0026e32be6ce1dc2b47d48c41d1f46de2c068 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:57:13 +0000 Subject: [PATCH 3/8] perf(windows): add D3D11 zero-copy to prepare_with_encoder NV12 path The prepare_with_encoder() method in DisplayLayer now checks for D3D11 shared texture handles (from the MediaFoundation decoder) before falling back to CPU data upload. This enables zero-copy GPU texture sharing when MF decoder provides shared Y/UV plane handles. The non-Windows code path is completely unchanged - it is now in its own #[cfg(not(target_os = "windows"))] block for clarity. Co-authored-by: Richie McIlroy --- crates/rendering/src/layers/display.rs | 126 ++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 13 deletions(-) diff --git a/crates/rendering/src/layers/display.rs b/crates/rendering/src/layers/display.rs index 05d545ce0a4..043e81d87ad 100644 --- a/crates/rendering/src/layers/display.rs +++ b/crates/rendering/src/layers/display.rs @@ -472,21 +472,121 @@ impl DisplayLayer { PixelFormat::Nv12 => { let screen_frame = &segment_frames.screen_frame; - if !self.prefer_cpu_conversion { + #[cfg(target_os = "windows")] + let d3d11_zero_copy_succeeded = { + let mut succeeded = false; + if !self.prefer_cpu_conversion { + if let (Some(y_handle), Some(uv_handle)) = ( + screen_frame.d3d11_y_handle(), + screen_frame.d3d11_uv_handle(), + ) { + if self + .yuv_converter + .convert_nv12_from_d3d11_shared_handles( + device, + queue, + y_handle, + uv_handle, + actual_width, + actual_height, + ) + .is_ok() + && self.yuv_converter.output_texture().is_some() + { + self.pending_copy = Some(PendingTextureCopy { + width: actual_width, + height: actual_height, + dst_texture_index: next_texture, + }); + succeeded = true; + } + } + } + succeeded + }; + + #[cfg(target_os = "windows")] + if d3d11_zero_copy_succeeded { + true + } else if !self.prefer_cpu_conversion { if let (Some(y_data), Some(uv_data)) = (screen_frame.y_plane(), screen_frame.uv_plane()) { let y_stride = screen_frame.y_stride(); let uv_stride = screen_frame.uv_stride(); - #[cfg(target_os = "windows")] - let convert_width = actual_width; - #[cfg(target_os = "windows")] - let convert_height = actual_height; - #[cfg(not(target_os = "windows"))] - let convert_width = frame_size.x; - #[cfg(not(target_os = "windows"))] - let convert_height = frame_size.y; + let convert_result = self.yuv_converter.convert_nv12_to_encoder( + device, + queue, + encoder, + y_data, + uv_data, + actual_width, + actual_height, + y_stride, + uv_stride, + ); + + match convert_result { + Ok(_) => { + if self.yuv_converter.output_texture().is_some() { + self.pending_copy = Some(PendingTextureCopy { + width: actual_width, + height: actual_height, + dst_texture_index: next_texture, + }); + true + } else { + false + } + } + Err(_) => false, + } + } else { + false + } + } else if let (Some(y_data), Some(uv_data)) = + (screen_frame.y_plane(), screen_frame.uv_plane()) + { + let y_stride = screen_frame.y_stride(); + let uv_stride = screen_frame.uv_stride(); + let convert_result = self.yuv_converter.convert_nv12_cpu( + device, + queue, + y_data, + uv_data, + actual_width, + actual_height, + y_stride, + uv_stride, + ); + + match convert_result { + Ok(_) => { + if self.yuv_converter.output_texture().is_some() { + self.pending_copy = Some(PendingTextureCopy { + width: actual_width, + height: actual_height, + dst_texture_index: next_texture, + }); + true + } else { + false + } + } + Err(_) => false, + } + } else { + false + } + + #[cfg(not(target_os = "windows"))] + if !self.prefer_cpu_conversion { + if let (Some(y_data), Some(uv_data)) = + (screen_frame.y_plane(), screen_frame.uv_plane()) + { + let y_stride = screen_frame.y_stride(); + let uv_stride = screen_frame.uv_stride(); let convert_result = self.yuv_converter.convert_nv12_to_encoder( device, @@ -494,8 +594,8 @@ impl DisplayLayer { encoder, y_data, uv_data, - convert_width, - convert_height, + frame_size.x, + frame_size.y, y_stride, uv_stride, ); @@ -504,8 +604,8 @@ impl DisplayLayer { Ok(_) => { if self.yuv_converter.output_texture().is_some() { self.pending_copy = Some(PendingTextureCopy { - width: convert_width, - height: convert_height, + width: frame_size.x, + height: frame_size.y, dst_texture_index: next_texture, }); true From 2883ccab3dc08bc1f8e8eeaf17be03d26d398c54 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:58:01 +0000 Subject: [PATCH 4/8] perf: switch editor renderer to NV12 output with RGBA fallback Change the editor frame renderer to output NV12 frames instead of RGBA. NV12 is ~33% smaller (1.5 bytes/pixel vs 4 bytes/pixel), significantly reducing WebSocket bandwidth during playback. The frontend WebSocket handling and WebGPU renderer already fully support NV12 frames, so no frontend changes are needed. If NV12 rendering fails for any reason, the renderer falls back to the existing RGBA path. Co-authored-by: Richie McIlroy --- crates/editor/src/editor.rs | 40 ++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f378538a08f..399987888d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -147,20 +147,42 @@ impl Renderer { break; } } - match frame_renderer - .render_immediate( - current.segment_frames, - current.uniforms, + let nv12_result = frame_renderer + .render_nv12( + current.segment_frames.clone(), + current.uniforms.clone(), ¤t.cursor, &mut layers, ) - .await - { - Ok(frame) => { - (self.frame_cb)(EditorFrameOutput::Rgba(frame)); + .await; + + match nv12_result { + Ok(Some(frame)) => { + (self.frame_cb)(EditorFrameOutput::Nv12(frame)); + } + Ok(None) => { + if let Some(Ok(flushed)) = frame_renderer.flush_pipeline_nv12().await { + (self.frame_cb)(EditorFrameOutput::Nv12(flushed)); + } } Err(e) => { - tracing::error!(error = %e, "Failed to render frame in editor"); + tracing::warn!(error = %e, "NV12 render failed, falling back to RGBA"); + match frame_renderer + .render_immediate( + current.segment_frames, + current.uniforms, + ¤t.cursor, + &mut layers, + ) + .await + { + Ok(frame) => { + (self.frame_cb)(EditorFrameOutput::Rgba(frame)); + } + Err(e) => { + tracing::error!(error = %e, "Failed to render frame in editor"); + } + } } } From c5f39828e8527bca1f1d87b42fc6add0b7c78393 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:58:33 +0000 Subject: [PATCH 5/8] perf: request high-performance GPU adapter in frontend WebGPU Add powerPreference: 'high-performance' to both isWebGPUSupported() and initWebGPU() calls. This ensures the discrete GPU is selected on systems with both integrated and discrete GPUs, providing better frame rendering performance in the editor preview canvas. Co-authored-by: Richie McIlroy --- apps/desktop/src/utils/webgpu-renderer.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/utils/webgpu-renderer.ts b/apps/desktop/src/utils/webgpu-renderer.ts index b33f859e784..3ab05955aae 100644 --- a/apps/desktop/src/utils/webgpu-renderer.ts +++ b/apps/desktop/src/utils/webgpu-renderer.ts @@ -81,7 +81,9 @@ export async function isWebGPUSupported(): Promise { return false; } try { - const adapter = await navigator.gpu.requestAdapter(); + const adapter = await navigator.gpu.requestAdapter({ + powerPreference: "high-performance", + }); return adapter !== null; } catch { return false; @@ -91,7 +93,9 @@ export async function isWebGPUSupported(): Promise { export async function initWebGPU( canvas: OffscreenCanvas, ): Promise { - const adapter = await navigator.gpu.requestAdapter(); + const adapter = await navigator.gpu.requestAdapter({ + powerPreference: "high-performance", + }); if (!adapter) { throw new Error("No WebGPU adapter available"); } From 0281ad4090f4bcca4b5d2e814a1e6d5d36cf5842 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 21:59:34 +0000 Subject: [PATCH 6/8] perf(windows): prefer Dx12 backend in gpu-converters Apply the same DX12 | Vulkan backend preference to all gpu-converter crates for consistency with the rendering and gpu_context changes. Co-authored-by: Richie McIlroy --- crates/gpu-converters/src/bgra_rgba/mod.rs | 6 ++++++ crates/gpu-converters/src/nv12_rgba/mod.rs | 7 ++++++- crates/gpu-converters/src/uyvy_nv12/mod.rs | 6 ++++++ crates/gpu-converters/src/uyvy_rgba/mod.rs | 6 ++++++ crates/gpu-converters/src/yuyv_nv12/mod.rs | 6 ++++++ crates/gpu-converters/src/yuyv_rgba/mod.rs | 6 ++++++ 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/gpu-converters/src/bgra_rgba/mod.rs b/crates/gpu-converters/src/bgra_rgba/mod.rs index 52e11dcca4e..6c2dbb05d1d 100644 --- a/crates/gpu-converters/src/bgra_rgba/mod.rs +++ b/crates/gpu-converters/src/bgra_rgba/mod.rs @@ -14,6 +14,12 @@ pub struct BGRAToRGBA { impl BGRAToRGBA { pub async fn new() -> Result { + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance diff --git a/crates/gpu-converters/src/nv12_rgba/mod.rs b/crates/gpu-converters/src/nv12_rgba/mod.rs index 1ab46be9eea..037e9df8e9a 100644 --- a/crates/gpu-converters/src/nv12_rgba/mod.rs +++ b/crates/gpu-converters/src/nv12_rgba/mod.rs @@ -12,9 +12,14 @@ pub struct NV12ToRGBA { impl NV12ToRGBA { pub async fn new() -> Self { println!("NV12ToRGBA"); + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); - // Get adapter for GPU let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, diff --git a/crates/gpu-converters/src/uyvy_nv12/mod.rs b/crates/gpu-converters/src/uyvy_nv12/mod.rs index c083ec47980..142e2be65f1 100644 --- a/crates/gpu-converters/src/uyvy_nv12/mod.rs +++ b/crates/gpu-converters/src/uyvy_nv12/mod.rs @@ -14,6 +14,12 @@ impl UYVYToNV12 { pub async fn new() -> Self { todo!("implement UV downsampling for UYVYToNV12"); + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance diff --git a/crates/gpu-converters/src/uyvy_rgba/mod.rs b/crates/gpu-converters/src/uyvy_rgba/mod.rs index 84fa3e86149..4181cb430ad 100644 --- a/crates/gpu-converters/src/uyvy_rgba/mod.rs +++ b/crates/gpu-converters/src/uyvy_rgba/mod.rs @@ -14,6 +14,12 @@ pub struct UYVYToRGBA { impl UYVYToRGBA { pub async fn new() -> Self { + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance diff --git a/crates/gpu-converters/src/yuyv_nv12/mod.rs b/crates/gpu-converters/src/yuyv_nv12/mod.rs index 62dcbb0bf51..6ab6482f19d 100644 --- a/crates/gpu-converters/src/yuyv_nv12/mod.rs +++ b/crates/gpu-converters/src/yuyv_nv12/mod.rs @@ -11,6 +11,12 @@ pub struct YUYVToNV12 { impl YUYVToNV12 { pub async fn new() -> Result { + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance diff --git a/crates/gpu-converters/src/yuyv_rgba/mod.rs b/crates/gpu-converters/src/yuyv_rgba/mod.rs index 30d9c3d86c7..50602a9ee43 100644 --- a/crates/gpu-converters/src/yuyv_rgba/mod.rs +++ b/crates/gpu-converters/src/yuyv_rgba/mod.rs @@ -13,6 +13,12 @@ pub struct YUYVToRGBA { impl YUYVToRGBA { pub async fn new() -> Result { + #[cfg(target_os = "windows")] + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, + ..Default::default() + }); + #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance From 62381d00f3a8eb4d1e7eac247c6ca370b79cb2b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 22:37:35 +0000 Subject: [PATCH 7/8] fix: always flush NV12 pipeline in editor and fix collapsible_if - Editor renderer now always flushes the NV12 pipeline after render_nv12() to ensure the current frame is emitted immediately. Previously, when render_nv12 returned Ok(Some(prev_frame)), the current frame would stay stuck in the GPU pipeline until the next render request arrived. This caused the last frame during playback to never be displayed, and preview frames could show stale content. - Fixed potential collapsible_if clippy violation in the Windows D3D11 zero-copy path in display.rs by combining nested if conditions into a single chained condition using let-chains. - Applied cargo fmt formatting fix. Co-authored-by: Richie McIlroy --- crates/editor/src/editor.rs | 12 +++---- crates/rendering/src/layers/display.rs | 46 ++++++++++++-------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 399987888d0..6dbeda8981a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -157,12 +157,12 @@ impl Renderer { .await; match nv12_result { - Ok(Some(frame)) => { - (self.frame_cb)(EditorFrameOutput::Nv12(frame)); - } - Ok(None) => { - if let Some(Ok(flushed)) = frame_renderer.flush_pipeline_nv12().await { - (self.frame_cb)(EditorFrameOutput::Nv12(flushed)); + Ok(pipeline_frame) => { + if let Some(prev) = pipeline_frame { + (self.frame_cb)(EditorFrameOutput::Nv12(prev)); + } + if let Some(Ok(current_frame)) = frame_renderer.flush_pipeline_nv12().await { + (self.frame_cb)(EditorFrameOutput::Nv12(current_frame)); } } Err(e) => { diff --git a/crates/rendering/src/layers/display.rs b/crates/rendering/src/layers/display.rs index 043e81d87ad..fcca1100d97 100644 --- a/crates/rendering/src/layers/display.rs +++ b/crates/rendering/src/layers/display.rs @@ -475,32 +475,30 @@ impl DisplayLayer { #[cfg(target_os = "windows")] let d3d11_zero_copy_succeeded = { let mut succeeded = false; - if !self.prefer_cpu_conversion { - if let (Some(y_handle), Some(uv_handle)) = ( + if !self.prefer_cpu_conversion + && let (Some(y_handle), Some(uv_handle)) = ( screen_frame.d3d11_y_handle(), screen_frame.d3d11_uv_handle(), - ) { - if self - .yuv_converter - .convert_nv12_from_d3d11_shared_handles( - device, - queue, - y_handle, - uv_handle, - actual_width, - actual_height, - ) - .is_ok() - && self.yuv_converter.output_texture().is_some() - { - self.pending_copy = Some(PendingTextureCopy { - width: actual_width, - height: actual_height, - dst_texture_index: next_texture, - }); - succeeded = true; - } - } + ) + && self + .yuv_converter + .convert_nv12_from_d3d11_shared_handles( + device, + queue, + y_handle, + uv_handle, + actual_width, + actual_height, + ) + .is_ok() + && self.yuv_converter.output_texture().is_some() + { + self.pending_copy = Some(PendingTextureCopy { + width: actual_width, + height: actual_height, + dst_texture_index: next_texture, + }); + succeeded = true; } succeeded }; From e53808aa0f8bb0e41f37e2b059da0e79f6d6e8f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Feb 2026 23:06:17 +0000 Subject: [PATCH 8/8] fix: address PR review feedback - Log flush_pipeline_nv12() errors instead of silently dropping them, making it easier to diagnose GPU pipeline issues during playback. - Add name and path context to MediaFoundation decoder error messages, matching the pattern used by FFmpeg decoder errors for consistent debugging experience. - Revert unnecessary Windows backend preference in uyvy_nv12 since the function body is unreachable (todo!() macro). Co-authored-by: Richie McIlroy --- crates/editor/src/editor.rs | 10 ++++++++-- crates/gpu-converters/src/uyvy_nv12/mod.rs | 6 ------ crates/rendering/src/decoder/mod.rs | 14 ++++++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6dbeda8981a..d7ce869c9df 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -161,8 +161,14 @@ impl Renderer { if let Some(prev) = pipeline_frame { (self.frame_cb)(EditorFrameOutput::Nv12(prev)); } - if let Some(Ok(current_frame)) = frame_renderer.flush_pipeline_nv12().await { - (self.frame_cb)(EditorFrameOutput::Nv12(current_frame)); + match frame_renderer.flush_pipeline_nv12().await { + Some(Ok(current_frame)) => { + (self.frame_cb)(EditorFrameOutput::Nv12(current_frame)); + } + Some(Err(e)) => { + tracing::warn!(error = %e, "Failed to flush NV12 pipeline frame"); + } + None => {} } } Err(e) => { diff --git a/crates/gpu-converters/src/uyvy_nv12/mod.rs b/crates/gpu-converters/src/uyvy_nv12/mod.rs index 142e2be65f1..c083ec47980 100644 --- a/crates/gpu-converters/src/uyvy_nv12/mod.rs +++ b/crates/gpu-converters/src/uyvy_nv12/mod.rs @@ -14,12 +14,6 @@ impl UYVYToNV12 { pub async fn new() -> Self { todo!("implement UV downsampling for UYVYToNV12"); - #[cfg(target_os = "windows")] - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::DX12 | wgpu::Backends::VULKAN, - ..Default::default() - }); - #[cfg(not(target_os = "windows"))] let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); let adapter = instance diff --git a/crates/rendering/src/decoder/mod.rs b/crates/rendering/src/decoder/mod.rs index 07de65e68ce..64d6e892621 100644 --- a/crates/rendering/src/decoder/mod.rs +++ b/crates/rendering/src/decoder/mod.rs @@ -729,13 +729,19 @@ pub async fn spawn_decoder( status, }) } - Ok(Ok(Err(e))) => Err(format!("MediaFoundation initialization failed: {e}")), - Ok(Err(e)) => Err(format!("MediaFoundation channel closed: {e}")), + Ok(Ok(Err(e))) => Err(format!( + "'{name}' MediaFoundation initialization failed: {e} ({path_display})" + )), + Ok(Err(e)) => Err(format!( + "'{name}' MediaFoundation channel closed: {e} ({path_display})" + )), Err(_) => Err(format!( - "MediaFoundation timed out after 30s initializing: {path_display}" + "'{name}' MediaFoundation timed out after 30s initializing: {path_display}" )), }, - Err(e) => Err(format!("MediaFoundation spawn failed: {e}")), + Err(e) => Err(format!( + "'{name}' MediaFoundation spawn failed: {e} ({path_display})" + )), } };