mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge branch 'fix/f6': community issue fixes
# Conflicts: # crates/gpu/src/tests.rs
This commit is contained in:
9 files changed
+375
-74
No files matched your search
+67
-28
@@ -18,14 +18,6 @@ const ENTRIES: &[&str] = &[
|
||||
"quantize",
|
||||
"convert",
|
||||
"half",
|
||||
"box_h",
|
||||
"box_v",
|
||||
"directional",
|
||||
"glow_bright",
|
||||
"glow_combine",
|
||||
"shadow_make",
|
||||
"shadow_combine",
|
||||
"pointwise",
|
||||
"adjust_mix",
|
||||
"bokeh_boost",
|
||||
"bokeh_prefix",
|
||||
@@ -33,6 +25,11 @@ const ENTRIES: &[&str] = &[
|
||||
"fill",
|
||||
];
|
||||
|
||||
/// Effect entry points in `kernels.wgsl`. Like the `fx_*` kernels, one that fails to build
|
||||
/// (a driver's shader compiler rejecting it, #613) leaves its effects to the CPU instead of
|
||||
/// turning the whole compositor off.
|
||||
const EFFECT_ENTRIES: &[&str] = &["box_h", "box_v", "directional", "glow_bright", "glow_combine", "shadow_make", "shadow_combine", "pointwise"];
|
||||
|
||||
/// Entry points that also bind group 1 (four read-only storage buffers; see
|
||||
/// [`Enc::dispatch_ext`]).
|
||||
const EXT_ENTRIES: &[&str] = &["classic3d"];
|
||||
@@ -244,6 +241,8 @@ pub struct GpuContext {
|
||||
bgl: wgpu::BindGroupLayout,
|
||||
bgl_ext: wgpu::BindGroupLayout,
|
||||
pipelines: HashMap<&'static str, wgpu::ComputePipeline>,
|
||||
/// Effect kernels that failed to build, and why (their effects render on the CPU).
|
||||
unavailable: Vec<(&'static str, String)>,
|
||||
display_bgl: wgpu::BindGroupLayout,
|
||||
display: wgpu::ComputePipeline,
|
||||
/// The display texture's next mip level from the one above (`display.wgsl` `mip`).
|
||||
@@ -450,7 +449,29 @@ impl GpuContext {
|
||||
immediate_size: 0,
|
||||
})
|
||||
})?;
|
||||
let pipelines = ENTRIES
|
||||
let build = |e: &'static str, layout: &wgpu::PipelineLayout| {
|
||||
init_resource(&device, e, || {
|
||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some(e),
|
||||
layout: Some(layout),
|
||||
module: &module,
|
||||
entry_point: Some(e),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
})
|
||||
})
|
||||
};
|
||||
// The compositor's own kernels are required; an effect kernel that doesn't build is
|
||||
// left out, and its effects render on the CPU (see `Enc::take_missing`).
|
||||
let mut pipelines = ENTRIES
|
||||
.iter()
|
||||
.chain(crate::adv3d::SKY_KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| Ok((*e, build(e, layout)?)))
|
||||
.collect::<Result<HashMap<_, _>, String>>()?;
|
||||
let mut unavailable = vec![];
|
||||
for e in EFFECT_ENTRIES
|
||||
.iter()
|
||||
.chain(crate::fx_color::KERNELS)
|
||||
.chain(crate::fx_distort::KERNELS)
|
||||
@@ -472,23 +493,17 @@ impl GpuContext {
|
||||
.chain(crate::fx_time::KERNELS)
|
||||
.chain(crate::fx_pixel2::KERNELS)
|
||||
.chain(crate::fx_gen2::KERNELS)
|
||||
.chain(crate::adv3d::SKY_KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
let p = init_resource(&device, e, || {
|
||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some(e),
|
||||
layout: Some(layout),
|
||||
module: &module,
|
||||
entry_point: Some(e),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
})
|
||||
})?;
|
||||
Ok((*e, p))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, String>>()?;
|
||||
{
|
||||
match build(e, &layout) {
|
||||
Ok(p) => {
|
||||
pipelines.insert(*e, p);
|
||||
}
|
||||
Err(why) => {
|
||||
log::warn!("gpu {name}: kernel {e} unavailable, its effects render on the CPU: {why}");
|
||||
unavailable.push((*e, why));
|
||||
}
|
||||
}
|
||||
}
|
||||
let dmodule = init_resource(&device, "display shader module", || {
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("effectcraft display"),
|
||||
@@ -547,6 +562,7 @@ impl GpuContext {
|
||||
bgl,
|
||||
bgl_ext,
|
||||
pipelines,
|
||||
unavailable,
|
||||
display_bgl,
|
||||
display,
|
||||
display_mip,
|
||||
@@ -721,6 +737,18 @@ impl GpuContext {
|
||||
|
||||
/// Readbacks (GPU → CPU) are possible: waited for natively, or deferred (a browser
|
||||
/// worker, [`crate::deferred`]). Impossible on the browser's main thread.
|
||||
/// Effect kernels the device couldn't build, with why: their effects render on the CPU.
|
||||
pub fn unavailable_kernels(&self) -> &[(&'static str, String)] {
|
||||
&self.unavailable
|
||||
}
|
||||
|
||||
/// Leave kernel `entry` out, as if it had failed to build (tests of the CPU fallback).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn drop_kernel(&mut self, entry: &'static str) {
|
||||
self.pipelines.remove(entry);
|
||||
self.unavailable.push((entry, "dropped by a test".into()));
|
||||
}
|
||||
|
||||
pub fn can_readback(&self) -> bool {
|
||||
self.readbacks.check().is_ok() && (self.can_wait() || self.deferred.is_some())
|
||||
}
|
||||
@@ -859,6 +887,8 @@ pub(crate) struct Enc<'g> {
|
||||
/// The open command encoder and its id in the texture pool.
|
||||
enc: Option<(wgpu::CommandEncoder, u64)>,
|
||||
pending: usize,
|
||||
/// A dispatch named a kernel the device doesn't have (see [`Enc::take_missing`]).
|
||||
missing: bool,
|
||||
}
|
||||
|
||||
impl Drop for Enc<'_> {
|
||||
@@ -874,7 +904,13 @@ impl Drop for Enc<'_> {
|
||||
|
||||
impl<'g> Enc<'g> {
|
||||
pub fn new(g: &'g GpuContext) -> Enc<'g> {
|
||||
Enc { g, enc: None, pending: 0 }
|
||||
Enc { g, enc: None, pending: 0, missing: false }
|
||||
}
|
||||
|
||||
/// Whether a dispatch since the last call named a kernel the device doesn't have (one
|
||||
/// that failed to build): its output is wrong, and the effect must render on the CPU.
|
||||
pub(crate) fn take_missing(&mut self) -> bool {
|
||||
std::mem::take(&mut self.missing)
|
||||
}
|
||||
|
||||
pub(crate) fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
|
||||
@@ -954,7 +990,10 @@ impl<'g> Enc<'g> {
|
||||
) {
|
||||
let g = self.g;
|
||||
let Some(pipe) = g.pipelines.get(entry) else {
|
||||
log::error!("gpu: no kernel {entry}");
|
||||
if !g.unavailable.iter().any(|(e, _)| *e == entry) {
|
||||
log::error!("gpu: no kernel {entry}");
|
||||
}
|
||||
self.missing = true;
|
||||
return;
|
||||
};
|
||||
let ub = g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: &p.bytes(), usage: wgpu::BufferUsages::UNIFORM });
|
||||
|
||||
@@ -66,8 +66,12 @@ pub(crate) fn run_chain(e: &mut Enc, chain: &[FxStep], buf: &Buf, levels: Option
|
||||
}
|
||||
let misses = deferred.as_ref().map(|d| d.misses());
|
||||
let mut b = GBuf { img: e.g.upload_image(&buf.img)?, offset: buf.offset, scale: buf.scale };
|
||||
e.take_missing();
|
||||
for step in chain {
|
||||
b = apply(e, step.spec.id, &step.ctx, b)?;
|
||||
if e.take_missing() {
|
||||
return None;
|
||||
}
|
||||
if let Some(l) = levels {
|
||||
b.img = ops::quantize(e, &b.img, l);
|
||||
}
|
||||
@@ -119,8 +123,13 @@ impl effectcraft_render::FxTarget for GpuFx<'_, '_> {
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool {
|
||||
let Some(b) = &self.b else { return false };
|
||||
let mut cur = GBuf { img: b.img.clone(), offset: b.offset, scale: b.scale };
|
||||
self.e.take_missing();
|
||||
for step in steps {
|
||||
let Some(next) = apply(self.e, step.spec.id, &step.ctx, cur) else { return false };
|
||||
// A kernel the device couldn't build: the effects render on the CPU (#613).
|
||||
if self.e.take_missing() {
|
||||
return false;
|
||||
}
|
||||
cur = next;
|
||||
if let Some(l) = self.levels {
|
||||
cur.img = ops::quantize(self.e, &cur.img, l);
|
||||
|
||||
@@ -785,3 +785,35 @@ fn nested_precomps_render_resident() {
|
||||
check(&format!("nested precomps half {depth:?}"), compare_at(&s, half, Tick::from_seconds_f64(0.2)), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// #613: an effect kernel the device's shader compiler rejects (FXC on DirectX 12 aborting on
|
||||
/// `pointwise`) no longer turns the whole GPU compositor off: the context is built without it,
|
||||
/// lists it, and the effects that need it render on the CPU, matching the CPU render.
|
||||
#[test]
|
||||
fn effects_whose_kernels_failed_render_on_the_cpu() {
|
||||
if gpu().is_none() {
|
||||
return;
|
||||
}
|
||||
let Ok(mut ctx) = pollster::block_on(crate::context::GpuContext::request()) else { return };
|
||||
ctx.drop_kernel("pointwise");
|
||||
ctx.drop_kernel("box_h");
|
||||
let names: Vec<&str> = ctx.unavailable_kernels().iter().map(|(e, _)| *e).collect();
|
||||
assert_eq!(names, ["pointwise", "box_h"]);
|
||||
let g = Gpu::from_context(ctx);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let mut l = s.footage(80, 50);
|
||||
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
|
||||
s.effect(&mut l, "ec.color.tint", &[("black", c(0.1, 0.0, 0.3)), ("amount", n(80.0))]);
|
||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
|
||||
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
|
||||
s.push(l);
|
||||
let cpu = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Cpu, ..opts() }).comp_frame_cpu(s.cid, Tick::ZERO);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(&g);
|
||||
let img = g.render(&r, s.cid, Tick::ZERO).expect("the GPU renders the frame");
|
||||
let mut d = diff(&cpu, &img, tolerance(depth));
|
||||
d.quantized = depth != BitDepth::Bpc32;
|
||||
check(&format!("missing kernels {depth:?}"), Some(d), 0.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user