GPU: adapters without float render targets keep the GPU path for effect-free documents
Once Format stopped failing first, the Linux runner reached the GPU tests for the first time: its software adapter (GL) can't render Rgba32Float or R32Float. The app already picks Rgba16Float there, but the effect-map pipelines (R32Float/R16Float) still panicked. They are now built inside an error scope; where they can't be, documents with layer effects use the CPU compositor and everything else stays on the GPU. The exact-parity tests skip on adapters without 32-bit float targets (the app uses the CPU compositor there), like they already skip without an adapter; the fallback-path tests build with try_new_with_format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
71272d5cd7
commit
66d5a5381e
|
|
@ -344,6 +344,9 @@ pub struct Compositor {
|
|||
max_dim: u32,
|
||||
frame: u64,
|
||||
acc_format: wgpu::TextureFormat,
|
||||
/// Whether the effect-map pipelines could be built (else documents with layer effects are
|
||||
/// [`Unsupported`] and use the CPU compositor).
|
||||
effect_maps: bool,
|
||||
}
|
||||
|
||||
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
||||
|
|
@ -380,6 +383,24 @@ struct Bound {
|
|||
patterns: Vec<Option<wgpu::TextureView>>,
|
||||
}
|
||||
|
||||
/// Runs `f` inside validation and internal-error scopes and returns the first error raised.
|
||||
/// Native wgpu reports errors synchronously; where they only arrive asynchronously (WebGPU) none
|
||||
/// is reported.
|
||||
fn first_error<T>(device: &wgpu::Device, f: impl FnOnce() -> T) -> (T, Option<wgpu::Error>) {
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let out = f();
|
||||
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
|
||||
let mut error = None;
|
||||
// Scopes pop in reverse order.
|
||||
for pop in [internal.pop(), validation.pop()] {
|
||||
if let std::task::Poll::Ready(Some(e)) = std::pin::pin!(pop).poll(&mut cx) {
|
||||
error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
(out, error)
|
||||
}
|
||||
|
||||
impl Compositor {
|
||||
/// Create a compositor with the default accumulation format ([`ACC_FORMAT`]).
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
|
|
@ -399,18 +420,10 @@ impl Compositor {
|
|||
/// to the CPU compositor. Where errors only arrive asynchronously (WebGPU), creation is assumed
|
||||
/// to have worked.
|
||||
pub fn try_new_with_format(device: &wgpu::Device, acc_format: wgpu::TextureFormat) -> Result<Self, Unsupported> {
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let comp = Self::new_with_format(device, acc_format);
|
||||
// Pop in reverse order; native wgpu reports errors synchronously, so the futures are ready.
|
||||
let errors = [internal.pop(), validation.pop()];
|
||||
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
|
||||
for f in errors {
|
||||
if let std::task::Poll::Ready(Some(e)) = std::pin::pin!(f).poll(&mut cx) {
|
||||
return Err(Unsupported(format!("couldn't build the compositor's pipelines: {e}")));
|
||||
}
|
||||
match first_error(device, || Self::new_with_format(device, acc_format)) {
|
||||
(comp, None) => Ok(comp),
|
||||
(_, Some(e)) => Err(Unsupported(format!("couldn't build the compositor's pipelines: {e}"))),
|
||||
}
|
||||
Ok(comp)
|
||||
}
|
||||
|
||||
/// Create a compositor whose accumulation/render-target format is `acc_format`. Pass
|
||||
|
|
@ -431,29 +444,34 @@ impl Compositor {
|
|||
bind_group_layouts: &[Some(&bgl0), Some(&bgl1)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let mut pipelines = HashMap::new();
|
||||
for k in Kernel::DRAWN {
|
||||
let pipeline = |k: Kernel, format: wgpu::TextureFormat| {
|
||||
let entry = k.entry().expect("drawn kernels have an entry point");
|
||||
let formats: &[wgpu::TextureFormat] = if k.is_map() { &[MAP32, MAP16] } else { &[acc_format] };
|
||||
for &format in formats {
|
||||
let p = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(entry),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(entry),
|
||||
targets: &[Some(wgpu::ColorTargetState { format, blend: None, write_mask: wgpu::ColorWrites::ALL })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
pipelines.insert((k, format), p);
|
||||
}
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(entry),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(entry),
|
||||
targets: &[Some(wgpu::ColorTargetState { format, blend: None, write_mask: wgpu::ColorWrites::ALL })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let mut pipelines: HashMap<_, _> = Kernel::DRAWN.iter().filter(|k| !k.is_map()).map(|&k| ((k, acc_format), pipeline(k, acc_format))).collect();
|
||||
// Effect maps render to R32Float / R16Float, which some adapters (e.g. GL without float
|
||||
// render targets) can't: then layer effects use the CPU compositor and the rest stays here.
|
||||
let maps = Kernel::DRAWN.iter().filter(|k| k.is_map()).flat_map(|&k| [(k, MAP32), (k, MAP16)]);
|
||||
let (maps, map_error) = first_error(device, || maps.map(|key| (key, pipeline(key.0, key.1))).collect::<Vec<_>>());
|
||||
if let Some(e) = &map_error {
|
||||
log::info!("GPU compositor: no effect-map pipelines ({e}); layer effects use the CPU");
|
||||
} else {
|
||||
pipelines.extend(maps);
|
||||
}
|
||||
let dummy = Tex::new(device, "pc_compose_dummy", 1, 1, wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureUsages::TEXTURE_BINDING).view;
|
||||
Self {
|
||||
|
|
@ -466,6 +484,7 @@ impl Compositor {
|
|||
max_dim: device.limits().max_texture_dimension_2d,
|
||||
frame: 0,
|
||||
acc_format,
|
||||
effect_maps: map_error.is_none(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -481,6 +500,9 @@ impl Compositor {
|
|||
|
||||
/// Effect regions and patterns must fit in textures.
|
||||
fn check_fx(&self, doc: &Document, p: &Plan<'_>) -> Result<(), Unsupported> {
|
||||
if !self.effect_maps && !p.fx.is_empty() {
|
||||
return Err(Unsupported("layer effects need float effect-map render targets, which this GPU lacks".into()));
|
||||
}
|
||||
for f in &p.fx {
|
||||
if f.region.width() > self.max_dim || f.region.height() > self.max_dim {
|
||||
return Err(Unsupported(format!("effect region of `{}` larger than the GPU texture limit ({})", f.layer.name, self.max_dim)));
|
||||
|
|
|
|||
|
|
@ -25,10 +25,29 @@ fn gpu() -> Option<Gpu> {
|
|||
}
|
||||
};
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
|
||||
let comp = Compositor::new(&device);
|
||||
// Exact parity needs 32-bit float targets. Adapters without them (e.g. GL software
|
||||
// rasterizers) use the CPU compositor in the app, so there is nothing to compare there.
|
||||
if Compositor::preferred_acc_format(&adapter) != wgpu::TextureFormat::Rgba32Float {
|
||||
eprintln!("skipping GPU parity tests: adapter can't render Rgba32Float");
|
||||
return None;
|
||||
}
|
||||
let comp = match Compositor::try_new_with_format(&device, wgpu::TextureFormat::Rgba32Float) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("skipping GPU parity tests: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(Gpu { device, queue, comp })
|
||||
}
|
||||
|
||||
/// Any adapter and device, for the fallback-path tests (which don't need 32-bit float targets).
|
||||
fn any_device() -> Option<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> {
|
||||
let adapter = pollster::block_on(wgpu::Instance::default().request_adapter(&wgpu::RequestAdapterOptions::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
|
||||
Some((adapter, device, queue))
|
||||
}
|
||||
|
||||
/// Deterministic pseudo-random values in 0..1.
|
||||
fn rnd(seed: u32, i: u32) -> f32 {
|
||||
let mut h = seed.wrapping_mul(0x9e37_79b9) ^ i.wrapping_mul(0x85eb_ca6b);
|
||||
|
|
@ -1092,14 +1111,11 @@ fn rgba16f_fallback_path_renders() {
|
|||
// Rgba16Float. Force that path here (even on a 32f-capable GPU) to prove it works end to end —
|
||||
// pipeline creation, the accumulation texture, and the half-float readback — within display
|
||||
// tolerance of the CPU reference. This is the path Intel-Vulkan / limited GPUs take.
|
||||
let instance = wgpu::Instance::default();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())) else {
|
||||
return;
|
||||
let Some((adapter, device, queue)) = any_device() else { return };
|
||||
let mut comp = match Compositor::try_new_with_format(&device, wgpu::TextureFormat::Rgba16Float) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return eprintln!("skipping: {e}"),
|
||||
};
|
||||
let Some((device, queue)) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok() else {
|
||||
return;
|
||||
};
|
||||
let mut comp = Compositor::new_with_format(&device, wgpu::TextureFormat::Rgba16Float);
|
||||
let mut d = base_doc(48, 32);
|
||||
let mut top = noise_layer("top", PixelFormat::RGBA8, Rect::from_xywh(0, 0, 48, 32), 7, 1.0);
|
||||
top.blend = BlendMode::Multiply;
|
||||
|
|
@ -1125,8 +1141,12 @@ fn rgba16f_fallback_path_renders() {
|
|||
fn unbuildable_pipelines_are_an_error_not_a_panic() {
|
||||
// The app falls back to the CPU compositor on Err; a panic here would crash it (as FXC once
|
||||
// did on D3D12). A depth format can't be a colour target, so its pipelines fail to build.
|
||||
let Some(g) = gpu() else { return };
|
||||
let e = Compositor::try_new_with_format(&g.device, wgpu::TextureFormat::Depth32Float).err().expect("depth target must fail");
|
||||
let Some((adapter, device, _)) = any_device() else { return };
|
||||
let e = Compositor::try_new_with_format(&device, wgpu::TextureFormat::Depth32Float).err().expect("depth target must fail");
|
||||
assert!(e.0.contains("pipelines"), "{e}");
|
||||
assert!(Compositor::try_new_with_format(&g.device, wgpu::TextureFormat::Rgba16Float).is_ok());
|
||||
// The format the app picks builds (effect maps the adapter can't render are left out, not fatal).
|
||||
let f = Compositor::preferred_acc_format(&adapter);
|
||||
if adapter.get_texture_format_features(f).allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) {
|
||||
assert!(Compositor::try_new_with_format(&device, f).is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ fn gpu() -> Option<(wgpu::Device, wgpu::Queue, photocraft_gpu::Compositor)> {
|
|||
let instance = wgpu::Instance::default();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
|
||||
let comp = photocraft_gpu::Compositor::new(&device);
|
||||
// Exact comparison needs 32-bit float targets (see parity.rs); skip on adapters without them.
|
||||
if photocraft_gpu::Compositor::preferred_acc_format(&adapter) != wgpu::TextureFormat::Rgba32Float {
|
||||
return None;
|
||||
}
|
||||
let comp = photocraft_gpu::Compositor::try_new_with_format(&device, wgpu::TextureFormat::Rgba32Float).ok()?;
|
||||
Some((device, queue, comp))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue