use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError, Weak};
use std::task::{Context, Poll, Waker};
use super::Shared;
use super::shaders::{Binding, MARK_GROUP, PATH_ENDS, Shader, VIEW_GROUP};
use super::work::{Count, Counters};
use crate::Error;
use crate::view::DEPTH;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub(crate) enum Urgency {
Draw,
Text,
Answer,
Warm,
}
const WORKERS: u64 = 8;
#[cfg(not(target_arch = "wasm32"))]
type Check = Pin<Box<dyn Future<Output = Option<wgpu::Error>> + Send>>;
#[cfg(target_arch = "wasm32")]
type Check = Pin<Box<dyn Future<Output = Option<wgpu::Error>>>>;
struct Custom {
pipeline: wgpu::RenderPipeline,
check: Option<Check>,
failed: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct Variant {
shader: Shader,
entry: &'static str,
primitive: u32,
path: u32,
reads: Reads,
target: Option<(wgpu::TextureFormat, u32)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct Reads {
pub(crate) joined: bool,
pub(crate) timed: bool,
pub(crate) viewed: bool,
}
impl Reads {
pub(crate) const ALL: Reads = Reads { joined: true, timed: true, viewed: true };
}
impl Variant {
pub(crate) fn draw(shader: Shader, format: wgpu::TextureFormat, samples: u32) -> Variant {
Variant {
shader,
entry: "vertex",
primitive: 0,
path: PATH_ENDS,
reads: Reads::ALL,
target: Some((format, samples)),
}
}
pub(crate) fn compute(shader: Shader, entry: &'static str) -> Variant {
Variant { shader, entry, primitive: 0, path: PATH_ENDS, reads: Reads::ALL, target: None }
}
pub(crate) fn probe(shader: Shader, primitive: u32) -> Variant {
Variant::compute(shader, "answer").primitive(primitive)
}
pub(crate) fn channels(reads: Reads) -> Variant {
Variant { reads, ..Variant::compute(Shader::Evaluate, "evaluate") }
}
#[must_use]
pub(crate) fn primitive(mut self, primitive: u32) -> Variant {
if self.reads_constant("PRIMITIVE") {
self.primitive = primitive;
}
self
}
#[must_use]
pub(crate) fn path(mut self, path: u32) -> Variant {
if self.reads_constant("PATH") {
self.path = path;
}
self
}
fn reads_constant(&self, name: &str) -> bool {
self.shader.source().overrides.contains(&name)
}
fn constants(&self) -> Vec<(&'static str, f64)> {
let value = |name: &str| match name {
"PRIMITIVE" => f64::from(self.primitive),
"PATH" => f64::from(self.path),
"JOINED" => f64::from(u8::from(self.reads.joined)),
"TIMED" => f64::from(u8::from(self.reads.timed)),
"VIEWED" => f64::from(u8::from(self.reads.viewed)),
other => unreachable!("no value for the pipeline constant {other}"),
};
self.shader.source().overrides.iter().map(|&name| (name, value(name))).collect()
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) enum Asked {
Shader(Variant),
Custom(Arc<str>, Variant),
}
impl Asked {
pub(crate) fn size(&self) -> usize {
match self {
Asked::Shader(variant) => variant.shader.source().size(),
Asked::Custom(source, variant) => variant.shader.source().size() + source.len(),
}
}
}
#[derive(Default)]
struct Ahead {
compiled: AtomicBool,
landed: AtomicBool,
waiting: Mutex<Vec<Waker>>,
}
impl Ahead {
fn landed(&self) -> bool {
self.landed.load(Ordering::Acquire)
}
fn wait(&self, waker: &Waker) {
self.waiting.lock().unwrap_or_else(PoisonError::into_inner).push(waker.clone());
}
fn land(&self, landed: &AtomicU64) {
self.landed.store(true, Ordering::Release);
landed.fetch_add(1, Ordering::AcqRel);
for waker in self.waiting.lock().unwrap_or_else(PoisonError::into_inner).drain(..) {
waker.wake();
}
}
}
pub(crate) struct Pipelines {
pub(crate) view_layout: wgpu::BindGroupLayout,
pub(crate) mark_layout: wgpu::BindGroupLayout,
pub(crate) empty: wgpu::BindGroup,
empty_layout: wgpu::BindGroupLayout,
pass_layouts: HashMap<Vec<wgpu::BindGroupLayoutEntry>, wgpu::BindGroupLayout>,
modules: HashMap<Shader, wgpu::ShaderModule>,
render: HashMap<Variant, wgpu::RenderPipeline>,
compute: HashMap<Variant, wgpu::ComputePipeline>,
custom: HashMap<(Arc<str>, Variant), Custom>,
asked: HashMap<Asked, Arc<Ahead>>,
queue: Vec<(Urgency, Asked)>,
landed: Arc<AtomicU64>,
#[cfg(all(feature = "web", target_arch = "wasm32"))]
compiling: Arc<AtomicU64>,
#[cfg(all(feature = "web", target_arch = "wasm32"))]
drawing: Arc<AtomicU64>,
drew: bool,
warming: bool,
#[cfg(all(feature = "web", target_arch = "wasm32"))]
warmed: bool,
counters: Arc<Counters>,
#[cfg_attr(
not(all(target_arch = "wasm32", feature = "web")),
expect(dead_code, reason = "only a browser compiles ahead")
)]
gpu: Weak<Shared>,
}
const CASTERS: [Shader; 5] =
[Shader::PointsCast, Shader::StrokesCast, Shader::TrianglesCast, Shader::Shade, Shader::PointsCustomCast];
pub(crate) fn probe(samples: u32) -> Shader {
if samples > 1 { Shader::ProbeMultisampled } else { Shader::Probe }
}
fn topology(shader: Shader) -> wgpu::PrimitiveTopology {
match shader {
Shader::Backdrop | Shader::Triangles | Shader::TrianglesCast | Shader::Shade => {
wgpu::PrimitiveTopology::TriangleList
}
_ => wgpu::PrimitiveTopology::TriangleStrip,
}
}
fn casts(shader: Shader) -> bool {
CASTERS.contains(&shader)
}
fn depth(shader: Shader) -> Option<wgpu::DepthStencilState> {
if casts(shader) {
return None;
}
let compare = match shader {
Shader::Backdrop | Shader::Overlay | Shader::Glyphs => wgpu::CompareFunction::Always,
_ => wgpu::CompareFunction::GreaterEqual,
};
Some(wgpu::DepthStencilState {
format: DEPTH,
depth_write_enabled: Some(matches!(shader, Shader::Triangles | Shader::Backdrop)),
depth_compare: Some(compare),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
})
}
fn blend(shader: Shader) -> Option<wgpu::BlendState> {
let component = |src, dst, operation| wgpu::BlendComponent { src_factor: src, dst_factor: dst, operation };
match shader {
Shader::Shade => None,
Shader::PointsCast | Shader::StrokesCast | Shader::TrianglesCast | Shader::PointsCustomCast => {
let least = component(wgpu::BlendFactor::One, wgpu::BlendFactor::One, wgpu::BlendOperation::Min);
Some(wgpu::BlendState { color: least, alpha: least })
}
_ => {
let over =
component(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrcAlpha, wgpu::BlendOperation::Add);
Some(wgpu::BlendState { color: over, alpha: over })
}
}
}
fn layout(device: &wgpu::Device, label: &str, bindings: &[Binding]) -> wgpu::BindGroupLayout {
let entries: Vec<_> = bindings.iter().map(Binding::layout_entry).collect();
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some(label), entries: &entries })
}
impl Pipelines {
pub(crate) fn new(device: &wgpu::Device, counters: Arc<Counters>, gpu: Weak<Shared>) -> Pipelines {
let empty_layout = layout(device, "latkit empty", &[]);
let empty = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("latkit empty"),
layout: &empty_layout,
entries: &[],
});
Pipelines {
view_layout: layout(device, "latkit view", VIEW_GROUP),
mark_layout: layout(device, "latkit mark", MARK_GROUP),
empty,
empty_layout,
pass_layouts: HashMap::new(),
modules: HashMap::new(),
render: HashMap::new(),
compute: HashMap::new(),
custom: HashMap::new(),
asked: HashMap::new(),
queue: Vec::new(),
landed: Arc::default(),
#[cfg(all(feature = "web", target_arch = "wasm32"))]
compiling: Arc::default(),
#[cfg(all(feature = "web", target_arch = "wasm32"))]
drawing: Arc::default(),
drew: false,
warming: false,
#[cfg(all(feature = "web", target_arch = "wasm32"))]
warmed: false,
counters,
gpu,
}
}
pub(crate) fn ask(&mut self, device: &wgpu::Device, needs: &[Asked], urgency: Urgency, waker: &Waker) {
self.queue(device, needs, urgency, waker);
self.pump(device);
}
pub(crate) fn queue(&mut self, device: &wgpu::Device, needs: &[Asked], urgency: Urgency, waker: &Waker) {
for need in needs {
if let Some(ahead) = self.asked.get(need) {
if !ahead.landed() {
ahead.wait(waker);
}
if let Some(queued) = self.queue.iter_mut().find(|(_, queued)| queued == need) {
queued.0 = queued.0.min(urgency);
}
continue;
}
let ahead = Arc::new(Ahead::default());
ahead.wait(waker);
self.asked.insert(need.clone(), ahead);
if let Asked::Shader(variant) = need {
self.module(device, variant.shader);
}
self.queue.push((urgency, need.clone()));
}
crate::order::by_key(&mut self.queue, |(urgency, need)| {
let size = if *urgency == Urgency::Warm { 0 } else { !(need.size().min(u32::MAX as usize) as u32) };
crate::order::pair(*urgency as u32, size)
});
}
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn warm(&mut self, device: &wgpu::Device, needs: impl FnOnce() -> Vec<Asked>) {
if !std::mem::replace(&mut self.warmed, true) {
self.queue(device, &needs(), Urgency::Warm, Waker::noop());
}
}
pub(crate) fn pump(&mut self, device: &wgpu::Device) {
self.warming |= self.drew && self.drawing() == 0;
while let Some(&(urgency, _)) = self.queue.first()
&& match urgency {
Urgency::Draw => true,
Urgency::Text => self.drawing() == 0 || self.in_flight() < WORKERS,
Urgency::Answer => self.in_flight() < WORKERS / 2,
Urgency::Warm => self.warming && self.in_flight() < WORKERS / 2,
}
{
let (urgency, need) = self.queue.remove(0);
self.drew |= urgency == Urgency::Draw;
let ahead = self.asked[&need].clone();
if !self.compile_ahead(device, &need, &ahead, urgency == Urgency::Draw) {
ahead.land(&self.landed);
}
}
}
#[cfg_attr(
not(all(target_arch = "wasm32", feature = "web")),
expect(clippy::unused_self, reason = "only a browser's device compiles ahead")
)]
fn in_flight(&self) -> u64 {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
return self.compiling.load(Ordering::Acquire);
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
0
}
#[cfg_attr(
not(all(target_arch = "wasm32", feature = "web")),
expect(clippy::unused_self, reason = "only a browser's device compiles ahead")
)]
fn drawing(&self) -> u64 {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
return self.drawing.load(Ordering::Acquire);
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
0
}
pub(crate) fn ready(&self, needs: &[Asked]) -> bool {
needs.iter().all(|need| self.asked.get(need).is_some_and(|ahead| ahead.landed()))
}
pub(crate) fn landed(&self) -> u64 {
self.landed.load(Ordering::Acquire)
}
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn compiling(&self) -> u64 {
self.in_flight() + self.queue.len() as u64
}
#[cfg_attr(
not(all(target_arch = "wasm32", feature = "web")),
expect(clippy::unused_self, reason = "only a browser's device compiles ahead")
)]
fn compile_ahead(&mut self, device: &wgpu::Device, need: &Asked, ahead: &Arc<Ahead>, draws: bool) -> bool {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let Some(promise) = compiled::capture(device, || self.create(device, need)) else { return false };
self.counters.add(Count::Pipelines, 1);
ahead.compiled.store(true, Ordering::Release);
let (ahead, landed, compiling) = (ahead.clone(), self.landed.clone(), self.compiling.clone());
let drawing = draws.then(|| self.drawing.clone());
let gpu = self.gpu.clone();
compiling.fetch_add(1, Ordering::AcqRel);
if let Some(drawing) = &drawing {
drawing.fetch_add(1, Ordering::AcqRel);
}
wasm_bindgen_futures::spawn_local(async move {
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
compiling.fetch_sub(1, Ordering::AcqRel);
if let Some(drawing) = drawing {
drawing.fetch_sub(1, Ordering::AcqRel);
}
if let Some(gpu) = gpu.upgrade()
&& let Some(mut inner) = gpu.try_lock()
{
inner.pipelines.pump(&gpu.device);
}
ahead.land(&landed);
});
true
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let _ = (device, need, ahead, draws);
false
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn create(&mut self, device: &wgpu::Device, need: &Asked) {
match need {
Asked::Shader(variant) if variant.target.is_some() => {
let module = self.module(device, variant.shader);
drop(self.pipeline(device, *variant, &module));
}
Asked::Shader(variant) => drop(self.compute_made(device, *variant)),
Asked::Custom(source, variant) => {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = custom_module(device, source, variant.shader);
drop(self.pipeline(device, *variant, &module));
drop(scope.pop());
}
}
}
fn count(&self, need: &Asked) {
let ahead = self.asked.get(need);
if !ahead.is_some_and(|ahead| ahead.compiled.load(Ordering::Acquire)) {
self.counters.add(Count::Pipelines, 1);
}
if ahead.is_none() {
self.counters.add(Count::Late, 1);
}
}
pub(crate) fn pass_layout(&mut self, device: &wgpu::Device, shader: Shader) -> wgpu::BindGroupLayout {
let entries: Vec<_> = shader.source().pass.iter().map(Binding::layout_entry).collect();
self.pass_layouts
.entry(entries)
.or_insert_with_key(|entries| {
device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("latkit pass"), entries })
})
.clone()
}
fn module(&mut self, device: &wgpu::Device, shader: Shader) -> wgpu::ShaderModule {
self.modules
.entry(shader)
.or_insert_with(|| {
let source = shader.source();
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(source.label),
source: wgpu::ShaderSource::Wgsl(source.wgsl().into()),
})
})
.clone()
}
fn pipeline_layout(&mut self, device: &wgpu::Device, shader: Shader) -> wgpu::PipelineLayout {
let pass = (!shader.source().pass.is_empty()).then(|| self.pass_layout(device, shader));
let mut groups = if shader.source().shared {
vec![Some(&self.view_layout), Some(&self.mark_layout)]
} else {
vec![Some(&self.empty_layout), Some(&self.empty_layout)]
};
if let Some(pass) = &pass {
groups.push(Some(pass));
}
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(shader.source().label),
bind_group_layouts: &groups,
immediate_size: 0,
})
}
pub(crate) fn custom(
&mut self,
device: &wgpu::Device,
source: &str,
shader: Shader,
format: wgpu::TextureFormat,
samples: u32,
) -> Option<wgpu::RenderPipeline> {
let key = (Arc::<str>::from(source), Variant::draw(shader, format, samples));
if let Some(custom) = self.custom.get(&key) {
return (!custom.failed).then(|| custom.pipeline.clone());
}
self.count(&Asked::Custom(key.0.clone(), key.1));
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = custom_module(device, source, shader);
let pipeline = self.pipeline(device, key.1, &module);
let check: Check = Box::pin(scope.pop());
let custom = Custom { pipeline: pipeline.clone(), check: Some(check), failed: false };
self.custom.insert(key, custom);
Some(pipeline)
}
pub(crate) fn custom_errors(&mut self) -> Vec<Error> {
let mut context = Context::from_waker(Waker::noop());
let mut errors = Vec::new();
for custom in self.custom.values_mut() {
let Some(check) = &mut custom.check else { continue };
if let Poll::Ready(error) = check.as_mut().poll(&mut context) {
custom.check = None;
if let Some(error) = error {
custom.failed = true;
errors.push(Error::invalid(format!("the custom marker does not compile: {error}")).at("marker"));
}
}
}
errors
}
pub(crate) fn render(
&mut self,
device: &wgpu::Device,
shader: Shader,
format: wgpu::TextureFormat,
samples: u32,
) -> wgpu::RenderPipeline {
self.render_pipeline(device, Variant::draw(shader, format, samples))
}
pub(crate) fn render_pipeline(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::RenderPipeline {
if let Some(pipeline) = self.render.get(&variant) {
return pipeline.clone();
}
self.count(&Asked::Shader(variant));
let module = self.module(device, variant.shader);
let pipeline = self.pipeline(device, variant, &module);
self.render.insert(variant, pipeline.clone());
pipeline
}
fn pipeline(
&mut self,
device: &wgpu::Device,
variant: Variant,
module: &wgpu::ShaderModule,
) -> wgpu::RenderPipeline {
let shader = variant.shader;
let (format, samples) = variant.target.expect("a render variant");
let layout = self.pipeline_layout(device, shader);
let constants = variant.constants();
let options = || wgpu::PipelineCompilationOptions { constants: &constants, ..Default::default() };
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(shader.source().label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module,
entry_point: Some("vertex"),
compilation_options: options(),
buffers: &[],
},
primitive: wgpu::PrimitiveState { topology: topology(shader), ..Default::default() },
depth_stencil: depth(shader),
multisample: wgpu::MultisampleState { count: samples, ..Default::default() },
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some("fragment"),
compilation_options: options(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: blend(shader),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
pub(crate) fn compute(
&mut self,
device: &wgpu::Device,
shader: Shader,
entry: &'static str,
) -> wgpu::ComputePipeline {
self.compute_pipeline(device, Variant::compute(shader, entry))
}
pub(crate) fn compute_pipeline(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::ComputePipeline {
if let Some(pipeline) = self.compute.get(&variant) {
return pipeline.clone();
}
self.count(&Asked::Shader(variant));
let pipeline = self.compute_made(device, variant);
self.compute.insert(variant, pipeline.clone());
pipeline
}
fn compute_made(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::ComputePipeline {
let module = self.module(device, variant.shader);
let layout = self.pipeline_layout(device, variant.shader);
let constants = variant.constants();
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(variant.entry),
layout: Some(&layout),
module: &module,
entry_point: Some(variant.entry),
compilation_options: wgpu::PipelineCompilationOptions { constants: &constants, ..Default::default() },
cache: None,
})
}
}
fn custom_module(device: &wgpu::Device, source: &str, shader: Shader) -> wgpu::ShaderModule {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("latkit custom marker"),
source: wgpu::ShaderSource::Wgsl(format!("{}\n{source}", shader.source().wgsl()).into()),
})
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mod compiled {
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
const MAKERS: [(&str, &str); 2] = [
("createRenderPipeline", "createRenderPipelineAsync"),
("createComputePipeline", "createComputePipelineAsync"),
];
pub(super) fn capture(device: &wgpu::Device, make: impl FnOnce()) -> Option<js_sys::Promise> {
let raw: &JsValue = device.as_webgpu()?.as_ref();
let twins: Vec<js_sys::Function> = MAKERS
.iter()
.map(|(_, twin)| js_sys::Reflect::get(raw, &JsValue::from_str(twin)).ok()?.dyn_into().ok())
.collect::<Option<_>>()?;
let caught: Rc<RefCell<Option<js_sys::Promise>>> = Rc::default();
let shims: Vec<Closure<dyn FnMut(JsValue) -> JsValue>> = twins
.into_iter()
.map(|twin| {
let (device, caught) = (raw.clone(), caught.clone());
Closure::new(move |descriptor: JsValue| {
let promise = twin.call1(&device, &descriptor).unwrap_or(JsValue::UNDEFINED);
*caught.borrow_mut() = promise.clone().dyn_into().ok();
promise
})
})
.collect();
let swapped = MAKERS
.iter()
.zip(&shims)
.all(|((maker, _), shim)| js_sys::Reflect::set(raw, &JsValue::from_str(maker), shim.as_ref()).is_ok());
if swapped {
make();
}
for (maker, _) in MAKERS {
let _ = js_sys::Reflect::delete_property(raw.unchecked_ref::<js_sys::Object>(), &JsValue::from_str(maker));
}
drop(shims);
caught.take()
}
}