gpu: a device that cannot render the effect stage falls back to CPU effects instead of panicking

The GPU effect stage (#35) renders into Rgba32Float, which is not renderable on every device (the FreeBSD CI's software adapter rejects it). FxStage::supported only checks compute/storage limits, so GpuCompositor::new created an invalid pipeline and wgpu's default error handler panicked. Stage creation now runs inside a validation error scope; on an error the compositor runs without it and effects render on the CPU, as on devices without compute shaders. Found by the FreeBSD CI job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-06 18:23:57 -04:00
1 parent f2b36fe277
commit c736a97432
1 file changed
+27 -1
+27 -1
View File
@@ -317,7 +317,23 @@ impl GpuCompositor {
view_formats: &[],
});
let dummy = dummy_tex.create_view(&Default::default());
let fx = fx::FxStage::supported(device).then(|| fx::FxStage::new(device, &shader, &layer_bgl));
// The effect stage's pipelines can still be invalid on a device that passes the limits
// check (Rgba32Float is not renderable everywhere, e.g. software Vulkan/GL). Validation
// errors would panic, so they are captured: without the stage, effects render on the CPU.
let fx = fx::FxStage::supported(device)
.then(|| {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let stage = fx::FxStage::new(device, &shader, &layer_bgl);
match poll_now(scope.pop()) {
Some(None) => Some(stage),
Some(Some(e)) => {
log::warn!("GPU effects unavailable on this device, rendering them on the CPU: {e}");
None
}
None => None,
}
})
.flatten();
Self {
device: device.clone(),
queue: queue.clone(),
@@ -846,6 +862,16 @@ struct SrcInfo {
color: filmcraft_color::ColorInfo,
}
/// Poll a future once: wgpu's native error scopes resolve immediately. `None` if it is still
/// pending (then the caller assumes the worst).
fn poll_now<F: std::future::Future>(f: F) -> Option<F::Output> {
let mut f = std::pin::pin!(f);
match f.as_mut().poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) {
std::task::Poll::Ready(v) => Some(v),
std::task::Poll::Pending => None,
}
}
/// A render pass drawing into the accumulator.
fn accum_pass<'e>(enc: &'e mut wgpu::CommandEncoder, view: &wgpu::TextureView, load: wgpu::LoadOp<wgpu::Color>) -> wgpu::RenderPass<'e> {
enc.begin_render_pass(&wgpu::RenderPassDescriptor {