Skip to main content

gpu/
web_res.rs

1// web_res -- WebGPU resources, bindings and pipelines in the browser (through web/gpu.js)
2//
3//   device.create_buffer(&BufferDesc)      copies to and from it allowed.  Upload: Buffer::write is
4//        queue.writeBuffer (before the next submission, as mapped memory would be there).  Readback:
5//        only copies may write it (WebGPU: MAP_READ goes with COPY_DST alone); after a submission that
6//        copied into it the browser maps it, Buffer::read says NotReady until then
7//   device.create_texture, texture.create_view, device.create_sampler   as WebGPU's
8//   ShaderModule::from_wgsl(&device, &str) the browser takes the WGSL as it is, its validator is the
9//        only one (lib/wgsl is not in the bundle; scan below reads names, overrides and bindings for
10//        what the API promises).  from_spirv: Unsupported
11//   Push constants: WGSL `var<immediate> x: T;` becomes `@group(3) @binding(0) var<uniform> x: T;`
12//        (one block, a uniform layout), group 3 of a pipeline layout with push constants: at most
13//        three groups of its own then.  web/gpu.js puts them into a ring behind a dynamic offset.
14//   Pipeline constants: WebGPU's `constants`, the key the id as text; an override without @id (ids from
15//        0x10000: docs/GPU.md "Pipeline-Konstanten") by its name.  Unknown or doubled ids are Invalid.
16//   device.create_bind_group_layout(&[Binding])   WebGPU wants more than Binding says (a texture's view
17//        dimension and sample type, a storage texture's format, a sampler's kind).  So the layout is made
18//        late: from the first bind group's resources, or from the WGSL of the first pipeline that uses
19//        it, whichever comes first
20//
21// A browser's errors come later than the call (web_dev.rs); what can be known here is Invalid at once.
22
23use alloc::format;
24use alloc::rc::Rc;
25use alloc::string::String;
26use alloc::vec::Vec;
27use core::cell::RefCell;
28
29use crate::web_dev::{call, create, json_f64, DevInner, Device, Handle, CALL_PIPELINE_LAYOUT, CALL_READ_BUFFER, CALL_SHADER, CALL_WRITE_BUFFER};
30use crate::*;
31
32const METHOD_BUFFER: u32 = 0;
33const METHOD_TEXTURE: u32 = 1;
34const METHOD_SAMPLER: u32 = 2;
35const METHOD_BIND_GROUP_LAYOUT: u32 = 3;
36const METHOD_BIND_GROUP: u32 = 4;
37const METHOD_COMPUTE_PIPELINE: u32 = 5;
38const METHOD_RENDER_PIPELINE: u32 = 6;
39const METHOD_VIEW: u32 = 100;
40
41// GPUBufferUsage
42const BU_MAP_READ: u32 = 1;
43const BU_COPY_SRC: u32 = 4;
44const BU_COPY_DST: u32 = 8;
45const BU_INDEX: u32 = 16;
46const BU_VERTEX: u32 = 32;
47const BU_UNIFORM: u32 = 64;
48const BU_STORAGE: u32 = 128;
49const BU_INDIRECT: u32 = 256;
50
51pub(crate) struct BufInner {
52    h: Handle,
53    d: Rc<DevInner>,
54    size: u64,
55    usage: BufferUsage,
56    memory: MemoryKind,
57}
58
59impl Drop for BufInner {
60    fn drop(&mut self) {
61        self.d.reserved.set(self.d.reserved.get().saturating_sub(self.size));
62    }
63}
64
65/// GPU memory for data: vertices, indices, storage, uniforms, indirect arguments
66#[derive(Clone)]
67pub struct Buffer(pub(crate) Rc<BufInner>);
68
69impl Buffer {
70    pub fn size(&self) -> u64 {
71        self.0.size
72    }
73    pub fn usage(&self) -> BufferUsage {
74        self.0.usage
75    }
76    pub fn memory(&self) -> MemoryKind {
77        self.0.memory
78    }
79    pub(crate) fn handle(&self) -> u32 {
80        self.0.h.h
81    }
82
83    /// data into an Upload buffer at offset: the GPU sees it in the next submission (queue.writeBuffer)
84    pub fn write<T: Pod>(&self, offset: u64, data: &[T]) -> Result<()> {
85        if self.0.memory == MemoryKind::Device {
86            return Err(GpuError::Invalid("Buffer::write: an Upload buffer"));
87        }
88        if self.0.memory == MemoryKind::Readback {
89            return Err(GpuError::Unsupported("Buffer::write on a Readback buffer: WebGPU maps it for reading only"));
90        }
91        let b = bytes_of(data);
92        if offset % 4 != 0 || b.len() % 4 != 0 || offset.checked_add(b.len() as u64).is_none_or(|e| e > self.0.size) {
93            return Err(GpuError::Invalid("Buffer::write: outside the buffer, or not in multiples of 4"));
94        }
95        call(CALL_WRITE_BUFFER, &[self.0.d.h, self.handle(), offset as u32, (offset >> 32) as u32, b.as_ptr() as u32, b.len() as u32]);
96        Ok(())
97    }
98
99    /// a Readback buffer's contents at offset into out: Err(NotReady) until the submission that copied
100    /// into it has finished and the browser has mapped it
101    pub fn read<T: Pod>(&self, offset: u64, out: &mut [T]) -> Result<()> {
102        if self.0.memory != MemoryKind::Readback {
103            return Err(GpuError::Unsupported("Buffer::read: only a Readback buffer in the browser"));
104        }
105        let n = core::mem::size_of_val(out);
106        if offset.checked_add(n as u64).is_none_or(|e| e > self.0.size) {
107            return Err(GpuError::Invalid("Buffer::read: outside the buffer"));
108        }
109        self.0.d.check()?;
110        match call(CALL_READ_BUFFER, &[self.handle(), offset as u32, (offset >> 32) as u32, out.as_mut_ptr() as u32, n as u32]) {
111            0 => Err(GpuError::NotReady),
112            _ => Ok(()),
113        }
114    }
115}
116
117impl Device {
118    pub fn create_buffer(&self, desc: &BufferDesc) -> Result<Buffer> {
119        self.d.check()?;
120        if desc.size == 0 {
121            return Err(GpuError::Invalid("a buffer of 0 bytes"));
122        }
123        let u = desc.usage;
124        let mut bits = 0;
125        for (f, b) in [(BufferUsage::UNIFORM, BU_UNIFORM), (BufferUsage::STORAGE, BU_STORAGE), (BufferUsage::INDEX, BU_INDEX), (BufferUsage::VERTEX, BU_VERTEX), (BufferUsage::INDIRECT, BU_INDIRECT)] {
126            if u.contains(f) {
127                bits |= b;
128            }
129        }
130        let usage = if desc.memory == MemoryKind::Readback {
131            if bits != 0 {
132                return Err(GpuError::Unsupported("a Readback buffer takes copies only in WebGPU"));
133            }
134            BU_MAP_READ | BU_COPY_DST
135        } else {
136            bits | BU_COPY_SRC | BU_COPY_DST
137        };
138        let h = create(self.d.h, METHOD_BUFFER, &format!("{{\"size\":{},\"usage\":{usage}}}", desc.size))?;
139        self.d.reserved.set(self.d.reserved.get() + desc.size);
140        Ok(Buffer(Rc::new(BufInner { h: Handle { h, destroy: true }, d: self.d.clone(), size: desc.size, usage: desc.usage, memory: desc.memory })))
141    }
142}
143
144/// the name WebGPU has for a format
145pub(crate) fn format_name(f: Format) -> &'static str {
146    use Format::*;
147    match f {
148        R8Unorm => "r8unorm",
149        Rg8Unorm => "rg8unorm",
150        Rgba8Unorm => "rgba8unorm",
151        Rgba8Srgb => "rgba8unorm-srgb",
152        Bgra8Unorm => "bgra8unorm",
153        Bgra8Srgb => "bgra8unorm-srgb",
154        Rgb10a2Unorm => "rgb10a2unorm",
155        Rg11b10Float => "rg11b10ufloat",
156        R16Float => "r16float",
157        Rg16Float => "rg16float",
158        Rgba16Float => "rgba16float",
159        R32Uint => "r32uint",
160        R32Sint => "r32sint",
161        R32Float => "r32float",
162        Rg32Uint => "rg32uint",
163        Rg32Float => "rg32float",
164        Rgba32Uint => "rgba32uint",
165        Rgba32Float => "rgba32float",
166        Depth16 => "depth16unorm",
167        Depth32Float => "depth32float",
168        Depth24Stencil8 => "depth24plus-stencil8",
169        Depth32FloatStencil8 => "depth32float-stencil8",
170        Bc1Unorm => "bc1-rgba-unorm",
171        Bc1Srgb => "bc1-rgba-unorm-srgb",
172        Bc3Unorm => "bc3-rgba-unorm",
173        Bc3Srgb => "bc3-rgba-unorm-srgb",
174        Bc4Unorm => "bc4-r-unorm",
175        Bc5Unorm => "bc5-rg-unorm",
176        Bc7Unorm => "bc7-rgba-unorm",
177        Bc7Srgb => "bc7-rgba-unorm-srgb",
178    }
179}
180
181fn dim_name(d: ViewDim) -> &'static str {
182    match d {
183        ViewDim::D2 => "2d",
184        ViewDim::D2Array => "2d-array",
185        ViewDim::Cube => "cube",
186        ViewDim::CubeArray => "cube-array",
187        ViewDim::D3 => "3d",
188    }
189}
190
191pub(crate) struct ViewInner {
192    pub h: Handle,
193    format: Format,
194    w: u32,
195    hh: u32,
196    dim: ViewDim,
197    _tex: Option<Rc<TexInner>>,
198}
199
200/// a range of a texture's mips and layers as shaders and render passes see it
201#[derive(Clone)]
202pub struct TextureView(pub(crate) Rc<ViewInner>);
203
204impl TextureView {
205    pub fn format(&self) -> Format {
206        self.0.format
207    }
208    /// width and height of the view's first mip
209    pub fn size(&self) -> (u32, u32) {
210        (self.0.w, self.0.hh)
211    }
212    pub(crate) fn handle(&self) -> u32 {
213        self.0.h.h
214    }
215    /// what its texture may be used for (a swapchain frame's: a colour target)
216    pub fn usage(&self) -> TextureUsage {
217        self.0._tex.as_ref().map_or(TextureUsage::COLOR_TARGET, |t| t.desc.usage)
218    }
219    /// the same view (it or a clone of it), not merely one of the same range: a cache's key
220    pub fn same(&self, other: &TextureView) -> bool {
221        Rc::ptr_eq(&self.0, &other.0)
222    }
223    /// (the swapchain's image, web_swap.rs: its object changes with every frame)
224    pub(crate) fn for_frame(h: u32, format: Format, w: u32, hh: u32) -> TextureView {
225        TextureView(Rc::new(ViewInner { h: Handle { h, destroy: false }, format, w, hh, dim: ViewDim::D2, _tex: None }))
226    }
227}
228
229pub(crate) struct TexInner {
230    h: Handle,
231    d: Option<Rc<DevInner>>,
232    desc: TextureDesc,
233    bytes: u64,
234}
235
236impl Drop for TexInner {
237    fn drop(&mut self) {
238        if let Some(d) = &self.d {
239            d.reserved.set(d.reserved.get().saturating_sub(self.bytes));
240        }
241    }
242}
243
244/// an image on the GPU: 2D, 2D array, cube (array) or 3D, with mips
245#[derive(Clone)]
246pub struct Texture {
247    t: Rc<TexInner>,
248    view: TextureView,
249}
250
251impl Texture {
252    /// the view of everything (its natural dimension)
253    pub fn view(&self) -> &TextureView {
254        &self.view
255    }
256    pub fn width(&self) -> u32 {
257        self.t.desc.width
258    }
259    pub fn height(&self) -> u32 {
260        self.t.desc.height
261    }
262    pub fn depth(&self) -> u32 {
263        self.t.desc.depth
264    }
265    pub fn layers(&self) -> u32 {
266        self.t.desc.layers
267    }
268    pub fn mips(&self) -> u32 {
269        self.t.desc.mips
270    }
271    pub fn format(&self) -> Format {
272        self.t.desc.format
273    }
274    pub fn usage(&self) -> TextureUsage {
275        self.t.desc.usage
276    }
277    pub(crate) fn handle(&self) -> u32 {
278        self.t.h.h
279    }
280    /// (the swapchain's image, web_swap.rs: its object changes with every frame)
281    pub(crate) fn for_frame(h: u32, view: TextureView, format: Format, width: u32, height: u32, usage: TextureUsage) -> Texture {
282        let desc = TextureDesc::d2(width, height, format, usage);
283        Texture { t: Rc::new(TexInner { h: Handle { h, destroy: false }, d: None, desc, bytes: 0 }), view }
284    }
285
286    /// a view of some mips / layers (counts 0: to the end)
287    pub fn create_view(&self, d: &ViewDesc) -> Result<TextureView> {
288        let t = &self.t.desc;
289        let mips = if d.mips == 0 { t.mips.saturating_sub(d.base_mip) } else { d.mips };
290        let total = if t.depth > 1 { 1 } else { t.layers };
291        let layers = if d.layers == 0 { total.saturating_sub(d.base_layer) } else { d.layers };
292        if mips == 0 || d.base_mip.checked_add(mips).is_none_or(|e| e > t.mips) || layers == 0
293            || d.base_layer.checked_add(layers).is_none_or(|e| e > total) {
294            return Err(GpuError::Invalid("create_view: outside the texture"));
295        }
296        make_view(&self.t, d.dim, d.base_mip, mips, d.base_layer, layers)
297    }
298}
299
300fn make_view(t: &Rc<TexInner>, dim: ViewDim, base_mip: u32, mips: u32, base_layer: u32, layers: u32) -> Result<TextureView> {
301    let json = format!(
302        "{{\"dimension\":\"{}\",\"baseMipLevel\":{base_mip},\"mipLevelCount\":{mips},\"baseArrayLayer\":{base_layer},\"arrayLayerCount\":{layers}}}",
303        dim_name(dim)
304    );
305    let h = create(t.h.h, METHOD_VIEW, &json)?;
306    let (w, hh) = ((t.desc.width >> base_mip).max(1), (t.desc.height >> base_mip).max(1));
307    Ok(TextureView(Rc::new(ViewInner { h: Handle { h, destroy: false }, format: t.desc.format, w, hh, dim, _tex: Some(t.clone()) })))
308}
309
310fn default_dim(d: &TextureDesc) -> ViewDim {
311    if d.depth > 1 {
312        ViewDim::D3
313    } else if d.cube {
314        if d.layers == 6 { ViewDim::Cube } else { ViewDim::CubeArray }
315    } else if d.layers > 1 {
316        ViewDim::D2Array
317    } else {
318        ViewDim::D2
319    }
320}
321
322impl Device {
323    pub fn create_texture(&self, desc: &TextureDesc) -> Result<Texture> {
324        self.d.check()?;
325        if desc.width == 0 || desc.height == 0 || desc.depth == 0 || desc.layers == 0 || desc.mips == 0 {
326            return Err(GpuError::Invalid("a texture without size"));
327        }
328        let lim = self.d.limits.max_texture_2d;
329        if desc.width > lim || desc.height > lim {
330            return Err(GpuError::Unsupported("the texture is larger than this device's limit"));
331        }
332        if desc.format.is_compressed() && !self.d.features.texture_bc {
333            return Err(GpuError::Unsupported("BC textures"));
334        }
335        let u = desc.usage.0;
336        // (GPUTextureUsage has the first four bits; RENDER_ATTACHMENT for both kinds of target)
337        let usage = (u & 0xF) | if u & 0x30 != 0 { 16 } else { 0 };
338        let (third, dimension) = if desc.depth > 1 { (desc.depth, "3d") } else { (desc.layers, "2d") };
339        let json = format!(
340            "{{\"size\":[{},{},{third}],\"mipLevelCount\":{},\"format\":\"{}\",\"usage\":{usage},\"dimension\":\"{dimension}\"}}",
341            desc.width, desc.height, desc.mips, format_name(desc.format)
342        );
343        let h = create(self.d.h, METHOD_TEXTURE, &json)?;
344        let dim = desc.width as u64 / desc.format.block_dim() as u64 * (desc.height / desc.format.block_dim()) as u64;
345        let mut bytes = dim * desc.format.block_bytes() as u64 * third as u64;
346        if desc.mips > 1 {
347            bytes += bytes / 3;
348        }
349        self.d.reserved.set(self.d.reserved.get() + bytes);
350        let t = Rc::new(TexInner { h: Handle { h, destroy: true }, d: Some(self.d.clone()), desc: *desc, bytes });
351        let layers = if desc.depth > 1 { 1 } else { desc.layers };
352        let view = make_view(&t, default_dim(desc), 0, desc.mips, 0, layers)?;
353        Ok(Texture { t, view })
354    }
355
356    pub fn create_sampler(&self, d: &SamplerDesc) -> Result<Sampler> {
357        self.d.check()?;
358        let address = |m: AddressMode| match m {
359            AddressMode::Repeat => "repeat",
360            AddressMode::MirrorRepeat => "mirror-repeat",
361            AddressMode::ClampToEdge => "clamp-to-edge",
362        };
363        let filter = |f: Filter| if f == Filter::Linear { "linear" } else { "nearest" };
364        let compare = d.compare.map_or(String::new(), |c| format!(",\"compare\":\"{}\"", compare_name(c)));
365        let json = format!(
366            "{{\"addressModeU\":\"{}\",\"addressModeV\":\"{}\",\"addressModeW\":\"{}\",\"magFilter\":\"{}\",\"minFilter\":\"{}\",\"mipmapFilter\":\"{}\",\"lodMinClamp\":{},\"lodMaxClamp\":{},\"maxAnisotropy\":{}{compare}}}",
367            address(d.address[0]), address(d.address[1]), address(d.address[2]), filter(d.mag), filter(d.min), filter(d.mip),
368            json_f64(d.lod_min as f64), json_f64(d.lod_max as f64), d.anisotropy.max(1),
369        );
370        let h = create(self.d.h, METHOD_SAMPLER, &json)?;
371        let filtering = d.mag == Filter::Linear || d.min == Filter::Linear || d.mip == Filter::Linear;
372        Ok(Sampler(Rc::new(SamplerInner { h: Handle { h, destroy: false }, comparison: d.compare.is_some(), filtering })))
373    }
374}
375
376fn compare_name(c: Compare) -> &'static str {
377    match c {
378        Compare::Never => "never",
379        Compare::Less => "less",
380        Compare::Equal => "equal",
381        Compare::LessEqual => "less-equal",
382        Compare::Greater => "greater",
383        Compare::NotEqual => "not-equal",
384        Compare::GreaterEqual => "greater-equal",
385        Compare::Always => "always",
386    }
387}
388
389pub(crate) struct SamplerInner {
390    h: Handle,
391    comparison: bool,
392    filtering: bool,
393}
394
395/// how textures are filtered and addressed
396#[derive(Clone)]
397pub struct Sampler(pub(crate) Rc<SamplerInner>);
398
399// ---------------------------------------------------------------- what a WGSL text declares
400
401/// what a binding of a module looks like: the rest of a WebGPU layout entry
402#[derive(Clone, PartialEq)]
403struct Detail {
404    binding: u32,
405    group: u32,
406    json: String,
407}
408
409/// names and numbers the API promises from a module; the browser's validator judges the code
410#[derive(Default)]
411struct Scan {
412    entries: Vec<(u32, String)>,
413    /// id (from 0x10000: no @id, by position), kind, default value, name
414    overrides: Vec<(u32, shader::reflect::Kind, f64, String)>,
415    bindings: Vec<Detail>,
416}
417
418// the text without comments
419fn strip_comments(src: &str) -> String {
420    let b = src.as_bytes();
421    let mut out = Vec::with_capacity(b.len());
422    let (mut i, mut depth) = (0, 0);
423    while i < b.len() {
424        if b[i..].starts_with(b"/*") {
425            depth += 1;
426            i += 2;
427        } else if depth > 0 {
428            if b[i..].starts_with(b"*/") {
429                depth -= 1;
430                i += 2;
431            } else {
432                i += 1;
433            }
434        } else if b[i..].starts_with(b"//") {
435            while i < b.len() && b[i] != b'\n' {
436                i += 1;
437            }
438        } else {
439            out.push(b[i]);
440            i += 1;
441        }
442    }
443    String::from_utf8(out).unwrap_or_default()
444}
445
446// the tokens: words (numbers and identifiers), each other character by itself
447fn tokens(s: &str) -> Vec<&str> {
448    let mut out = Vec::new();
449    let mut at = None;
450    for (i, c) in s.char_indices() {
451        let word = c.is_alphanumeric() || c == '_' || c == '.';
452        match (word, at) {
453            (true, None) => at = Some(i),
454            (false, Some(a)) => {
455                out.push(&s[a..i]);
456                at = None;
457            }
458            _ => {}
459        }
460        if !word && !c.is_whitespace() {
461            out.push(&s[i..i + c.len_utf8()]);
462        }
463    }
464    if let Some(a) = at {
465        out.push(&s[a..]);
466    }
467    out
468}
469
470fn scan(src: &str) -> Scan {
471    let text = strip_comments(src);
472    let t = tokens(&text);
473    let mut s = Scan::default();
474    let (mut group, mut binding, mut id, mut stage) = (None, None, None, None);
475    let mut nameless = 0x10000u32;
476    let mut depth = 0i32;
477    let mut i = 0;
478    while i < t.len() {
479        match t[i] {
480            "{" => depth += 1,
481            "}" => depth -= 1,
482            "@" if depth == 0 => {
483                let what = t.get(i + 1).copied().unwrap_or("");
484                let arg = if t.get(i + 2) == Some(&"(") { t.get(i + 3).and_then(|v| v.trim_end_matches(['u', 'i']).parse::<u32>().ok()) } else { None };
485                match what {
486                    "group" => group = arg,
487                    "binding" => binding = arg,
488                    "id" => id = arg,
489                    "vertex" => stage = Some(0),
490                    "fragment" => stage = Some(4),
491                    "compute" => stage = Some(5),
492                    _ => {}
493                }
494                i += 2;
495                continue;
496            }
497            "fn" if depth == 0 => {
498                if let (Some(m), Some(name)) = (stage.take(), t.get(i + 1)) {
499                    s.entries.push((m, String::from(*name)));
500                }
501            }
502            "override" if depth == 0 => {
503                let name = t.get(i + 1).copied().unwrap_or("");
504                let mut k = i + 2;
505                let mut kind = shader::reflect::Kind::Float { width: 32 };
506                if t.get(k) == Some(&":") {
507                    kind = match t.get(k + 1).copied().unwrap_or("") {
508                        "bool" => shader::reflect::Kind::Bool,
509                        "i32" => shader::reflect::Kind::Int { width: 32, signed: true },
510                        "u32" => shader::reflect::Kind::Int { width: 32, signed: false },
511                        "f16" => shader::reflect::Kind::Float { width: 16 },
512                        _ => shader::reflect::Kind::Float { width: 32 },
513                    };
514                    k += 2;
515                }
516                let mut value = 0.0;
517                if t.get(k) == Some(&"=") {
518                    let neg = t.get(k + 1) == Some(&"-");
519                    let lit = t.get(k + if neg { 2 } else { 1 }).copied().unwrap_or("");
520                    value = match lit {
521                        "true" => 1.0,
522                        "false" => 0.0,
523                        _ => lit.trim_end_matches(['u', 'i', 'f', 'h']).parse().unwrap_or(0.0),
524                    };
525                    if neg {
526                        value = -value;
527                    }
528                }
529                let n = id.take().unwrap_or_else(|| {
530                    let v = nameless;
531                    nameless += 1;
532                    v
533                });
534                s.overrides.push((n, kind, value, String::from(name)));
535            }
536            "var" if depth == 0 => {
537                if let (Some(g), Some(b)) = (group.take(), binding.take()) {
538                    let end = (i..t.len()).find(|&k| t[k] == ";").unwrap_or(t.len());
539                    if let Some(json) = binding_detail(&t[i..end]) {
540                        s.bindings.push(Detail { group: g, binding: b, json });
541                    }
542                }
543            }
544            _ => {}
545        }
546        i += 1;
547    }
548    s
549}
550
551// the layout entry of `var name: type` or `var<space> name: type`
552fn binding_detail(t: &[&str]) -> Option<String> {
553    let at = t.iter().position(|w| *w == ":")?;
554    let ty = *t.get(at + 1)?;
555    let args: Vec<&str> = t[at + 2..].iter().copied().filter(|w| !matches!(*w, "<" | ">" | ",")).collect();
556    let dim = |ty: &str| {
557        if ty.ends_with("2d_array") {
558            "2d-array"
559        } else if ty.ends_with("cube_array") {
560            "cube-array"
561        } else if ty.ends_with("cube") {
562            "cube"
563        } else if ty.ends_with("3d") {
564            "3d"
565        } else if ty.ends_with("1d") {
566            "1d"
567        } else {
568            "2d"
569        }
570    };
571    if ty == "sampler" {
572        return Some(String::from("\"sampler\":{\"type\":\"filtering\"}"));
573    }
574    if ty == "sampler_comparison" {
575        return Some(String::from("\"sampler\":{\"type\":\"comparison\"}"));
576    }
577    if ty.starts_with("texture_storage_") {
578        let access = match args.get(1).copied().unwrap_or("write") {
579            "read" => "read-only",
580            "read_write" => "read-write",
581            _ => "write-only",
582        };
583        return Some(format!("\"storageTexture\":{{\"access\":\"{access}\",\"format\":\"{}\",\"viewDimension\":\"{}\"}}", args.first().copied().unwrap_or("rgba8unorm"), dim(ty)));
584    }
585    if ty.starts_with("texture_depth_") {
586        return Some(format!("\"texture\":{{\"sampleType\":\"depth\",\"viewDimension\":\"{}\"}}", dim(ty)));
587    }
588    if ty.starts_with("texture_") {
589        let sample = match args.first().copied().unwrap_or("f32") {
590            "i32" => "sint",
591            "u32" => "uint",
592            _ => "float",
593        };
594        return Some(format!("\"texture\":{{\"sampleType\":\"{sample}\",\"viewDimension\":\"{}\"}}", dim(ty)));
595    }
596    None
597}
598
599pub(crate) struct ShaderInner {
600    h: Handle,
601    scan: Scan,
602}
603
604/// shader code: WGSL, as the browser takes it
605#[derive(Clone)]
606pub struct ShaderModule(pub(crate) Rc<ShaderInner>);
607
608impl ShaderModule {
609    /// the browser takes WGSL: ShaderModule::from_wgsl
610    pub fn from_spirv(_device: &Device, _code: &[u32]) -> Result<ShaderModule> {
611        Err(GpuError::Unsupported("SPIR-V in the browser: ShaderModule::from_wgsl"))
612    }
613
614    /// a module from WGSL source, which the browser compiles and validates (its errors come later:
615    /// web_dev.rs); `var<immediate>` becomes the uniform block behind push constants
616    pub fn from_wgsl(device: &Device, source: &str) -> Result<ShaderModule> {
617        device.d.check()?;
618        let code = source.replace("var<immediate>", "@group(3) @binding(0) var<uniform>");
619        let h = call(CALL_SHADER, &[device.d.h, code.as_ptr() as u32, code.len() as u32]);
620        if h == 0 {
621            return Err(GpuError::Failed(0));
622        }
623        Ok(ShaderModule(Rc::new(ShaderInner { h: Handle { h, destroy: false }, scan: scan(source) })))
624    }
625
626    /// the entry points: (model as SPIR-V numbers it: 0 vertex, 4 fragment, 5 compute, name)
627    pub fn entry_points(&self) -> Vec<(u32, &str)> {
628        self.0.scan.entries.iter().map(|(m, n)| (*m, n.as_str())).collect()
629    }
630
631    /// the pipeline-overridable constants the module declares: (id, kind, default value)
632    pub fn constants(&self) -> Vec<(u32, shader::reflect::Kind, f64)> {
633        self.0.scan.overrides.iter().map(|(i, k, v, _)| (*i, *k, *v)).collect()
634    }
635}
636
637// ---------------------------------------------------------------- bindings
638
639pub(crate) struct BglInner {
640    d: Rc<DevInner>,
641    bindings: Vec<Binding>,
642    /// made when its details are known (the header)
643    h: RefCell<Option<Handle>>,
644}
645
646/// the bindings of one bind group (@group)
647#[derive(Clone)]
648pub struct BindGroupLayout(pub(crate) Rc<BglInner>);
649
650impl BindGroupLayout {
651    // the WebGPU layout, from what `detail` says of each binding (None: its kind's simplest form)
652    fn make(&self, detail: impl Fn(&Binding) -> Option<String>) -> Result<u32> {
653        if let Some(h) = &*self.0.h.borrow() {
654            return Ok(h.h);
655        }
656        let mut entries = Vec::new();
657        for b in &self.0.bindings {
658            let kind = match detail(b) {
659                Some(j) => j,
660                None => String::from(match b.kind {
661                    BindingKind::UniformBuffer => "\"buffer\":{\"type\":\"uniform\"}",
662                    BindingKind::StorageBuffer => "\"buffer\":{\"type\":\"storage\"}",
663                    BindingKind::StorageBufferRead => "\"buffer\":{\"type\":\"read-only-storage\"}",
664                    BindingKind::Texture => "\"texture\":{\"sampleType\":\"float\",\"viewDimension\":\"2d\"}",
665                    BindingKind::StorageTexture => return Err(GpuError::Invalid("a storage texture binding: neither a bind group nor WGSL says its format yet")),
666                    BindingKind::Sampler => "\"sampler\":{\"type\":\"filtering\"}",
667                }),
668            };
669            entries.push(format!("{{\"binding\":{},\"visibility\":{},{kind}}}", b.binding, b.stages.0 & 7));
670        }
671        let json = format!("{{\"entries\":[{}]}}", entries.join(","));
672        let h = create(self.0.d.h, METHOD_BIND_GROUP_LAYOUT, &json)?;
673        *self.0.h.borrow_mut() = Some(Handle { h, destroy: false });
674        Ok(h)
675    }
676}
677
678pub(crate) struct PlInner {
679    d: Rc<DevInner>,
680    pub groups: Vec<BindGroupLayout>,
681    pub push: u32,
682    h: RefCell<Option<Handle>>,
683}
684
685/// the bind group layouts and push constants of pipelines
686#[derive(Clone)]
687pub struct PipelineLayout(pub(crate) Rc<PlInner>);
688
689impl PipelineLayout {
690    // the WebGPU layout; what its groups do not know yet comes from these modules' WGSL
691    fn make(&self, modules: &[&ShaderModule]) -> Result<u32> {
692        if let Some(h) = &*self.0.h.borrow() {
693            return Ok(h.h);
694        }
695        let mut args = alloc::vec![self.0.d.h, self.0.push, self.0.groups.len() as u32];
696        for (g, l) in self.0.groups.iter().enumerate() {
697            let h = l.make(|b| {
698                if matches!(b.kind, BindingKind::UniformBuffer | BindingKind::StorageBuffer | BindingKind::StorageBufferRead) {
699                    return None;
700                }
701                modules.iter().find_map(|m| m.0.scan.bindings.iter().find(|d| d.group == g as u32 && d.binding == b.binding).map(|d| d.json.clone()))
702            })?;
703            args.push(h);
704        }
705        match call(CALL_PIPELINE_LAYOUT, &args) {
706            0 => Err(GpuError::Failed(0)),
707            h => {
708                *self.0.h.borrow_mut() = Some(Handle { h, destroy: false });
709                Ok(h)
710            }
711        }
712    }
713}
714
715pub(crate) struct BgInner {
716    pub h: Handle,
717    layout: BindGroupLayout,
718    _keep: Vec<Keep>,
719}
720
721// what a bind group keeps alive: destroying a buffer or a texture it names would invalidate it
722#[allow(dead_code)]
723enum Keep {
724    Buffer(Buffer),
725    View(TextureView),
726    Sampler(Sampler),
727}
728
729/// resources bound for shaders
730#[derive(Clone)]
731pub struct BindGroup(pub(crate) Rc<BgInner>);
732
733impl BindGroup {
734    pub(crate) fn handle(&self) -> u32 {
735        self.0.h.h
736    }
737    pub(crate) fn fits(&self, l: &BindGroupLayout) -> bool {
738        Rc::ptr_eq(&self.0.layout.0, &l.0)
739    }
740}
741
742impl Device {
743    pub fn create_bind_group_layout(&self, bindings: &[Binding]) -> Result<BindGroupLayout> {
744        let mut seen = 0u64;
745        for b in bindings {
746            if b.binding >= 64 || seen & (1 << b.binding) != 0 {
747                return Err(GpuError::Invalid("a binding twice, or beyond 63"));
748            }
749            seen |= 1 << b.binding;
750        }
751        Ok(BindGroupLayout(Rc::new(BglInner { d: self.d.clone(), bindings: bindings.to_vec(), h: RefCell::new(None) })))
752    }
753
754    /// groups: at most BIND_GROUPS_MAX, and with push constants at most 3 (the last is theirs)
755    pub fn create_pipeline_layout(&self, desc: &PipelineLayoutDesc) -> Result<PipelineLayout> {
756        if desc.groups.len() > BIND_GROUPS_MAX as usize {
757            return Err(GpuError::Invalid("too many bind groups"));
758        }
759        if desc.push_constants > PUSH_CONSTANTS_MAX || desc.push_constants % 4 != 0 {
760            return Err(GpuError::Invalid("push constants: a multiple of 4 up to PUSH_CONSTANTS_MAX"));
761        }
762        if desc.push_constants != 0 && desc.groups.len() > 3 {
763            return Err(GpuError::Unsupported("four bind groups and push constants: they take the fourth in the browser"));
764        }
765        Ok(PipelineLayout(Rc::new(PlInner {
766            d: self.d.clone(),
767            groups: desc.groups.iter().map(|g| (*g).clone()).collect(),
768            push: desc.push_constants,
769            h: RefCell::new(None),
770        })))
771    }
772
773    pub fn create_bind_group(&self, desc: &BindGroupDesc) -> Result<BindGroup> {
774        self.d.check()?;
775        let bindings = &desc.layout.0.bindings;
776        if desc.entries.len() != bindings.len() {
777            return Err(GpuError::Invalid("a bind group needs one entry per binding"));
778        }
779        let entry = |b: u32| desc.entries.iter().find(|e| e.binding == b);
780        let layout = desc.layout.make(|b| match (b.kind, entry(b.binding).map(|e| e.resource)) {
781            (BindingKind::Texture, Some(Resource::Texture(v))) => {
782                let f = v.format();
783                let sample = if f.is_depth() {
784                    "depth"
785                } else if matches!(f, Format::R32Uint | Format::Rg32Uint | Format::Rgba32Uint) {
786                    "uint"
787                } else if f == Format::R32Sint {
788                    "sint"
789                } else if matches!(f, Format::R32Float | Format::Rg32Float | Format::Rgba32Float) {
790                    "unfilterable-float"
791                } else {
792                    "float"
793                };
794                Some(format!("\"texture\":{{\"sampleType\":\"{sample}\",\"viewDimension\":\"{}\"}}", dim_name(v.0.dim)))
795            }
796            (BindingKind::StorageTexture, Some(Resource::StorageTexture(v))) => {
797                Some(format!("\"storageTexture\":{{\"access\":\"write-only\",\"format\":\"{}\",\"viewDimension\":\"{}\"}}", format_name(v.format()), dim_name(v.0.dim)))
798            }
799            (BindingKind::Sampler, Some(Resource::Sampler(s))) => {
800                let t = if s.0.comparison { "comparison" } else if s.0.filtering { "filtering" } else { "non-filtering" };
801                Some(format!("\"sampler\":{{\"type\":\"{t}\"}}"))
802            }
803            _ => None,
804        })?;
805        let mut keep = Vec::with_capacity(desc.entries.len());
806        let mut entries = Vec::with_capacity(desc.entries.len());
807        for e in desc.entries {
808            let Some(b) = bindings.iter().find(|b| b.binding == e.binding) else {
809                return Err(GpuError::Invalid("a bind group entry for a binding the layout lacks"));
810            };
811            let res = match (b.kind, e.resource) {
812                (BindingKind::UniformBuffer | BindingKind::StorageBuffer | BindingKind::StorageBufferRead, Resource::Buffer { buffer, offset, size }) => {
813                    let need = if b.kind == BindingKind::UniformBuffer { BufferUsage::UNIFORM } else { BufferUsage::STORAGE };
814                    if !buffer.usage().contains(need) {
815                        return Err(GpuError::Invalid("a bind group's buffer lacks the usage"));
816                    }
817                    keep.push(Keep::Buffer(buffer.clone()));
818                    let size = if size == 0 { buffer.size().saturating_sub(offset) } else { size };
819                    format!("{{\"buffer\":{{\"$\":{}}},\"offset\":{offset},\"size\":{size}}}", buffer.handle())
820                }
821                (BindingKind::Texture, Resource::Texture(v)) | (BindingKind::StorageTexture, Resource::StorageTexture(v)) => {
822                    keep.push(Keep::View(v.clone()));
823                    format!("{{\"$\":{}}}", v.handle())
824                }
825                (BindingKind::Sampler, Resource::Sampler(s)) => {
826                    keep.push(Keep::Sampler(s.clone()));
827                    format!("{{\"$\":{}}}", s.0.h.h)
828                }
829                _ => return Err(GpuError::Invalid("a bind group entry of the wrong kind")),
830            };
831            entries.push(format!("{{\"binding\":{},\"resource\":{res}}}", e.binding));
832        }
833        let json = format!("{{\"layout\":{{\"$\":{layout}}},\"entries\":[{}]}}", entries.join(","));
834        let h = create(self.d.h, METHOD_BIND_GROUP, &json)?;
835        Ok(BindGroup(Rc::new(BgInner { h: Handle { h, destroy: false }, layout: desc.layout.clone(), _keep: keep })))
836    }
837
838    /// (level Voll)
839    pub fn create_bindless(&self, _desc: &BindlessDesc) -> Result<Bindless> {
840        Err(GpuError::Unsupported("Bindless: level Voll, not in the browser"))
841    }
842}
843
844/// a large table of textures, samplers and buffers (level Voll: never in the browser)
845pub struct Bindless {
846    group: BindGroup,
847    layout: BindGroupLayout,
848}
849
850impl Bindless {
851    pub fn group(&self) -> &BindGroup {
852        &self.group
853    }
854    pub fn layout(&self) -> &BindGroupLayout {
855        &self.layout
856    }
857    pub fn counts(&self) -> [u32; 4] {
858        [0; 4]
859    }
860    pub fn set_texture(&self, _slot: u32, _view: &TextureView) -> Result<()> {
861        Err(GpuError::Unsupported("Bindless"))
862    }
863    pub fn set_sampler(&self, _slot: u32, _s: &Sampler) -> Result<()> {
864        Err(GpuError::Unsupported("Bindless"))
865    }
866    pub fn set_buffer(&self, _slot: u32, _buf: &Buffer, _offset: u64, _size: u64) -> Result<()> {
867        Err(GpuError::Unsupported("Bindless"))
868    }
869    pub fn set_storage_texture(&self, _slot: u32, _view: &TextureView) -> Result<()> {
870        Err(GpuError::Unsupported("Bindless"))
871    }
872}
873
874// ---------------------------------------------------------------- pipelines
875
876// a stage's descriptor: the checks of docs/GPU.md "Pipeline-Konstanten", WebGPU's constants
877fn stage_json(s: &ShaderStage, model: u32, extra: &str) -> Result<String> {
878    let scan = &s.module.0.scan;
879    if !scan.entries.iter().any(|(m, n)| *m == model && n == s.entry) {
880        return Err(GpuError::Invalid("the module has no such entry point for this stage"));
881    }
882    let mut constants = Vec::with_capacity(s.constants.len());
883    for (i, c) in s.constants.iter().enumerate() {
884        let Some(o) = scan.overrides.iter().find(|o| o.0 == c.id) else {
885            return Err(GpuError::Invalid("a pipeline constant the module does not declare"));
886        };
887        if s.constants[..i].iter().any(|p| p.id == c.id) {
888            return Err(GpuError::Invalid("a pipeline constant twice"));
889        }
890        if c.value.is_nan() && o.1 != shader::reflect::Kind::Bool || c.value.is_infinite() {
891            return Err(GpuError::Invalid("a pipeline constant out of range"));
892        }
893        let key = if c.id >= 0x10000 { o.3.clone() } else { format!("{}", c.id) };
894        let value = if c.value.is_nan() { String::from("0") } else { json_f64(c.value) };
895        constants.push(format!("\"{key}\":{value}"));
896    }
897    Ok(format!("{{\"module\":{{\"$\":{}}},\"entryPoint\":\"{}\",\"constants\":{{{}}}{extra}}}", s.module.0.h.h, s.entry, constants.join(",")))
898}
899
900pub(crate) struct PipeInner {
901    pub h: Handle,
902    pub layout: PipelineLayout,
903    pub targets: Vec<Format>,
904    pub depth: Option<Format>,
905}
906
907/// a compute shader with its layout
908#[derive(Clone)]
909pub struct ComputePipeline(pub(crate) Rc<PipeInner>);
910
911/// vertex + fragment shaders with their fixed state and target formats
912#[derive(Clone)]
913pub struct RenderPipeline(pub(crate) Rc<PipeInner>);
914
915fn blend_factor(f: BlendFactor) -> &'static str {
916    match f {
917        BlendFactor::Zero => "zero",
918        BlendFactor::One => "one",
919        BlendFactor::Src => "src",
920        BlendFactor::OneMinusSrc => "one-minus-src",
921        BlendFactor::Dst => "dst",
922        BlendFactor::OneMinusDst => "one-minus-dst",
923        BlendFactor::SrcAlpha => "src-alpha",
924        BlendFactor::OneMinusSrcAlpha => "one-minus-src-alpha",
925        BlendFactor::DstAlpha => "dst-alpha",
926        BlendFactor::OneMinusDstAlpha => "one-minus-dst-alpha",
927    }
928}
929
930fn blend_component(c: BlendComponent) -> String {
931    let op = match c.op {
932        BlendOp::Add => "add",
933        BlendOp::Subtract => "subtract",
934        BlendOp::ReverseSubtract => "reverse-subtract",
935        BlendOp::Min => "min",
936        BlendOp::Max => "max",
937    };
938    format!("{{\"operation\":\"{op}\",\"srcFactor\":\"{}\",\"dstFactor\":\"{}\"}}", blend_factor(c.src), blend_factor(c.dst))
939}
940
941impl Device {
942    pub fn create_compute_pipeline(&self, desc: &ComputePipelineDesc) -> Result<ComputePipeline> {
943        self.d.check()?;
944        let layout = desc.layout.make(&[desc.shader.module])?;
945        let json = format!("{{\"layout\":{{\"$\":{layout}}},\"compute\":{}}}", stage_json(&desc.shader, 5, "")?);
946        let h = create(self.d.h, METHOD_COMPUTE_PIPELINE, &json)?;
947        Ok(ComputePipeline(Rc::new(PipeInner { h: Handle { h, destroy: false }, layout: desc.layout.clone(), targets: Vec::new(), depth: None })))
948    }
949
950    /// (a required subgroup size: not built in the browser; size 0 is the plain pipeline)
951    pub fn create_compute_pipeline_sized(&self, desc: &ComputePipelineDesc, size: u32) -> Result<ComputePipeline> {
952        if size != 0 {
953            return Err(GpuError::Unsupported("a required subgroup size in the browser"));
954        }
955        self.create_compute_pipeline(desc)
956    }
957
958    pub fn create_render_pipeline(&self, desc: &RenderPipelineDesc) -> Result<RenderPipeline> {
959        self.d.check()?;
960        let mut modules = alloc::vec![desc.vertex.module];
961        if let Some(f) = &desc.fragment {
962            modules.push(f.module);
963        }
964        let layout = desc.layout.make(&modules)?;
965        let mut buffers = Vec::with_capacity(desc.buffers.len());
966        for b in desc.buffers {
967            let attrs: Vec<String> = b.attributes.iter().map(|a| {
968                let format = match a.format {
969                    VertexFormat::F32 => "float32",
970                    VertexFormat::F32x2 => "float32x2",
971                    VertexFormat::F32x3 => "float32x3",
972                    VertexFormat::F32x4 => "float32x4",
973                    VertexFormat::U32 => "uint32",
974                    VertexFormat::U32x2 => "uint32x2",
975                    VertexFormat::U32x4 => "uint32x4",
976                    VertexFormat::Unorm8x4 => "unorm8x4",
977                };
978                format!("{{\"format\":\"{format}\",\"offset\":{},\"shaderLocation\":{}}}", a.offset, a.location)
979            }).collect();
980            buffers.push(format!("{{\"arrayStride\":{},\"stepMode\":\"{}\",\"attributes\":[{}]}}", b.stride, if b.per_instance { "instance" } else { "vertex" }, attrs.join(",")));
981        }
982        let vertex = stage_json(&desc.vertex, 0, &format!(",\"buffers\":[{}]", buffers.join(",")))?;
983        let mut json = format!("{{\"layout\":{{\"$\":{layout}}},\"vertex\":{vertex}");
984        if let Some(f) = &desc.fragment {
985            let targets: Vec<String> = desc.targets.iter().map(|t| {
986                let blend = t.blend.map_or(String::new(), |b| format!(",\"blend\":{{\"color\":{},\"alpha\":{}}}", blend_component(b.color), blend_component(b.alpha)));
987                format!("{{\"format\":\"{}\",\"writeMask\":{}{blend}}}", format_name(t.format), t.write_mask & 0xF)
988            }).collect();
989            json.push_str(&format!(",\"fragment\":{}", stage_json(f, 4, &format!(",\"targets\":[{}]", targets.join(",")))?));
990        }
991        let topology = match desc.topology {
992            Topology::Points => "point-list",
993            Topology::Lines => "line-list",
994            Topology::LineStrip => "line-strip",
995            Topology::Triangles => "triangle-list",
996            Topology::TriangleStrip => "triangle-strip",
997        };
998        let cull = match desc.cull {
999            Cull::None => "none",
1000            Cull::Front => "front",
1001            Cull::Back => "back",
1002        };
1003        json.push_str(&format!(",\"primitive\":{{\"topology\":\"{topology}\",\"cullMode\":\"{cull}\",\"frontFace\":\"{}\"}}", if desc.front_cw { "cw" } else { "ccw" }));
1004        if let Some(d) = desc.depth {
1005            json.push_str(&format!(",\"depthStencil\":{{\"format\":\"{}\",\"depthWriteEnabled\":{},\"depthCompare\":\"{}\"}}", format_name(d.format), d.write, compare_name(d.compare)));
1006        }
1007        json.push('}');
1008        let h = create(self.d.h, METHOD_RENDER_PIPELINE, &json)?;
1009        Ok(RenderPipeline(Rc::new(PipeInner {
1010            h: Handle { h, destroy: false },
1011            layout: desc.layout.clone(),
1012            targets: desc.targets.iter().map(|t| t.format).collect(),
1013            depth: desc.depth.map(|d| d.format),
1014        })))
1015    }
1016
1017    /// (not built in the browser: Features::timestamps is false; WebGPU has them only at the start and
1018    /// the end of a pass)
1019    pub fn create_timestamps(&self, _count: u32) -> Result<Timestamps> {
1020        Err(GpuError::Unsupported("timestamps in the browser"))
1021    }
1022}
1023
1024/// GPU timestamps (not built in the browser)
1025pub struct Timestamps {
1026    _none: (),
1027}
1028
1029impl Timestamps {
1030    pub fn count(&self) -> u32 {
1031        0
1032    }
1033    pub fn read_ticks(&self, _first: u32, _n: u32) -> Result<Vec<u64>> {
1034        Err(GpuError::Unsupported("timestamps in the browser"))
1035    }
1036    pub fn read_ms(&self, _first: u32, _n: u32) -> Result<Vec<f64>> {
1037        Err(GpuError::Unsupported("timestamps in the browser"))
1038    }
1039}