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