Skip to content
6 changes: 6 additions & 0 deletions apps/desktop/src-tauri/src/gpu_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ pub struct SharedGpuContext {
static GPU: OnceCell<Option<SharedGpuContext>> = OnceCell::const_new();

async fn init_gpu_inner() -> Option<SharedGpuContext> {
#[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
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/utils/webgpu-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export async function isWebGPUSupported(): Promise<boolean> {
return false;
}
try {
const adapter = await navigator.gpu.requestAdapter();
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
});
Comment on lines +84 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option can throw or be ignored on some WebGPU implementations; falling back to a plain requestAdapter() keeps the capability check from returning false for supported devices.

Suggested change
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
});
const adapter = await navigator.gpu
.requestAdapter({
powerPreference: "high-performance",
})
.catch(() => navigator.gpu.requestAdapter());

return adapter !== null;
} catch {
return false;
Expand All @@ -91,7 +93,9 @@ export async function isWebGPUSupported(): Promise<boolean> {
export async function initWebGPU(
canvas: OffscreenCanvas,
): Promise<WebGPURenderer> {
const adapter = await navigator.gpu.requestAdapter();
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
});
if (!adapter) {
throw new Error("No WebGPU adapter available");
}
Comment on lines +96 to 101
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea here: try high-performance, but fall back to default adapter request so initialization still works if the preference is unsupported.

Suggested change
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
});
if (!adapter) {
throw new Error("No WebGPU adapter available");
}
let adapter = await navigator.gpu
.requestAdapter({
powerPreference: "high-performance",
})
.catch(() => null);
adapter ??= await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No WebGPU adapter available");
}

Expand Down
46 changes: 37 additions & 9 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,48 @@ impl Renderer {
break;
}
}
match frame_renderer
.render_immediate(
current.segment_frames,
current.uniforms,
let nv12_result = frame_renderer
.render_nv12(
current.segment_frames.clone(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.clone() on segment_frames may be expensive if DecodedFrame contains large buffers. Verify whether this clone is necessary or if the data can be moved/borrowed instead.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/editor/src/editor.rs
Line: 152:152

Comment:
`.clone()` on `segment_frames` may be expensive if `DecodedFrame` contains large buffers. Verify whether this clone is necessary or if the data can be moved/borrowed instead.

How can I resolve this? If you propose a fix, please make it concise.

current.uniforms.clone(),
&current.cursor,
&mut layers,
)
.await
{
Ok(frame) => {
(self.frame_cb)(EditorFrameOutput::Rgba(frame));
.await;

match nv12_result {
Ok(pipeline_frame) => {
if let Some(prev) = pipeline_frame {
(self.frame_cb)(EditorFrameOutput::Nv12(prev));
}
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) => {
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,
&current.cursor,
&mut layers,
)
.await
{
Ok(frame) => {
(self.frame_cb)(EditorFrameOutput::Rgba(frame));
}
Err(e) => {
tracing::error!(error = %e, "Failed to render frame in editor");
}
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/gpu-converters/src/bgra_rgba/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ pub struct BGRAToRGBA {

impl BGRAToRGBA {
pub async fn new() -> Result<Self, GpuConverterError> {
#[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
Expand Down
7 changes: 6 additions & 1 deletion crates/gpu-converters/src/nv12_rgba/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions crates/gpu-converters/src/uyvy_rgba/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions crates/gpu-converters/src/yuyv_nv12/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ pub struct YUYVToNV12 {

impl YUYVToNV12 {
pub async fn new() -> Result<Self, GpuConverterError> {
#[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
Expand Down
6 changes: 6 additions & 0 deletions crates/gpu-converters/src/yuyv_rgba/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ pub struct YUYVToRGBA {

impl YUYVToRGBA {
pub async fn new() -> Result<Self, GpuConverterError> {
#[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
Expand Down
149 changes: 121 additions & 28 deletions crates/rendering/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,38 +667,131 @@ pub async fn spawn_decoder(

#[cfg(target_os = "windows")]
{
let _ = force_ffmpeg;
let (ready_tx, ready_rx) = oneshot::channel::<Result<DecoderInitResult, String>>();
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::<Result<DecoderInitResult, String>>();
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::<Result<DecoderInitResult, String>>();
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!(
"'{name}' MediaFoundation initialization failed: {e} ({path_display})"
)),
Ok(Err(e)) => Err(format!(
"'{name}' MediaFoundation channel closed: {e} ({path_display})"
)),
Err(_) => Err(format!(
"'{name}' MediaFoundation timed out after 30s initializing: {path_display}"
)),
},
Err(e) => Err(format!(
"'{name}' MediaFoundation spawn failed: {e} ({path_display})"
)),
}
};

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::<Result<DecoderInitResult, String>>();
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}"
)),
}
}

Expand Down
Loading
Loading