GPU: build effect kernels on first use, each family from a module of its own (fixes #1590) (#1705)

* #1590: build effect kernels on first use instead of with the GPU context

GpuContext::new built every compute pipeline up front: the compositor's own
kernels and ~208 effect kernels. In Chrome on Windows that kept the GPU
process busy past its watchdog, so the device was lost ~60 s after every load
(#1590); on DirectX 12 with FXC the window took over a minute to appear (#613).

The compositor's kernels are still built up front and required. Effect kernels
are built the first time a dispatch asks for one, inside the same error scopes,
so one a compiler rejects still renders its effects on the CPU. unavailable_kernels
lists the failures so far; build_all_kernels builds the rest.

RX 6900 XT, Windows: Vulkan context 0.27 s, all effect kernels 0.9 s, GPU tests
77 s -> 26 s; on #1571's DX12/FXC, context ~66 s -> 1.96 s, tests 1306 s -> 252 s,
same results.

Co-Authored-By: Claude <noreply@anthropic.com>

* #1590: compile each kernel family as a shader module of its own

Building effect kernels on first use was not enough in Chrome: the page and
its frame workers each create a device, every one compiled its required
kernels against the single ~460K-character module, and Dawn compiles the
module again for each pipeline, so the GPU process still passed its watchdog
and the device was lost ~64 s after load (RX 6900 XT, Chrome 155).

Each kernel family (FAMILIES) is now common.wgsl + kernels.wgsl + its own file
and the files it borrows functions from (at most ~120K characters), built with
the family's first kernel; the compositor's kernels, sky and classic3d use
small modules of their own.

Chrome 155 on Windows / RX 6900 XT, release web build, default flags: main
loses the device at ~64 s (60.7 s with ?nogpuworkers); with this, two 100 s
runs keep it, and both frame workers get the GPU. Native Vulkan tests
unchanged (212 passed, the 3 failures that main has, #1605).

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-04 00:28:51 +01:00
1 parent ff68626597
commit 025c038af5
4 files changed
+223 -105

No files matched your search

+201 -101
View File
@@ -24,9 +24,10 @@ 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.
/// Effect entry points in `kernels.wgsl`. Like the `fx_*` kernels, they are built on first use
/// (building all ~200 up front kept Chrome's GPU process busy past its watchdog, #1590, and
/// FXC took over a minute, #613), and one that fails to build (a driver's shader compiler
/// rejecting it) 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
@@ -34,6 +35,79 @@ const EFFECT_ENTRIES: &[&str] = &["box_h", "box_v", "directional", "glow_bright"
/// can't unroll `classic3d`'s loops, #613) leaves its work to the CPU.
const EXT_ENTRIES: &[&str] = &["classic3d"];
/// What every kernel module starts with: the shared helpers and bindings, and `kernels.wgsl`
/// (the compositor's kernels, which declare `out` and helpers the effect files use).
const BASE_WGSL: [&str; 2] = [include_str!("shaders/common.wgsl"), include_str!("shaders/kernels.wgsl")];
/// The family whose module also gets the adapter's median implementation
/// ([`crate::fx_noise::MedianKernel::source`]), chosen when the context is created.
const MEDIAN_FAMILY: &str = "fx_noise";
/// Kernel families: each is compiled as a module of its own, `BASE_WGSL` plus its file and the
/// files it borrows functions from, built the first time one of its kernels is. A pipeline then
/// compiles at most ~120K characters of WGSL instead of all ~460K: on WebGPU each pipeline
/// compiles its module again, and the page's and its workers' devices doing that for every
/// kernel kept Chrome's GPU process busy past its watchdog (#1590). Family 0 is the base module.
const FAMILIES: &[(&str, &[&str], &[&str])] = &[
("kernels", &[], EFFECT_ENTRIES),
("fx_color", &[include_str!("shaders/fx_lut.wgsl"), include_str!("shaders/fx_color.wgsl")], crate::fx_color::KERNELS),
("fx_distort", &[include_str!("shaders/fx_distort.wgsl")], crate::fx_distort::KERNELS),
("fx_generate", &[include_str!("shaders/fx_generate.wgsl")], crate::fx_generate::KERNELS),
("fx_key", &[include_str!("shaders/fx_distort.wgsl"), include_str!("shaders/fx_key.wgsl")], crate::fx_key::KERNELS),
("fx_stylize", &[include_str!("shaders/fx_stylize.wgsl")], crate::fx_stylize::KERNELS),
(MEDIAN_FAMILY, &[include_str!("shaders/fx_noise.wgsl")], crate::fx_noise::KERNELS),
("fx_tone", &[include_str!("shaders/fx_tone.wgsl")], crate::fx_tone::KERNELS),
("fx_warp", &[include_str!("shaders/fx_warp.wgsl")], crate::fx_warp::KERNELS),
("fx_extra", &[include_str!("shaders/fx_generate.wgsl"), include_str!("shaders/fx_extra.wgsl")], crate::fx_extra::KERNELS),
("fx_depth", &[include_str!("shaders/fx_depth.wgsl")], crate::fx_depth::KERNELS),
("fx_lut", &[include_str!("shaders/fx_lut.wgsl")], crate::fx_lut::KERNELS),
("fx_sim", &[include_str!("shaders/fx_sim.wgsl")], crate::fx_sim::KERNELS),
(
"fx_particles",
&[include_str!("shaders/fx_stylize.wgsl"), include_str!("shaders/fx_sim.wgsl"), include_str!("shaders/fx_particles.wgsl")],
crate::fx_particles::KERNELS,
),
("fx_vr", &[include_str!("shaders/fx_vr.wgsl")], crate::fx_vr::KERNELS),
("fx_light", &[include_str!("shaders/fx_light.wgsl")], crate::fx_light::KERNELS),
("fx_transition", &[include_str!("shaders/fx_transition.wgsl")], crate::fx_transition::KERNELS),
("fx_text", &[include_str!("shaders/fx_text.wgsl")], crate::fx_text::KERNELS),
("fx_time", &[include_str!("shaders/fx_time.wgsl")], crate::fx_time::KERNELS),
("fx_pixel2", &[include_str!("shaders/fx_stylize.wgsl"), include_str!("shaders/fx_pixel2.wgsl")], crate::fx_pixel2::KERNELS),
("fx_gen2", &[include_str!("shaders/fx_gen2.wgsl")], crate::fx_gen2::KERNELS),
("inpaint", &[include_str!("shaders/inpaint.wgsl")], crate::inpaint::KERNELS),
];
/// Module `label`: `BASE_WGSL` followed by `files`.
fn kernel_module(device: &wgpu::Device, label: &str, files: &[&str]) -> Result<wgpu::ShaderModule, String> {
let src: String = BASE_WGSL.iter().chain(files).copied().collect();
init_resource(device, &format!("{label} shader module"), || {
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some(label), source: wgpu::ShaderSource::Wgsl(src.into()) })
})
}
/// Compute pipeline `entry` of `module`; a shader compiler rejecting it (FXC, #613) is an `Err`.
fn build_pipeline(
device: &wgpu::Device,
module: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
entry: &'static str,
) -> Result<wgpu::ComputePipeline, String> {
init_resource(device, entry, || {
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: Some(layout),
module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
})
})
}
/// Effect kernels by entry point: their family (index in `FAMILIES`) and, once asked for, the
/// built pipeline or why it didn't build.
type EffectPipelines = HashMap<&'static str, (usize, std::sync::OnceLock<Result<wgpu::ComputePipeline, String>>)>;
/// Pixel format of every working texture: premultiplied RGBA, 32-bit float (32 bpc headroom;
/// 8/16 bpc are emulated by clamping and quantising, as on the CPU).
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
@@ -601,9 +675,16 @@ pub struct GpuContext {
pub(crate) median_kernel: crate::fx_noise::MedianKernel,
bgl: wgpu::BindGroupLayout,
bgl_ext: wgpu::BindGroupLayout,
/// Each of `FAMILIES`' modules, built with its first kernel; and their group 0 layout.
modules: Vec<std::sync::OnceLock<Result<wgpu::ShaderModule, String>>>,
layout: wgpu::PipelineLayout,
/// The compositor's own kernels and Classic 3D (built up front).
pipelines: HashMap<&'static str, wgpu::ComputePipeline>,
/// Effect kernels that failed to build, and why (their effects render on the CPU).
/// Kernels built up front that failed to build, and why (Classic 3D renders on the CPU).
unavailable: Vec<(&'static str, String)>,
/// Effect kernels, built on first dispatch (`GpuContext::pipeline`); one that doesn't build
/// leaves its effects to the CPU.
effect_pipelines: EffectPipelines,
display_bgl: wgpu::BindGroupLayout,
display: wgpu::ComputePipeline,
/// The display texture's next mip level from the one above (`display.wgsl` `mip`).
@@ -778,40 +859,7 @@ impl GpuContext {
if let Some(why) = &adv3d_unsupported {
log::info!("gpu {name}: Advanced 3D renders on the CPU ({why})");
}
let src = [
include_str!("shaders/common.wgsl"),
include_str!("shaders/kernels.wgsl"),
include_str!("shaders/classic3d.wgsl"),
include_str!("shaders/fx_color.wgsl"),
include_str!("shaders/fx_distort.wgsl"),
include_str!("shaders/fx_generate.wgsl"),
include_str!("shaders/fx_key.wgsl"),
include_str!("shaders/fx_stylize.wgsl"),
include_str!("shaders/fx_noise.wgsl"),
median_kernel.source(),
include_str!("shaders/fx_tone.wgsl"),
include_str!("shaders/fx_warp.wgsl"),
include_str!("shaders/fx_extra.wgsl"),
include_str!("shaders/fx_depth.wgsl"),
include_str!("shaders/fx_lut.wgsl"),
include_str!("shaders/fx_sim.wgsl"),
include_str!("shaders/fx_particles.wgsl"),
include_str!("shaders/fx_vr.wgsl"),
include_str!("shaders/fx_light.wgsl"),
include_str!("shaders/fx_transition.wgsl"),
include_str!("shaders/fx_text.wgsl"),
include_str!("shaders/fx_time.wgsl"),
include_str!("shaders/fx_pixel2.wgsl"),
include_str!("shaders/fx_gen2.wgsl"),
include_str!("shaders/inpaint.wgsl"),
include_str!("shaders/sky.wgsl"),
]
.concat();
#[cfg(test)]
let src = src + crate::tests::MATH_PROBE_SHADER;
let module = init_resource(&device, "kernels shader module", || {
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("effectcraft kernels"), source: wgpu::ShaderSource::Wgsl(src.into()) })
})?;
let module = kernel_module(&device, "effectcraft kernels", &[])?;
let bgl = init_resource(&device, "kernels bind-group layout", || {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("effectcraft kernels"),
@@ -864,25 +912,21 @@ impl GpuContext {
immediate_size: 0,
})
})?;
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`). So is a
// missing Classic 3D kernel: its 3D runs render on the CPU.
let mut pipelines = ENTRIES.iter().chain(crate::adv3d::SKY_KERNELS).map(|e| Ok((*e, build(e, &layout)?))).collect::<Result<HashMap<_, _>, String>>()?;
// The compositor's own kernels are required and built now. A Classic 3D kernel that
// doesn't build is left out (its 3D runs render on the CPU), and effect kernels are built
// on first use (`GpuContext::pipeline`); one that doesn't build leaves its effects to the
// CPU (see `Enc::take_missing`).
let sky = kernel_module(&device, "effectcraft sky", &[include_str!("shaders/sky.wgsl")])?;
let mut pipelines = ENTRIES
.iter()
.map(|e| (e, &module))
.chain(crate::adv3d::SKY_KERNELS.iter().map(|e| (e, &sky)))
.map(|(e, module)| Ok((*e, build_pipeline(&device, module, &layout, e)?)))
.collect::<Result<HashMap<_, _>, String>>()?;
let mut unavailable = vec![];
let classic3d = kernel_module(&device, "effectcraft classic3d", &[include_str!("shaders/classic3d.wgsl")]);
for e in EXT_ENTRIES {
match build(e, &layout_ext) {
match classic3d.as_ref().map_err(Clone::clone).and_then(|m| build_pipeline(&device, m, &layout_ext, e)) {
Ok(p) => {
pipelines.insert(*e, p);
}
@@ -893,41 +937,14 @@ impl GpuContext {
}
}
#[cfg(test)]
pipelines.insert("math_probe", build("math_probe", &layout)?);
for e in EFFECT_ENTRIES
.iter()
.chain(crate::fx_color::KERNELS)
.chain(crate::fx_distort::KERNELS)
.chain(crate::fx_generate::KERNELS)
.chain(crate::fx_key::KERNELS)
.chain(crate::fx_stylize::KERNELS)
.chain(crate::fx_noise::KERNELS)
.chain(crate::fx_tone::KERNELS)
.chain(crate::fx_warp::KERNELS)
.chain(crate::fx_extra::KERNELS)
.chain(crate::fx_depth::KERNELS)
.chain(crate::fx_lut::KERNELS)
.chain(crate::fx_sim::KERNELS)
.chain(crate::fx_particles::KERNELS)
.chain(crate::fx_vr::KERNELS)
.chain(crate::fx_light::KERNELS)
.chain(crate::fx_transition::KERNELS)
.chain(crate::fx_text::KERNELS)
.chain(crate::fx_time::KERNELS)
.chain(crate::fx_pixel2::KERNELS)
.chain(crate::fx_gen2::KERNELS)
.chain(crate::inpaint::KERNELS)
{
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));
}
}
// The numerical probe (tests) calls `fx_stylize.wgsl` helpers.
let probe = kernel_module(&device, "effectcraft math probe", &[include_str!("shaders/fx_stylize.wgsl"), crate::tests::MATH_PROBE_SHADER])?;
pipelines.insert("math_probe", build_pipeline(&device, &probe, &layout, "math_probe")?);
}
let effect_pipelines =
FAMILIES.iter().enumerate().flat_map(|(family, (_, _, kernels))| kernels.iter().map(move |e| (*e, (family, std::sync::OnceLock::new())))).collect();
let modules = std::iter::once(std::sync::OnceLock::from(Ok(module))).chain(FAMILIES.iter().skip(1).map(|_| std::sync::OnceLock::new())).collect();
let dmodule = init_resource(&device, "display shader module", || {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effectcraft display"),
@@ -988,8 +1005,11 @@ impl GpuContext {
median_kernel,
bgl,
bgl_ext,
modules,
layout,
pipelines,
unavailable,
effect_pipelines,
display_bgl,
display,
display_mip,
@@ -1300,21 +1320,101 @@ 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
/// Kernels the device couldn't build so far, with why: their effects render on the CPU.
/// Effect kernels are built on first use, so one not used yet isn't listed.
pub fn unavailable_kernels(&self) -> Vec<(&'static str, String)> {
let mut out: Vec<_> = self
.unavailable
.iter()
.cloned()
.chain(self.effect_pipelines.iter().filter_map(|(e, (_, cell))| match cell.get() {
Some(Err(why)) => Some((*e, why.clone())),
_ => None,
}))
.collect();
out.sort_unstable_by_key(|(e, _)| *e);
out
}
/// Whether kernel `entry` was built (an effect or Classic 3D kernel can be missing, #613).
/// Whether kernel `entry` is available (an effect or Classic 3D kernel can be missing,
/// #613); an effect kernel not built yet is built now.
pub(crate) fn has_kernel(&self, entry: &str) -> bool {
self.pipelines.contains_key(entry)
self.pipeline(entry).is_some()
}
/// Kernel `entry`'s pipeline: one built up front, or an effect kernel, built the first time
/// it is asked for. `None` when the device doesn't have it.
fn pipeline(&self, entry: &str) -> Option<&wgpu::ComputePipeline> {
if let Some(p) = self.pipelines.get(entry) {
return Some(p);
}
let Some((e, (family, cell))) = self.effect_pipelines.get_key_value(entry) else {
if !self.unavailable.iter().any(|(e, _)| *e == entry) {
log::error!("gpu: no kernel {entry}");
}
return None;
};
let built = cell.get_or_init(|| {
let built = self.family_module(*family).and_then(|module| build_pipeline(&self.device, module, &self.layout, e));
if let Err(why) = &built {
log::warn!("gpu {}: kernel {e} unavailable, its effects render on the CPU: {why}", self.name);
}
built
});
built.as_ref().ok()
}
/// Family `family`'s module (`FAMILIES`), built the first time it is asked for.
fn family_module(&self, family: usize) -> Result<&wgpu::ShaderModule, String> {
let (Some((label, files, _)), Some(cell)) = (FAMILIES.get(family), self.modules.get(family)) else {
return Err(format!("no kernel family {family}"));
};
cell.get_or_init(|| {
let median = [self.median_kernel.source()];
let extra: &[&str] = if *label == MEDIAN_FAMILY { &median } else { &[] };
let files: Vec<&str> = files.iter().chain(extra).copied().collect();
kernel_module(&self.device, label, &files)
})
.as_ref()
.map_err(Clone::clone)
}
/// Build every effect kernel not built yet (tests; and a host that would rather pay the
/// compile time up front than on first use).
pub fn build_all_kernels(&self) {
for e in self.effect_pipelines.keys() {
let _ = self.pipeline(e);
}
}
/// How many effect kernels have been built (tests).
#[cfg(test)]
pub(crate) fn effect_pipelines_built(&self) -> usize {
self.effect_pipelines.values().filter(|(_, cell)| matches!(cell.get(), Some(Ok(_)))).count()
}
/// Leave kernel `entry` out of `pipelines`/`effect_pipelines`, as if it had failed to build.
#[cfg(test)]
fn drop_from(
pipelines: &mut HashMap<&'static str, wgpu::ComputePipeline>,
unavailable: &mut Vec<(&'static str, String)>,
effects: &mut EffectPipelines,
entry: &'static str,
) {
let why = "dropped by a test".to_string();
pipelines.remove(entry);
match effects.get(entry).map(|(f, _)| *f) {
Some(family) => {
effects.insert(entry, (family, std::sync::OnceLock::from(Err(why))));
}
None => unavailable.push((entry, why)),
}
}
/// 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()));
Self::drop_from(&mut self.pipelines, &mut self.unavailable, &mut self.effect_pipelines, entry);
}
/// A copy of this context sharing the same [`wgpu::Device`] and [`wgpu::Queue`] with
@@ -1323,9 +1423,9 @@ impl GpuContext {
#[cfg(test)]
pub(crate) fn without_kernel(&self, entry: &'static str) -> GpuContext {
let mut pipelines = self.pipelines.clone();
pipelines.remove(entry);
let mut unavailable = self.unavailable.clone();
unavailable.push((entry, "dropped by a test".into()));
let mut effect_pipelines = self.effect_pipelines.clone();
Self::drop_from(&mut pipelines, &mut unavailable, &mut effect_pipelines, entry);
GpuContext {
device: self.device.clone(),
queue: self.queue.clone(),
@@ -1334,8 +1434,11 @@ impl GpuContext {
median_kernel: self.median_kernel,
bgl: self.bgl.clone(),
bgl_ext: self.bgl_ext.clone(),
modules: self.modules.clone(),
layout: self.layout.clone(),
pipelines,
unavailable,
effect_pipelines,
display_bgl: self.display_bgl.clone(),
display: self.display.clone(),
display_mip: self.display_mip.clone(),
@@ -1766,10 +1869,7 @@ impl<'g> Enc<'g> {
) {
let KernelImages { src, aux, out } = images;
let g = self.g;
let Some(pipe) = g.pipelines.get(entry) else {
if !g.unavailable.iter().any(|(e, _)| *e == entry) {
log::error!("gpu: no kernel {entry}");
}
let Some(pipe) = g.pipeline(entry) else {
self.missing = true;
return;
};
+1 -1
View File
@@ -422,7 +422,7 @@ impl Accelerator for Gpu {
// Readback is synchronous-only, and a kernel that failed to build would make every stage
// fall back to the CPU *after* paying the upload and a failed dispatch (and the fill
// would still report the GPU path). Neither is a backend at all.
if !self.ctx.can_wait() || self.ctx.unavailable_kernels().iter().any(|(e, _)| crate::inpaint::KERNELS.contains(e)) {
if !self.ctx.can_wait() || !crate::inpaint::KERNELS.iter().all(|e| self.ctx.has_kernel(e)) {
return None;
}
Some(self as &dyn effectcraft_raster::inpaint::FillBackend)
+20 -2
View File
@@ -137,7 +137,7 @@ fn numerical_effects_remain_resident_on_vulkan() {
return;
}
for entry in ["fxs_point", "fxt_point", "ftr_grid", "fp2_hextile"] {
assert!(!g.ctx.unavailable_kernels().iter().any(|(name, _)| *name == entry), "{entry}: {:?}", g.ctx.unavailable_kernels());
assert!(g.ctx.has_kernel(entry), "{entry}: {:?}", g.ctx.unavailable_kernels());
}
for (id, vals) in [
("ec.stylize.posterize", vec![("level", n(4.0))]),
@@ -1107,7 +1107,7 @@ fn effects_whose_kernels_failed_render_on_the_cpu() {
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"]);
assert_eq!(names, ["box_h", "pointwise"]);
let g = Gpu::from_context(ctx);
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = Scene::new(depth);
@@ -1127,6 +1127,24 @@ fn effects_whose_kernels_failed_render_on_the_cpu() {
}
}
/// #1590: effect kernels are built on first use, not with the context (building all of them
/// up front kept Chrome's GPU process busy past its watchdog). Once asked for, every one of
/// them builds on this device: a kernel no test dispatches still can't break unnoticed.
#[test]
fn effect_kernels_build_on_first_use() {
if gpu().is_none() {
return;
}
let Ok(ctx) = pollster::block_on(crate::context::GpuContext::request()) else { return };
let built = |ctx: &crate::context::GpuContext| ctx.effect_pipelines_built();
assert_eq!(built(&ctx), 0, "no effect kernel is built with the context");
let started = std::time::Instant::now();
ctx.build_all_kernels();
eprintln!("{}: built {} effect kernels in {:.2?}", ctx.name, built(&ctx), started.elapsed());
assert_eq!(ctx.unavailable_kernels(), vec![], "every effect kernel builds on {}", ctx.name);
assert!(ctx.has_kernel("classic3d") && ctx.has_kernel("math_probe"), "the up-front kernels build on {}", ctx.name);
}
/// WGSL cannot read the CPU's constants, so Glow's bright pass repeats them as literals; this
/// ties them to `effects::GLOW_GAIN` / `GLOW_KNEE` (runs without a GPU).
#[test]
+1 -1
View File
@@ -44,7 +44,7 @@ fn rect_hole(w: u32, h: u32, x0: u32, y0: u32, s: u32) -> Vec<bool> {
#[test]
fn the_inpaint_kernels_are_available() {
let Some(g) = gpu() else { return };
let bad: Vec<&str> = g.ctx.unavailable_kernels().iter().filter(|(e, _)| crate::inpaint::KERNELS.contains(e)).map(|(e, _)| *e).collect();
let bad: Vec<&str> = crate::inpaint::KERNELS.iter().copied().filter(|e| !g.ctx.has_kernel(e)).collect();
assert!(bad.is_empty(), "inpaint kernels did not build on {}: {bad:?}", g.ctx.name);
}