Skip to main content

gasm_host/
gfx.rs

1//! gasm:gfx on wgpu, plus the 2D `video_present` blit.
2//!
3//! Handles index a single object table, numbered in creation order and never
4//! reused (`destroy` leaves a tombstone). Creation descriptors are JSON that
5//! mirrors WebGPU (see spec/ABI.md#gasmgfx).
6//!
7//! Every object has a [`Meta`] record (kind, sizes, layouts, vertex layout) and
8//! the render pass state (pipeline, bind groups, vertex and index buffers) is
9//! tracked the same way with and without a GPU, so handles, ranges, usages,
10//! bind group compatibility and draw ranges are validated (and trap) identically
11//! on the null GPU, the real GPU and the JS runner (`GfxModel` in gasm-host.js).
12//! What only the GPU can check (WGSL, pipeline/shader interface) is caught with
13//! wgpu error scopes and becomes `Err`, which traps the guest.
14
15use std::sync::Arc;
16
17use serde_json::Value;
18use crate::present::{Present, Presenter, VideoFrame};
19
20pub const SAMPLE_COUNT: u32 = 4;
21const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
22
23/// Drawable size of headless runs (null GPU and offscreen screenshots).
24pub const HEADLESS_SIZE: (u32, u32) = (1280, 720);
25/// Largest texture side (WebGPU's default `maxTextureDimension2D`).
26pub const MAX_TEXTURE_SIZE: u32 = 8192;
27/// Dynamic offsets must be multiples of this (WebGPU's default alignment limits).
28pub const OFFSET_ALIGNMENT: u32 = 256;
29/// WebGPU default limits used for validation.
30pub const MAX_BIND_GROUPS: u32 = 4;
31pub const MAX_VERTEX_BUFFERS: u32 = 8;
32const MAX_VERTEX_ATTRIBUTES: usize = 16;
33const MAX_VERTEX_STRIDE: u64 = 2048;
34
35// GASM_BUF_* (spec/abi.json; WebGPU GPUBufferUsage bits)
36pub const BUF_COPY_DST: u32 = 0x08;
37pub const BUF_INDEX: u32 = 0x10;
38pub const BUF_VERTEX: u32 = 0x20;
39pub const BUF_UNIFORM: u32 = 0x40;
40pub const BUF_STORAGE: u32 = 0x80;
41
42enum Obj {
43    Shader(wgpu::ShaderModule),
44    Buffer(wgpu::Buffer),
45    Pipeline(wgpu::RenderPipeline),
46    BindGroup(wgpu::BindGroup),
47    Layout(wgpu::BindGroupLayout),
48    Texture(wgpu::Texture, wgpu::TextureView),
49    Sampler(wgpu::Sampler),
50    /// null GPU, or destroyed
51    Null,
52}
53
54/// Backend-independent description of an object, used for validation.
55#[derive(Clone)]
56enum Meta {
57    Shader,
58    Buffer { size: u64, usage: u32 },
59    Pipeline(PipelineMeta),
60    BindGroup { dynamic: Vec<DynEntry>, layout: GroupLayout },
61    Layout { entries: Vec<LayoutEntry> },
62    Texture { width: u32, height: u32, mips: u32 },
63    Sampler,
64    Destroyed(&'static str),
65}
66
67impl Meta {
68    fn kind(&self) -> &'static str {
69        match self {
70            Meta::Shader => "shader",
71            Meta::Buffer { .. } => "buffer",
72            Meta::Pipeline(_) => "pipeline",
73            Meta::BindGroup { .. } => "bind group",
74            Meta::Layout { .. } => "bind group layout",
75            Meta::Texture { .. } => "texture",
76            Meta::Sampler => "sampler",
77            Meta::Destroyed(_) => "destroyed object",
78        }
79    }
80}
81
82#[derive(Clone)]
83struct PipelineMeta {
84    /// explicit layouts: the entries of each group; None = "auto"
85    groups: Option<Vec<Vec<LayoutEntry>>>,
86    vertex: Vec<VertexSlot>,
87    /// color targets of the fragment stage (the pass has exactly one)
88    targets: usize,
89}
90
91/// One vertex buffer of a pipeline.
92#[derive(Clone, Copy)]
93struct VertexSlot {
94    stride: u64,
95    instance: bool,
96    /// bytes the last element reads: max(attribute offset + size)
97    last: u64,
98}
99
100/// What a bind group was created against.
101#[derive(Clone)]
102enum GroupLayout {
103    Explicit(Vec<LayoutEntry>),
104    /// from a pipeline with layout "auto": only compatible with that pipeline
105    Auto { pipeline: u32, group: u32 },
106}
107
108#[derive(Clone, Copy)]
109struct DynEntry {
110    buffer_size: u64,
111    offset: u64,
112    size: u64,
113}
114
115#[derive(Clone, Copy, PartialEq)]
116enum Slot {
117    Uniform,
118    Storage,
119    Texture,
120    Sampler,
121}
122
123#[derive(Clone, PartialEq)]
124struct LayoutEntry {
125    binding: u32,
126    visibility: u32,
127    slot: Slot,
128    dynamic: bool,
129    min_binding_size: u64,
130    /// texture: "float" (filterable) or "unfilterable-float"; sampler: "filtering" or "non-filtering"
131    filterable: bool,
132}
133
134/// Render pass state between begin_frame and end_frame, tracked in every mode.
135#[derive(Default)]
136struct Pass {
137    pipeline: Option<u32>,
138    groups: [Option<u32>; MAX_BIND_GROUPS as usize],
139    vertex: [Option<(u32, u64)>; MAX_VERTEX_BUFFERS as usize],
140    /// (buffer, index size in bytes, offset)
141    index: Option<(u32, u64, u64)>,
142}
143
144pub struct Gpu {
145    pub device: wgpu::Device,
146    pub queue: wgpu::Queue,
147    pub format: wgpu::TextureFormat,
148}
149
150pub enum Target {
151    Window { surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration },
152    Offscreen { texture: wgpu::Texture },
153}
154
155/// GPU work of a shown frame.
156struct Frame {
157    pass: wgpu::RenderPass<'static>,
158    encoder: wgpu::CommandEncoder,
159    surface_texture: Option<wgpu::SurfaceTexture>,
160    /// viewport or scissor clamped to nothing: draws are skipped
161    empty_viewport: bool,
162    empty_scissor: bool,
163}
164
165struct Object {
166    obj: Obj,
167    meta: Meta,
168}
169
170pub struct Gfx {
171    gpu: Option<Gpu>,
172    target: Option<Target>,
173    width: u32,
174    height: u32,
175    objects: Vec<Object>,
176    msaa: Option<wgpu::TextureView>,
177    depth: Option<wgpu::TextureView>,
178    /// between begin_frame and end_frame (shown or not)
179    pass: Option<Pass>,
180    frame: Option<Frame>,
181    presenter: Option<Presenter>,
182    /// how video_present frames are shown (filter, integer scaling)
183    pub present: Present,
184    /// set by the guest calling any gfx draw API this frame (suppresses the 2D blit)
185    pub used: bool,
186}
187
188type R<T> = Result<T, String>;
189
190fn add_u64(a: u64, b: u64, what: &str) -> R<u64> {
191    a.checked_add(b).ok_or_else(|| format!("gfx: {what} overflows"))
192}
193
194impl Gfx {
195    fn new(gpu: Option<Gpu>, target: Option<Target>, width: u32, height: u32) -> Gfx {
196        let presenter = gpu.as_ref().map(Presenter::new);
197        Gfx { gpu, target, width, height, objects: Vec::new(), msaa: None, depth: None, pass: None, frame: None, presenter, present: Present::default(), used: false }
198    }
199
200    /// No GPU: ids and validation only (headless runs without screenshots, CI).
201    pub fn null() -> Gfx {
202        Gfx::new(None, None, HEADLESS_SIZE.0, HEADLESS_SIZE.1)
203    }
204
205    /// Render into a window.
206    #[cfg(feature = "window")]
207    pub fn for_window(window: Arc<winit::window::Window>) -> R<Gfx> {
208        let size = window.inner_size();
209        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
210        let surface = instance.create_surface(window).map_err(|e| e.to_string())?;
211        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
212            power_preference: wgpu::PowerPreference::HighPerformance,
213            compatible_surface: Some(&surface),
214            ..Default::default()
215        }))
216        .map_err(|e| format!("no GPU adapter: {e}"))?;
217        let gpu = open_device(&adapter, pick_format, &surface)?;
218        let mut config = surface
219            .get_default_config(&adapter, size.width.max(1), size.height.max(1))
220            .ok_or("surface not supported by adapter")?;
221        config.format = gpu.format;
222        config.present_mode = wgpu::PresentMode::AutoVsync;
223        surface.configure(&gpu.device, &config);
224        eprintln!("[gasm] gpu: {} ({:?}), surface {:?}", adapter.get_info().name, adapter.get_info().backend, gpu.format);
225        let mut g = Gfx::new(Some(gpu), Some(Target::Window { surface, config }), size.width.max(1), size.height.max(1));
226        g.recreate_attachments();
227        Ok(g)
228    }
229
230    /// Render into an RGBA texture that can be read back (headless screenshots).
231    pub fn offscreen(width: u32, height: u32) -> R<Gfx> {
232        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
233        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
234            .map_err(|e| format!("no GPU adapter: {e}"))?;
235        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
236            .map_err(|e| e.to_string())?;
237        device.on_uncaptured_error(Arc::new(|e| eprintln!("[gasm] gpu error: {e}")));
238        let format = wgpu::TextureFormat::Rgba8Unorm;
239        let texture = device.create_texture(&wgpu::TextureDescriptor {
240            label: Some("offscreen"),
241            size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
242            mip_level_count: 1,
243            sample_count: 1,
244            dimension: wgpu::TextureDimension::D2,
245            format,
246            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
247            view_formats: &[],
248        });
249        let mut g = Gfx::new(Some(Gpu { device, queue, format }), Some(Target::Offscreen { texture }), width, height);
250        g.recreate_attachments();
251        Ok(g)
252    }
253
254    pub fn size(&self) -> (u32, u32) {
255        (self.width, self.height)
256    }
257
258    pub fn resize(&mut self, width: u32, height: u32) {
259        if width == 0 || height == 0 || (width, height) == (self.width, self.height) {
260            return;
261        }
262        self.width = width;
263        self.height = height;
264        if let (Some(gpu), Some(Target::Window { surface, config })) = (&self.gpu, &mut self.target) {
265            config.width = width;
266            config.height = height;
267            surface.configure(&gpu.device, config);
268        }
269        self.recreate_attachments();
270    }
271
272    fn recreate_attachments(&mut self) {
273        let Some(gpu) = &self.gpu else { return };
274        let make = |format, label| {
275            gpu.device
276                .create_texture(&wgpu::TextureDescriptor {
277                    label: Some(label),
278                    size: wgpu::Extent3d { width: self.width, height: self.height, depth_or_array_layers: 1 },
279                    mip_level_count: 1,
280                    sample_count: SAMPLE_COUNT,
281                    dimension: wgpu::TextureDimension::D2,
282                    format,
283                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
284                    view_formats: &[],
285                })
286                .create_view(&Default::default())
287        };
288        self.msaa = Some(make(gpu.format, "msaa"));
289        self.depth = Some(make(DEPTH_FORMAT, "depth"));
290    }
291
292    // ---- object table ------------------------------------------------------------
293
294    fn add(&mut self, obj: Obj, meta: Meta) -> u32 {
295        self.objects.push(Object { obj, meta });
296        self.objects.len() as u32
297    }
298
299    fn object(&self, h: u32) -> R<&Object> {
300        let o = self.objects.get((h as usize).wrapping_sub(1)).ok_or_else(|| format!("gfx: invalid handle {h}"))?;
301        if let Meta::Destroyed(kind) = o.meta {
302            return Err(format!("gfx: {kind} {h} was destroyed"));
303        }
304        Ok(o)
305    }
306
307    fn meta(&self, h: u32) -> R<&Meta> {
308        Ok(&self.object(h)?.meta)
309    }
310
311    /// Check that `h` is a live object of kind `want` (see [`Meta::kind`]).
312    fn expect(&self, h: u32, want: &str) -> R<&Meta> {
313        let m = self.meta(h)?;
314        if m.kind() != want {
315            return Err(format!("gfx: handle {h} is a {}, not a {want}", m.kind()));
316        }
317        Ok(m)
318    }
319
320    fn buffer_meta(&self, h: u32) -> R<(u64, u32)> {
321        match self.expect(h, "buffer")? {
322            Meta::Buffer { size, usage } => Ok((*size, *usage)),
323            _ => unreachable!(),
324        }
325    }
326
327    fn pipeline_meta(&self, h: u32) -> R<&PipelineMeta> {
328        match self.expect(h, "pipeline")? {
329            Meta::Pipeline(p) => Ok(p),
330            _ => unreachable!(),
331        }
332    }
333
334    fn obj(&self, h: u32) -> R<&Obj> {
335        Ok(&self.object(h)?.obj)
336    }
337
338    fn shader(&self, h: u32) -> R<&wgpu::ShaderModule> {
339        match self.obj(h)? { Obj::Shader(s) => Ok(s), _ => Err(format!("gfx: handle {h} is not a shader")) }
340    }
341    fn buffer(&self, h: u32) -> R<&wgpu::Buffer> {
342        match self.obj(h)? { Obj::Buffer(b) => Ok(b), _ => Err(format!("gfx: handle {h} is not a buffer")) }
343    }
344    fn pipeline(&self, h: u32) -> R<&wgpu::RenderPipeline> {
345        match self.obj(h)? { Obj::Pipeline(p) => Ok(p), _ => Err(format!("gfx: handle {h} is not a pipeline")) }
346    }
347    fn bind_group(&self, h: u32) -> R<&wgpu::BindGroup> {
348        match self.obj(h)? { Obj::BindGroup(b) => Ok(b), _ => Err(format!("gfx: handle {h} is not a bind group")) }
349    }
350    fn layout(&self, h: u32) -> R<&wgpu::BindGroupLayout> {
351        match self.obj(h)? { Obj::Layout(l) => Ok(l), _ => Err(format!("gfx: handle {h} is not a bind group layout")) }
352    }
353
354    /// Release an object: the handle becomes a tombstone (never reused). wgpu
355    /// keeps the resource alive while bind groups or a recorded pass use it.
356    pub fn destroy(&mut self, h: u32) -> R<()> {
357        let kind = self.meta(h)?.kind();
358        let o = &mut self.objects[h as usize - 1];
359        o.obj = Obj::Null;
360        o.meta = Meta::Destroyed(kind);
361        Ok(())
362    }
363
364    /// Run `f` inside a validation error scope; wgpu errors become `Err`.
365    fn scoped<T>(&self, what: &str, f: impl FnOnce(&Gpu) -> R<T>) -> R<T> {
366        let gpu = self.gpu.as_ref().ok_or("gfx: no GPU")?;
367        let scope = gpu.device.push_error_scope(wgpu::ErrorFilter::Validation);
368        let out = f(gpu);
369        if let Some(e) = pollster::block_on(scope.pop()) {
370            return Err(format!("gfx: {what}: {e}"));
371        }
372        out
373    }
374
375    fn pass_mut(&mut self, what: &str) -> R<&mut Pass> {
376        self.pass.as_mut().ok_or_else(|| format!("gfx.{what} called outside begin_frame/end_frame"))
377    }
378
379    // ---- creation ------------------------------------------------------------------------
380
381    pub fn create_shader(&mut self, wgsl: &str) -> R<u32> {
382        if self.gpu.is_none() {
383            return Ok(self.add(Obj::Null, Meta::Shader));
384        }
385        let m = self.scoped("create_shader", |gpu| {
386            Ok(gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
387                label: None,
388                source: wgpu::ShaderSource::Wgsl(wgsl.into()),
389            }))
390        })?;
391        Ok(self.add(Obj::Shader(m), Meta::Shader))
392    }
393
394    pub fn create_buffer(&mut self, size: u32, usage: u32) -> R<u32> {
395        if size == 0 || size % 4 != 0 {
396            return Err(format!("gfx.create_buffer: size {size} must be a non-zero multiple of 4"));
397        }
398        let meta = Meta::Buffer { size: size as u64, usage: usage | BUF_COPY_DST };
399        let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, meta)) };
400        // WebGPU usage bits == wgpu BufferUsages bits; COPY_DST is always added for write_buffer.
401        let usage = wgpu::BufferUsages::from_bits_truncate(usage) | wgpu::BufferUsages::COPY_DST;
402        let b = gpu.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: size as u64, usage, mapped_at_creation: false });
403        Ok(self.add(Obj::Buffer(b), meta))
404    }
405
406    pub fn write_buffer(&mut self, h: u32, offset: u32, data: &[u8]) -> R<()> {
407        if offset % 4 != 0 || data.len() % 4 != 0 {
408            return Err(format!("gfx.write_buffer: offset {offset} and length {} must be multiples of 4", data.len()));
409        }
410        let (size, _) = self.buffer_meta(h)?;
411        if offset as u64 + data.len() as u64 > size {
412            return Err(format!("gfx.write_buffer: {}+{} exceeds buffer size {size}", offset, data.len()));
413        }
414        if let Some(gpu) = &self.gpu {
415            gpu.queue.write_buffer(self.buffer(h)?, offset as u64, data);
416        }
417        Ok(())
418    }
419
420    pub fn create_pipeline(&mut self, json: &str) -> R<u32> {
421        let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_pipeline: invalid JSON: {e}"))?;
422        let layouts = self.pipeline_layouts(&d)?;
423        let v = d.get("vertex").ok_or("pipeline: missing vertex")?;
424        self.expect(uint(v, "module")?, "shader")?;
425        let f = d.get("fragment");
426        if let Some(f) = f {
427            self.expect(uint(f, "module")?, "shader")?;
428        }
429        let meta = PipelineMeta {
430            groups: match &layouts {
431                Some(hs) => Some(hs.iter().map(|h| self.layout_entries(*h)).collect::<R<Vec<_>>>()?),
432                None => None,
433            },
434            vertex: vertex_slots(v)?,
435            targets: f.and_then(|f| f.get("targets")).and_then(Value::as_array).map_or(0, Vec::len),
436        };
437        Ok(match self.build_pipeline(&d, layouts)? {
438            Some(p) => self.add(Obj::Pipeline(p), Meta::Pipeline(meta)),
439            None => self.add(Obj::Null, Meta::Pipeline(meta)),
440        })
441    }
442
443    fn layout_entries(&self, h: u32) -> R<Vec<LayoutEntry>> {
444        match self.expect(h, "bind group layout")? {
445            Meta::Layout { entries } => Ok(entries.clone()),
446            _ => unreachable!(),
447        }
448    }
449
450    /// `"layout"`: omitted / `"auto"` (None), or an array of bind group layout handles.
451    fn pipeline_layouts(&self, d: &Value) -> R<Option<Vec<u32>>> {
452        match d.get("layout") {
453            None => Ok(None),
454            Some(Value::String(s)) if s == "auto" => Ok(None),
455            Some(Value::Array(a)) => {
456                if a.len() > MAX_BIND_GROUPS as usize {
457                    return Err(format!("pipeline: at most {MAX_BIND_GROUPS} bind group layouts, got {}", a.len()));
458                }
459                let mut out = Vec::new();
460                for v in a {
461                    let h = v.as_u64().and_then(|h| u32::try_from(h).ok()).ok_or("pipeline: layout entries must be bind group layout handles")?;
462                    self.expect(h, "bind group layout")?;
463                    out.push(h);
464                }
465                Ok(Some(out))
466            }
467            Some(o) => Err(format!("pipeline: layout must be \"auto\" or an array of bind group layout handles, got {o}")),
468        }
469    }
470
471    pub fn create_bind_group(&mut self, json: &str) -> R<u32> {
472        #[derive(PartialEq)]
473        enum Res {
474            Buffer,
475            Texture,
476            Sampler,
477        }
478        let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_bind_group: invalid JSON: {e}"))?;
479        let entries_json = d.get("entries").and_then(Value::as_array).cloned().unwrap_or_default();
480        // Resolve and check every entry against the object table (both modes).
481        let mut resolved: Vec<(u32, Res, u32, u64, Option<u64>)> = Vec::new(); // (binding, resource, handle, offset, size)
482        for e in &entries_json {
483            let binding = uint(e, "binding")?;
484            if resolved.iter().any(|r| r.0 == binding) {
485                return Err(format!("bind group: binding {binding} appears twice"));
486            }
487            if let Some(h) = e.get("buffer") {
488                let h = handle(h)?;
489                let (size, _) = self.buffer_meta(h)?;
490                let offset = e.get("offset").and_then(Value::as_u64).unwrap_or(0);
491                let bsize = e.get("size").and_then(Value::as_u64);
492                if offset > size || add_u64(offset, bsize.unwrap_or(0), "bind group range")? > size || bsize == Some(0) {
493                    return Err(format!("bind group: binding {binding}: range {offset}+{} exceeds buffer size {size}", bsize.unwrap_or(0)));
494                }
495                resolved.push((binding, Res::Buffer, h, offset, bsize));
496            } else if let Some(h) = e.get("texture") {
497                let h = handle(h)?;
498                self.expect(h, "texture")?;
499                resolved.push((binding, Res::Texture, h, 0, None));
500            } else if let Some(h) = e.get("sampler") {
501                let h = handle(h)?;
502                self.expect(h, "sampler")?;
503                resolved.push((binding, Res::Sampler, h, 0, None));
504            } else {
505                return Err(format!("bind group: binding {binding} needs a \"buffer\", \"texture\" or \"sampler\""));
506            }
507        }
508        let mut dynamic = Vec::new();
509        let explicit = match d.get("layout") {
510            Some(h) => Some(handle(h)?),
511            None => None,
512        };
513        // the layout's entries: an explicit layout, or group G of a pipeline with explicit layouts
514        let from_pipeline = match explicit {
515            Some(_) => None,
516            None => {
517                let p = uint(&d, "pipeline").map_err(|_| "bind group: needs \"layout\" or \"pipeline\"".to_string())?;
518                let group = d.get("group").and_then(Value::as_u64).unwrap_or(0);
519                if group >= MAX_BIND_GROUPS as u64 {
520                    return Err(format!("bind group: group {group} must be below {MAX_BIND_GROUPS}"));
521                }
522                Some((p, group as u32))
523            }
524        };
525        let explicit_entries = match (explicit, from_pipeline) {
526            (Some(lh), _) => Some(self.layout_entries(lh)?),
527            (None, Some((p, g))) => match &self.pipeline_meta(p)?.groups {
528                Some(groups) => Some(groups.get(g as usize).cloned().ok_or_else(|| format!("bind group: pipeline {p} has no group {g}"))?),
529                None => None,
530            },
531            _ => unreachable!(),
532        };
533        let layout_meta = if let Some(entries) = explicit_entries {
534            let lh = explicit.map_or_else(|| "of the pipeline".to_string(), |h| h.to_string());
535            if resolved.len() != entries.len() {
536                return Err(format!("bind group: layout {lh} has {} entries, got {}", entries.len(), resolved.len()));
537            }
538            for le in &entries {
539                let r = resolved.iter().find(|r| r.0 == le.binding).ok_or_else(|| format!("bind group: missing binding {}", le.binding))?;
540                let want = match le.slot {
541                    Slot::Uniform | Slot::Storage => Res::Buffer,
542                    Slot::Texture => Res::Texture,
543                    Slot::Sampler => Res::Sampler,
544                };
545                if r.1 != want {
546                    return Err(format!("bind group: binding {} has the wrong resource kind for its layout", le.binding));
547                }
548                if want == Res::Buffer {
549                    let (size, usage) = self.buffer_meta(r.2)?;
550                    let need = if le.slot == Slot::Uniform { BUF_UNIFORM } else { BUF_STORAGE };
551                    if usage & need == 0 {
552                        return Err(format!("bind group: binding {}: buffer {} lacks {} usage", le.binding, r.2, if need == BUF_UNIFORM { "UNIFORM" } else { "STORAGE" }));
553                    }
554                    let bsize = r.4.unwrap_or(size - r.3);
555                    if bsize < le.min_binding_size {
556                        return Err(format!("bind group: binding {}: size {bsize} is below minBindingSize {}", le.binding, le.min_binding_size));
557                    }
558                    if le.dynamic {
559                        dynamic.push((le.binding, DynEntry { buffer_size: size, offset: r.3, size: bsize }));
560                    }
561                }
562            }
563            dynamic.sort_by_key(|d| d.0);
564            GroupLayout::Explicit(entries)
565        } else {
566            let (pipeline, group) = from_pipeline.unwrap();
567            GroupLayout::Auto { pipeline, group }
568        };
569        let meta = Meta::BindGroup { dynamic: dynamic.into_iter().map(|d| d.1).collect(), layout: layout_meta };
570        if self.gpu.is_none() {
571            return Ok(self.add(Obj::Null, meta));
572        }
573        let layout = match explicit {
574            Some(lh) => self.layout(lh)?.clone(),
575            None => self.pipeline(uint(&d, "pipeline")?)?.get_bind_group_layout(d.get("group").and_then(Value::as_u64).unwrap_or(0) as u32),
576        };
577        let mut entries = Vec::new();
578        for (binding, res, h, offset, size) in &resolved {
579            let resource = match res {
580                Res::Texture => match self.obj(*h)? { Obj::Texture(_, v) => wgpu::BindingResource::TextureView(v), _ => unreachable!() },
581                Res::Sampler => match self.obj(*h)? { Obj::Sampler(s) => wgpu::BindingResource::Sampler(s), _ => unreachable!() },
582                Res::Buffer => wgpu::BindingResource::Buffer(wgpu::BufferBinding {
583                    buffer: self.buffer(*h)?,
584                    offset: *offset,
585                    size: size.and_then(wgpu::BufferSize::new),
586                }),
587            };
588            entries.push(wgpu::BindGroupEntry { binding: *binding, resource });
589        }
590        let bg = self.scoped("create_bind_group", |gpu| {
591            Ok(gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &layout, entries: &entries }))
592        })?;
593        Ok(self.add(Obj::BindGroup(bg), meta))
594    }
595    pub fn create_bind_group_layout(&mut self, json: &str) -> R<u32> {
596        let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_bind_group_layout: invalid JSON: {e}"))?;
597        let mut entries: Vec<LayoutEntry> = Vec::new();
598        for e in d.get("entries").and_then(Value::as_array).cloned().unwrap_or_default() {
599            let binding = uint(&e, "binding")?;
600            if entries.iter().any(|x| x.binding == binding) {
601                return Err(format!("bind group layout: binding {binding} appears twice"));
602            }
603            let visibility = uint(&e, "visibility")?;
604            if visibility == 0 || visibility & !3 != 0 {
605                return Err(format!("bind group layout: binding {binding}: visibility {visibility} must be GASM_STAGE_VERTEX (1) and/or GASM_STAGE_FRAGMENT (2)"));
606            }
607            let entry = if let Some(b) = e.get("buffer") {
608                let slot = match b.get("type").and_then(Value::as_str).unwrap_or("uniform") {
609                    "uniform" => Slot::Uniform,
610                    "read-only-storage" => Slot::Storage,
611                    o => return Err(format!("bind group layout: binding {binding}: unsupported buffer type {o:?}")),
612                };
613                LayoutEntry {
614                    binding, visibility, slot,
615                    dynamic: b.get("hasDynamicOffset").and_then(Value::as_bool).unwrap_or(false),
616                    min_binding_size: b.get("minBindingSize").and_then(Value::as_u64).unwrap_or(0),
617                    filterable: false,
618                }
619            } else if let Some(t) = e.get("texture") {
620                let filterable = match t.get("sampleType").and_then(Value::as_str).unwrap_or("float") {
621                    "float" => true,
622                    "unfilterable-float" => false,
623                    o => return Err(format!("bind group layout: binding {binding}: unsupported sampleType {o:?}")),
624                };
625                if let Some(v) = t.get("viewDimension").and_then(Value::as_str)
626                    && v != "2d"
627                {
628                    return Err(format!("bind group layout: binding {binding}: only viewDimension \"2d\" is supported, got {v:?}"));
629                }
630                if t.get("multisampled").and_then(Value::as_bool).unwrap_or(false) {
631                    return Err(format!("bind group layout: binding {binding}: multisampled textures are not supported"));
632                }
633                LayoutEntry { binding, visibility, slot: Slot::Texture, dynamic: false, min_binding_size: 0, filterable }
634            } else if let Some(sm) = e.get("sampler") {
635                let filterable = match sm.get("type").and_then(Value::as_str).unwrap_or("filtering") {
636                    "filtering" => true,
637                    "non-filtering" => false,
638                    o => return Err(format!("bind group layout: binding {binding}: unsupported sampler type {o:?}")),
639                };
640                LayoutEntry { binding, visibility, slot: Slot::Sampler, dynamic: false, min_binding_size: 0, filterable }
641            } else {
642                return Err(format!("bind group layout: binding {binding} needs \"buffer\", \"texture\" or \"sampler\""));
643            };
644            entries.push(entry);
645        }
646        entries.sort_by_key(|e| e.binding);
647        let meta = Meta::Layout { entries: entries.clone() };
648        if self.gpu.is_none() {
649            return Ok(self.add(Obj::Null, meta));
650        }
651        let wentries: Vec<wgpu::BindGroupLayoutEntry> = entries
652            .iter()
653            .map(|e| wgpu::BindGroupLayoutEntry {
654                binding: e.binding,
655                visibility: wgpu::ShaderStages::from_bits_truncate(e.visibility),
656                ty: match e.slot {
657                    Slot::Uniform | Slot::Storage => wgpu::BindingType::Buffer {
658                        ty: if e.slot == Slot::Uniform {
659                            wgpu::BufferBindingType::Uniform
660                        } else {
661                            wgpu::BufferBindingType::Storage { read_only: true }
662                        },
663                        has_dynamic_offset: e.dynamic,
664                        min_binding_size: wgpu::BufferSize::new(e.min_binding_size),
665                    },
666                    Slot::Texture => wgpu::BindingType::Texture {
667                        sample_type: wgpu::TextureSampleType::Float { filterable: e.filterable },
668                        view_dimension: wgpu::TextureViewDimension::D2,
669                        multisampled: false,
670                    },
671                    Slot::Sampler => wgpu::BindingType::Sampler(if e.filterable {
672                        wgpu::SamplerBindingType::Filtering
673                    } else {
674                        wgpu::SamplerBindingType::NonFiltering
675                    }),
676                },
677                count: None,
678            })
679            .collect();
680        let l = self.scoped("create_bind_group_layout", |gpu| {
681            Ok(gpu.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries: &wentries }))
682        })?;
683        Ok(self.add(Obj::Layout(l), meta))
684    }
685
686    pub fn create_texture(&mut self, json: &str) -> R<u32> {
687        let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_texture: invalid JSON: {e}"))?;
688        let size = d.get("size").and_then(Value::as_array).ok_or("texture: missing \"size\": [width, height]")?;
689        let dim = |i: usize| size.get(i).and_then(Value::as_u64).unwrap_or(0);
690        let (w, h) = (dim(0), dim(1));
691        if size.len() != 2 || w == 0 || h == 0 || w > MAX_TEXTURE_SIZE as u64 || h > MAX_TEXTURE_SIZE as u64 {
692            return Err(format!("texture: size must be [width, height], each 1-{MAX_TEXTURE_SIZE}"));
693        }
694        let (w, h) = (w as u32, h as u32);
695        let format = match d.get("format").and_then(Value::as_str).unwrap_or("rgba8unorm") {
696            "rgba8unorm" => wgpu::TextureFormat::Rgba8Unorm,
697            "rgba8unorm-srgb" => wgpu::TextureFormat::Rgba8UnormSrgb,
698            o => return Err(format!("texture: unsupported format {o:?} (rgba8unorm, rgba8unorm-srgb)")),
699        };
700        let max_mips = 32 - w.max(h).leading_zeros();
701        let mips = d.get("mipLevelCount").and_then(Value::as_u64).unwrap_or(1);
702        if mips == 0 || mips > max_mips as u64 {
703            return Err(format!("texture: mipLevelCount {mips} must be 1-{max_mips} for {w}x{h}"));
704        }
705        let meta = Meta::Texture { width: w, height: h, mips: mips as u32 };
706        let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, meta)) };
707        let t = gpu.device.create_texture(&wgpu::TextureDescriptor {
708            label: None,
709            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
710            mip_level_count: mips as u32,
711            sample_count: 1,
712            dimension: wgpu::TextureDimension::D2,
713            format,
714            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
715            view_formats: &[],
716        });
717        let view = t.create_view(&Default::default());
718        Ok(self.add(Obj::Texture(t, view), meta))
719    }
720
721    #[allow(clippy::too_many_arguments)]
722    pub fn write_texture(&mut self, h: u32, mip: u32, x: u32, y: u32, w: u32, ht: u32, data: &[u8]) -> R<()> {
723        let Meta::Texture { width, height, mips } = *self.expect(h, "texture")? else { unreachable!() };
724        if mip >= mips {
725            return Err(format!("gfx.write_texture: mip {mip} out of range (texture has {mips})"));
726        }
727        let (lw, lh) = ((width >> mip).max(1), (height >> mip).max(1));
728        if w == 0 || ht == 0 || x as u64 + w as u64 > lw as u64 || y as u64 + ht as u64 > lh as u64 {
729            return Err(format!("gfx.write_texture: region {x},{y} {w}x{ht} is outside mip {mip} ({lw}x{lh})"));
730        }
731        if data.len() as u64 != w as u64 * ht as u64 * 4 {
732            return Err(format!("gfx.write_texture: len {} must be width*height*4 = {}", data.len(), w as u64 * ht as u64 * 4));
733        }
734        let (Some(gpu), Obj::Texture(t, _)) = (&self.gpu, self.obj(h)?) else { return Ok(()) };
735        gpu.queue.write_texture(
736            wgpu::TexelCopyTextureInfo { texture: t, mip_level: mip, origin: wgpu::Origin3d { x, y, z: 0 }, aspect: wgpu::TextureAspect::All },
737            data,
738            wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(ht) },
739            wgpu::Extent3d { width: w, height: ht, depth_or_array_layers: 1 },
740        );
741        Ok(())
742    }
743
744    pub fn create_sampler(&mut self, json: &str) -> R<u32> {
745        let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_sampler: invalid JSON: {e}"))?;
746        let address = |k: &str| -> R<wgpu::AddressMode> {
747            Ok(match d.get(k).and_then(Value::as_str).unwrap_or("clamp-to-edge") {
748                "clamp-to-edge" => wgpu::AddressMode::ClampToEdge,
749                "repeat" => wgpu::AddressMode::Repeat,
750                "mirror-repeat" => wgpu::AddressMode::MirrorRepeat,
751                o => return Err(format!("sampler: unsupported {k} {o:?}")),
752            })
753        };
754        let linear = |k: &str| -> R<bool> {
755            match d.get(k).and_then(Value::as_str).unwrap_or("nearest") {
756                "nearest" => Ok(false),
757                "linear" => Ok(true),
758                o => Err(format!("sampler: unsupported {k} {o:?}")),
759            }
760        };
761        let (u, v) = (address("addressModeU")?, address("addressModeV")?);
762        let (mag, min, mip) = (linear("magFilter")?, linear("minFilter")?, linear("mipmapFilter")?);
763        let lod_min = d.get("lodMinClamp").and_then(Value::as_f64).unwrap_or(0.0) as f32;
764        let lod_max = d.get("lodMaxClamp").and_then(Value::as_f64).unwrap_or(32.0) as f32;
765        if !(lod_min >= 0.0 && lod_max >= lod_min) {
766            return Err(format!("sampler: lodMinClamp {lod_min} / lodMaxClamp {lod_max} must satisfy 0 <= min <= max"));
767        }
768        let aniso = d.get("maxAnisotropy").and_then(Value::as_u64).unwrap_or(1);
769        if !(1..=16).contains(&aniso) {
770            return Err(format!("sampler: maxAnisotropy {aniso} must be 1-16"));
771        }
772        if aniso > 1 && !(mag && min && mip) {
773            return Err("sampler: maxAnisotropy > 1 needs linear magFilter, minFilter and mipmapFilter".into());
774        }
775        let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, Meta::Sampler)) };
776        let f = |l: bool| if l { wgpu::FilterMode::Linear } else { wgpu::FilterMode::Nearest };
777        let smp = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
778            label: None,
779            address_mode_u: u,
780            address_mode_v: v,
781            address_mode_w: wgpu::AddressMode::ClampToEdge,
782            mag_filter: f(mag),
783            min_filter: f(min),
784            mipmap_filter: if mip { wgpu::MipmapFilterMode::Linear } else { wgpu::MipmapFilterMode::Nearest },
785            lod_min_clamp: lod_min,
786            lod_max_clamp: lod_max,
787            compare: None,
788            anisotropy_clamp: aniso as u16,
789            border_color: None,
790        });
791        Ok(self.add(Obj::Sampler(smp), Meta::Sampler))
792    }
793
794    /// Parse and validate a pipeline descriptor (both modes); with a GPU, create it.
795    fn build_pipeline(&self, d: &Value, group_layouts: Option<Vec<u32>>) -> R<Option<wgpu::RenderPipeline>> {
796        let surface = self.gpu.as_ref().map_or(wgpu::TextureFormat::Bgra8Unorm, |g| g.format);
797        let v = d.get("vertex").ok_or("pipeline: missing vertex")?;
798        self.expect(uint(v, "module")?, "shader")?;
799        let buffers_json = v.get("buffers").and_then(Value::as_array).cloned().unwrap_or_default();
800        let mut attrs: Vec<Vec<wgpu::VertexAttribute>> = Vec::new();
801        for b in &buffers_json {
802            let mut list = Vec::new();
803            for a in b.get("attributes").and_then(Value::as_array).cloned().unwrap_or_default() {
804                list.push(wgpu::VertexAttribute {
805                    format: vertex_format(string(&a, "format")?)?,
806                    offset: a.get("offset").and_then(Value::as_u64).unwrap_or(0),
807                    shader_location: uint(&a, "shaderLocation")?,
808                });
809            }
810            attrs.push(list);
811        }
812        let layouts: Vec<Option<wgpu::VertexBufferLayout>> = buffers_json
813            .iter()
814            .zip(&attrs)
815            .map(|(b, a)| {
816                Some(wgpu::VertexBufferLayout {
817                    array_stride: b.get("arrayStride").and_then(Value::as_u64).unwrap_or(0),
818                    step_mode: match b.get("stepMode").and_then(Value::as_str) {
819                        Some("instance") => wgpu::VertexStepMode::Instance,
820                        _ => wgpu::VertexStepMode::Vertex, // other values rejected by vertex_slots
821                    },
822                    attributes: a,
823                })
824            })
825            .collect();
826
827        let mut targets = Vec::new();
828        let fragment_module;
829        let f = d.get("fragment");
830        if let Some(f) = f {
831            fragment_module = Some(uint(f, "module")?);
832            self.expect(fragment_module.unwrap(), "shader")?;
833            for t in f.get("targets").and_then(Value::as_array).cloned().unwrap_or_default() {
834                let format = match t.get("format").and_then(Value::as_str) {
835                    Some("surface") | None => surface,
836                    Some(other) => return Err(format!("pipeline: color target format must be \"surface\", got {other:?}")),
837                };
838                let blend = match t.get("blend") {
839                    Some(b) => Some(wgpu::BlendState {
840                        color: blend_component(b.get("color"))?,
841                        alpha: blend_component(b.get("alpha"))?,
842                    }),
843                    None => None,
844                };
845                let write_mask = match t.get("writeMask").and_then(Value::as_u64) {
846                    None => wgpu::ColorWrites::ALL,
847                    Some(m) if m <= 0xf => wgpu::ColorWrites::from_bits_truncate(m as u32),
848                    Some(m) => return Err(format!("pipeline: writeMask {m} must be 0-15")),
849                };
850                targets.push(Some(wgpu::ColorTargetState { format, blend, write_mask }));
851            }
852        } else {
853            fragment_module = None;
854        }
855
856        let p = d.get("primitive");
857        let ps = |k: &str| p.and_then(|p| p.get(k)).and_then(Value::as_str);
858        let primitive = wgpu::PrimitiveState {
859            topology: match ps("topology").unwrap_or("triangle-list") {
860                "point-list" => wgpu::PrimitiveTopology::PointList,
861                "line-list" => wgpu::PrimitiveTopology::LineList,
862                "line-strip" => wgpu::PrimitiveTopology::LineStrip,
863                "triangle-list" => wgpu::PrimitiveTopology::TriangleList,
864                "triangle-strip" => wgpu::PrimitiveTopology::TriangleStrip,
865                o => return Err(format!("pipeline: unknown topology {o:?}")),
866            },
867            strip_index_format: match ps("stripIndexFormat") {
868                Some("uint16") => Some(wgpu::IndexFormat::Uint16),
869                Some("uint32") => Some(wgpu::IndexFormat::Uint32),
870                None => None,
871                Some(o) => return Err(format!("pipeline: unknown stripIndexFormat {o:?}")),
872            },
873            front_face: match ps("frontFace").unwrap_or("ccw") {
874                "cw" => wgpu::FrontFace::Cw,
875                "ccw" => wgpu::FrontFace::Ccw,
876                o => return Err(format!("pipeline: unknown frontFace {o:?}")),
877            },
878            cull_mode: match ps("cullMode").unwrap_or("none") {
879                "front" => Some(wgpu::Face::Front),
880                "back" => Some(wgpu::Face::Back),
881                "none" => None,
882                o => return Err(format!("pipeline: unknown cullMode {o:?}")),
883            },
884            ..Default::default()
885        };
886
887        let depth_stencil = match d.get("depthStencil") {
888            Some(ds) => {
889                if let Some(f) = ds.get("format").and_then(Value::as_str)
890                    && f != "depth24plus"
891                {
892                    return Err(format!("pipeline: depthStencil format must be \"depth24plus\", got {f:?}"));
893                }
894                Some(wgpu::DepthStencilState {
895                    format: DEPTH_FORMAT,
896                    depth_write_enabled: Some(ds.get("depthWriteEnabled").and_then(Value::as_bool).unwrap_or(false)),
897                    depth_compare: Some(compare(ds.get("depthCompare").and_then(Value::as_str).unwrap_or("always"))?),
898                    stencil: Default::default(),
899                    bias: wgpu::DepthBiasState {
900                        constant: ds.get("depthBias").and_then(Value::as_i64).unwrap_or(0) as i32,
901                        slope_scale: ds.get("depthBiasSlopeScale").and_then(Value::as_f64).unwrap_or(0.0) as f32,
902                        clamp: ds.get("depthBiasClamp").and_then(Value::as_f64).unwrap_or(0.0) as f32,
903                    },
904                })
905            }
906            // The render pass always has a depth attachment, so a pipeline without
907            // depthStencil gets a no-op one (no test, no writes) to stay compatible.
908            None => Some(wgpu::DepthStencilState {
909                format: DEPTH_FORMAT,
910                depth_write_enabled: Some(false),
911                depth_compare: Some(wgpu::CompareFunction::Always),
912                stencil: Default::default(),
913                bias: Default::default(),
914            }),
915        };
916
917        let entry = |s: &Value| s.get("entryPoint").and_then(Value::as_str).map(str::to_owned);
918        let (v_entry, f_entry) = (entry(v), f.and_then(entry));
919        if self.gpu.is_none() {
920            return Ok(None);
921        }
922        let vmod = self.shader(uint(v, "module")?)?;
923        let fragment_module = match fragment_module {
924            Some(h) => Some(self.shader(h)?),
925            None => None,
926        };
927        let bgls = match &group_layouts {
928            Some(hs) => Some(hs.iter().map(|h| self.layout(*h)).collect::<R<Vec<_>>>()?),
929            None => None,
930        };
931        self.scoped("create_pipeline", |gpu| {
932            let layout = bgls.map(|ls| {
933                let refs: Vec<Option<&wgpu::BindGroupLayout>> = ls.into_iter().map(Some).collect();
934                gpu.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &refs, immediate_size: 0 })
935            });
936            Ok(gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
937                label: None,
938                layout: layout.as_ref(), // None = "auto"
939                vertex: wgpu::VertexState {
940                    module: vmod,
941                    entry_point: v_entry.as_deref(),
942                    compilation_options: Default::default(),
943                    buffers: &layouts,
944                },
945                primitive,
946                depth_stencil,
947                multisample: wgpu::MultisampleState { count: SAMPLE_COUNT, ..Default::default() },
948                fragment: fragment_module.map(|m| wgpu::FragmentState {
949                    module: m,
950                    entry_point: f_entry.as_deref(),
951                    compilation_options: Default::default(),
952                    targets: &targets,
953                }),
954                multiview_mask: None,
955                cache: None,
956            }))
957        })
958        .map(Some)
959    }
960
961    // ---- frame -----------------------------------------------------------------------------
962
963    /// Start a frame. `show`: whether the runner will display it.
964    pub fn begin_frame(&mut self, clear: [f32; 4], show: bool) -> R<bool> {
965        if self.pass.is_some() {
966            return Err("gfx.begin_frame called twice without end_frame".into());
967        }
968        self.used = true;
969        self.pass = Some(Pass::default());
970        let (Some(gpu), Some(target), true) = (&self.gpu, &self.target, show) else { return Ok(false) };
971        let (resolve_view, surface_texture) = match target {
972            Target::Window { surface, config } => {
973                let st = match surface.get_current_texture() {
974                    wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
975                    wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
976                        surface.configure(&gpu.device, config);
977                        return Ok(false);
978                    }
979                    _ => return Ok(false), // timeout / occluded: skip this frame
980                };
981                (st.texture.create_view(&Default::default()), Some(st))
982            }
983            Target::Offscreen { texture } => (texture.create_view(&Default::default()), None),
984        };
985        let mut encoder = gpu.device.create_command_encoder(&Default::default());
986        let c = clear.map(|v| v as f64);
987        let pass = encoder
988            .begin_render_pass(&wgpu::RenderPassDescriptor {
989                label: Some("guest"),
990                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
991                    view: self.msaa.as_ref().unwrap(),
992                    depth_slice: None,
993                    resolve_target: Some(&resolve_view),
994                    ops: wgpu::Operations {
995                        load: wgpu::LoadOp::Clear(wgpu::Color { r: c[0], g: c[1], b: c[2], a: c[3] }),
996                        store: wgpu::StoreOp::Discard,
997                    },
998                })],
999                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1000                    view: self.depth.as_ref().unwrap(),
1001                    depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
1002                    stencil_ops: None,
1003                }),
1004                timestamp_writes: None,
1005                occlusion_query_set: None,
1006                multiview_mask: None,
1007            })
1008            .forget_lifetime();
1009        self.frame = Some(Frame { pass, encoder, surface_texture, empty_viewport: false, empty_scissor: false });
1010        Ok(true)
1011    }
1012
1013    pub fn set_pipeline(&mut self, h: u32) -> R<()> {
1014        let targets = self.pipeline_meta(h)?.targets;
1015        if targets != 1 {
1016            return Err(format!("gfx.set_pipeline: pipeline {h} has {targets} color targets; the render pass has one (\"surface\")"));
1017        }
1018        self.pass_mut("set_pipeline")?.pipeline = Some(h);
1019        if self.frame.is_some() {
1020            let p = self.pipeline(h)?.clone();
1021            if let Some(f) = &mut self.frame {
1022                f.pass.set_pipeline(&p);
1023            }
1024        }
1025        Ok(())
1026    }
1027
1028    pub fn set_bind_group(&mut self, index: u32, h: u32) -> R<()> {
1029        self.set_bind_group_offsets(index, h, &[])
1030    }
1031
1032    pub fn set_bind_group_offsets(&mut self, index: u32, h: u32, offsets: &[u32]) -> R<()> {
1033        if index >= MAX_BIND_GROUPS {
1034            return Err(format!("gfx.set_bind_group: index {index} must be below {MAX_BIND_GROUPS}"));
1035        }
1036        let Meta::BindGroup { dynamic, .. } = self.expect(h, "bind group")? else { unreachable!() };
1037        if offsets.len() != dynamic.len() {
1038            return Err(format!("gfx.set_bind_group_offsets: bind group {h} has {} dynamic entries, got {} offsets", dynamic.len(), offsets.len()));
1039        }
1040        for (o, d) in offsets.iter().zip(dynamic) {
1041            if o % OFFSET_ALIGNMENT != 0 {
1042                return Err(format!("gfx.set_bind_group_offsets: offset {o} is not a multiple of {OFFSET_ALIGNMENT}"));
1043            }
1044            if add_u64(*o as u64 + d.offset, d.size, "dynamic offset")? > d.buffer_size {
1045                return Err(format!("gfx.set_bind_group_offsets: offset {o} + binding {}+{} exceeds buffer size {}", d.offset, d.size, d.buffer_size));
1046            }
1047        }
1048        self.pass_mut("set_bind_group")?.groups[index as usize] = Some(h);
1049        if self.frame.is_some() {
1050            let bg = self.bind_group(h)?.clone();
1051            if let Some(f) = &mut self.frame {
1052                f.pass.set_bind_group(index, &bg, offsets);
1053            }
1054        }
1055        Ok(())
1056    }
1057
1058    /// Clamp `[x, x+w) x [y, y+h)` to the drawable.
1059    fn clamp_rect(&self, x: f64, y: f64, w: f64, h: f64) -> (f64, f64, f64, f64) {
1060        let (dw, dh) = (self.width as f64, self.height as f64);
1061        let (x0, y0) = (x.clamp(0.0, dw), y.clamp(0.0, dh));
1062        let (x1, y1) = ((x + w).clamp(0.0, dw), (y + h).clamp(0.0, dh));
1063        (x0, y0, x1 - x0, y1 - y0)
1064    }
1065
1066    pub fn set_viewport(&mut self, x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) -> R<()> {
1067        if ![x, y, w, h, min_depth, max_depth].iter().all(|v| v.is_finite()) || w < 0.0 || h < 0.0 {
1068            return Err(format!("gfx.set_viewport: invalid rectangle {x},{y} {w}x{h}"));
1069        }
1070        if !(0.0..=1.0).contains(&min_depth) || !(0.0..=1.0).contains(&max_depth) || min_depth > max_depth {
1071            return Err(format!("gfx.set_viewport: depth range {min_depth}..{max_depth} must be within 0..1"));
1072        }
1073        self.pass_mut("set_viewport")?;
1074        let (x, y, w, h) = self.clamp_rect(x as f64, y as f64, w as f64, h as f64);
1075        if let Some(f) = &mut self.frame {
1076            f.empty_viewport = w <= 0.0 || h <= 0.0;
1077            if !f.empty_viewport {
1078                f.pass.set_viewport(x as f32, y as f32, w as f32, h as f32, min_depth, max_depth);
1079            }
1080        }
1081        Ok(())
1082    }
1083
1084    pub fn set_scissor_rect(&mut self, x: u32, y: u32, w: u32, h: u32) -> R<()> {
1085        self.pass_mut("set_scissor_rect")?;
1086        let (x, y, w, h) = self.clamp_rect(x as f64, y as f64, w as f64, h as f64);
1087        if let Some(f) = &mut self.frame {
1088            f.empty_scissor = w <= 0.0 || h <= 0.0;
1089            if !f.empty_scissor {
1090                f.pass.set_scissor_rect(x as u32, y as u32, w as u32, h as u32);
1091            }
1092        }
1093        Ok(())
1094    }
1095
1096    pub fn set_vertex_buffer(&mut self, slot: u32, h: u32, offset: u32) -> R<()> {
1097        if slot >= MAX_VERTEX_BUFFERS {
1098            return Err(format!("gfx.set_vertex_buffer: slot {slot} must be below {MAX_VERTEX_BUFFERS}"));
1099        }
1100        let (size, usage) = self.buffer_meta(h)?;
1101        if usage & BUF_VERTEX == 0 {
1102            return Err(format!("gfx.set_vertex_buffer: buffer {h} lacks VERTEX usage"));
1103        }
1104        if offset % 4 != 0 || offset as u64 > size {
1105            return Err(format!("gfx.set_vertex_buffer: offset {offset} must be a multiple of 4 within the buffer ({size} bytes)"));
1106        }
1107        self.pass_mut("set_vertex_buffer")?.vertex[slot as usize] = Some((h, offset as u64));
1108        if self.frame.is_some() && (offset as u64) < size {
1109            let b = self.buffer(h)?.clone();
1110            if let Some(f) = &mut self.frame {
1111                f.pass.set_vertex_buffer(slot, b.slice(offset as u64..));
1112            }
1113        }
1114        Ok(())
1115    }
1116
1117    pub fn set_index_buffer(&mut self, h: u32, format: u32, offset: u32) -> R<()> {
1118        let (fmt, isize) = match format {
1119            0 => (wgpu::IndexFormat::Uint16, 2),
1120            1 => (wgpu::IndexFormat::Uint32, 4),
1121            f => return Err(format!("gfx.set_index_buffer: format {f} must be GASM_INDEX_U16 (0) or GASM_INDEX_U32 (1)")),
1122        };
1123        let (size, usage) = self.buffer_meta(h)?;
1124        if usage & BUF_INDEX == 0 {
1125            return Err(format!("gfx.set_index_buffer: buffer {h} lacks INDEX usage"));
1126        }
1127        if offset as u64 % isize != 0 || offset as u64 > size {
1128            return Err(format!("gfx.set_index_buffer: offset {offset} must be a multiple of {isize} within the buffer ({size} bytes)"));
1129        }
1130        self.pass_mut("set_index_buffer")?.index = Some((h, isize, offset as u64));
1131        if self.frame.is_some() && (offset as u64) < size {
1132            let b = self.buffer(h)?.clone();
1133            if let Some(f) = &mut self.frame {
1134                f.pass.set_index_buffer(b.slice(offset as u64..), fmt);
1135            }
1136        }
1137        Ok(())
1138    }
1139
1140    /// Validate a draw against the pass state: pipeline, compatible bind groups,
1141    /// vertex buffers large enough (`vertices`: first + count, None for indexed
1142    /// draws, whose per-vertex reads aren't checked, as in WebGPU).
1143    fn check_draw(&self, what: &str, vertices: Option<u64>, instances: u64) -> R<()> {
1144        let pass = self.pass.as_ref().ok_or_else(|| format!("gfx.{what} called outside begin_frame/end_frame"))?;
1145        let ph = pass.pipeline.ok_or_else(|| format!("gfx.{what}: no pipeline set"))?;
1146        let pm = self.pipeline_meta(ph)?;
1147        if let Some(groups) = &pm.groups {
1148            for (i, entries) in groups.iter().enumerate() {
1149                let bg = pass.groups[i].ok_or_else(|| format!("gfx.{what}: pipeline {ph} needs a bind group at index {i}"))?;
1150                let Meta::BindGroup { layout, .. } = self.expect(bg, "bind group")? else { unreachable!() };
1151                if !matches!(layout, GroupLayout::Explicit(e) if e == entries) {
1152                    return Err(format!("gfx.{what}: bind group {bg} at index {i} doesn't match pipeline {ph}'s layout"));
1153                }
1154            }
1155        } else {
1156            for (i, bg) in pass.groups.iter().enumerate() {
1157                let Some(bg) = *bg else { continue };
1158                let Meta::BindGroup { layout, .. } = self.expect(bg, "bind group")? else { unreachable!() };
1159                match layout {
1160                    GroupLayout::Auto { pipeline, group } if *group == i as u32 && *pipeline != ph => {
1161                        return Err(format!("gfx.{what}: bind group {bg} was made for pipeline {pipeline} (\"auto\" layouts are per pipeline), not {ph}"));
1162                    }
1163                    _ => {}
1164                }
1165            }
1166        }
1167        for (slot, vs) in pm.vertex.iter().enumerate() {
1168            let (b, offset) = pass.vertex[slot].ok_or_else(|| format!("gfx.{what}: pipeline {ph} needs a vertex buffer in slot {slot}"))?;
1169            let (size, _) = self.buffer_meta(b)?;
1170            let count = if vs.instance { Some(instances) } else { vertices };
1171            if let Some(n) = count.filter(|n| *n > 0) {
1172                let need = (n - 1).checked_mul(vs.stride).and_then(|x| x.checked_add(vs.last)).ok_or_else(|| format!("gfx.{what}: draw range overflows"))?;
1173                if need > size - offset {
1174                    return Err(format!("gfx.{what}: vertex buffer {b} in slot {slot} has {} bytes after its offset, the draw reads {need}", size - offset));
1175                }
1176            }
1177        }
1178        Ok(())
1179    }
1180
1181    fn skip_draw(&self) -> bool {
1182        self.frame.as_ref().is_none_or(|f| f.empty_viewport || f.empty_scissor)
1183    }
1184
1185    pub fn draw(&mut self, vc: u32, ic: u32, fv: u32, fi: u32) -> R<()> {
1186        self.check_draw("draw", Some(fv as u64 + vc as u64), fi as u64 + ic as u64)?;
1187        if !self.skip_draw() {
1188            let f = self.frame.as_mut().unwrap();
1189            f.pass.draw(fv..fv.saturating_add(vc), fi..fi.saturating_add(ic));
1190        }
1191        Ok(())
1192    }
1193
1194    pub fn draw_indexed(&mut self, ic: u32, inst: u32, first: u32, base: i32, fi: u32) -> R<()> {
1195        self.check_draw("draw_indexed", None, fi as u64 + inst as u64)?;
1196        let pass = self.pass.as_ref().unwrap();
1197        let (b, isize, offset) = pass.index.ok_or("gfx.draw_indexed: no index buffer set")?;
1198        let (size, _) = self.buffer_meta(b)?;
1199        let need = (first as u64 + ic as u64) * isize;
1200        if need > size - offset {
1201            return Err(format!("gfx.draw_indexed: index buffer {b} has {} bytes after its offset, the draw reads {need}", size - offset));
1202        }
1203        if !self.skip_draw() {
1204            let f = self.frame.as_mut().unwrap();
1205            f.pass.draw_indexed(first..first.saturating_add(ic), base, fi..fi.saturating_add(inst));
1206        }
1207        Ok(())
1208    }
1209
1210    /// End the frame: submit (inside an error scope, so GPU validation errors
1211    /// trap) and present.
1212    pub fn end_frame(&mut self) -> R<()> {
1213        self.pass.take();
1214        let Some(Frame { pass, encoder, surface_texture, .. }) = self.frame.take() else { return Ok(()) };
1215        self.scoped("end_frame", move |gpu| {
1216            drop(pass);
1217            gpu.queue.submit([encoder.finish()]);
1218            if let Some(st) = surface_texture {
1219                gpu.queue.present(st);
1220            }
1221            Ok(())
1222        })
1223    }
1224
1225    // ---- 2D path: show a video_present frame -----------------------------------------------
1226
1227    /// Show an RGBA frame in the window, letterboxed and filtered (`self.present`).
1228    pub fn present_video(&mut self, rgba: &[u8], w: u32, h: u32, aspect: Option<(u32, u32)>) {
1229        let (Some(gpu), Some(Target::Window { surface, config }), Some(presenter)) = (&self.gpu, &self.target, &mut self.presenter) else { return };
1230        let st = match surface.get_current_texture() {
1231            wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
1232            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
1233                surface.configure(&gpu.device, config);
1234                return;
1235            }
1236            _ => return,
1237        };
1238        let view = st.texture.create_view(&Default::default());
1239        let mut encoder = gpu.device.create_command_encoder(&Default::default());
1240        presenter.draw(gpu, &mut encoder, &view, (self.width, self.height), VideoFrame { rgba, width: w, height: h, aspect }, self.present);
1241        gpu.queue.submit([encoder.finish()]);
1242        gpu.queue.present(st);
1243    }
1244
1245    /// Render an RGBA frame as the window would show it at `size` and read it back
1246    /// (headless `--screenshot-filtered`). None without a GPU.
1247    pub fn render_video(&mut self, rgba: &[u8], w: u32, h: u32, aspect: Option<(u32, u32)>, size: (u32, u32)) -> Option<(u32, u32, Vec<u8>)> {
1248        let (Some(gpu), Some(presenter)) = (&self.gpu, &mut self.presenter) else { return None };
1249        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
1250            label: Some("filtered"),
1251            size: wgpu::Extent3d { width: size.0, height: size.1, depth_or_array_layers: 1 },
1252            mip_level_count: 1,
1253            sample_count: 1,
1254            dimension: wgpu::TextureDimension::D2,
1255            format: gpu.format,
1256            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1257            view_formats: &[],
1258        });
1259        let mut encoder = gpu.device.create_command_encoder(&Default::default());
1260        presenter.draw(gpu, &mut encoder, &texture.create_view(&Default::default()), size, VideoFrame { rgba, width: w, height: h, aspect }, self.present);
1261        gpu.queue.submit([encoder.finish()]);
1262        read_texture(gpu, &texture, size.0, size.1).map(|px| (size.0, size.1, px))
1263    }
1264
1265    /// Read back the offscreen target as tightly packed RGBA8 (headless screenshots).
1266    pub fn read_offscreen(&self) -> Option<(u32, u32, Vec<u8>)> {
1267        let (Some(gpu), Some(Target::Offscreen { texture })) = (&self.gpu, &self.target) else { return None };
1268        read_texture(gpu, texture, self.width, self.height).map(|px| (self.width, self.height, px))
1269    }
1270}
1271
1272/// Copy a `w×h` 4-byte-per-texel texture back to the CPU, rows tightly packed.
1273fn read_texture(gpu: &Gpu, texture: &wgpu::Texture, w: u32, h: u32) -> Option<Vec<u8>> {
1274    let row = (w * 4).div_ceil(256) * 256;
1275    let buf = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1276        label: Some("readback"),
1277        size: (row * h) as u64,
1278        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1279        mapped_at_creation: false,
1280    });
1281    let mut enc = gpu.device.create_command_encoder(&Default::default());
1282    enc.copy_texture_to_buffer(
1283        texture.as_image_copy(),
1284        wgpu::TexelCopyBufferInfo {
1285            buffer: &buf,
1286            layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(h) },
1287        },
1288        wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1289    );
1290    gpu.queue.submit([enc.finish()]);
1291    buf.slice(..).map_async(wgpu::MapMode::Read, |_| {});
1292    gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
1293    let data = buf.slice(..).get_mapped_range().ok()?;
1294    let mut out = Vec::with_capacity((w * h * 4) as usize);
1295    for y in 0..h as usize {
1296        out.extend_from_slice(&data[y * row as usize..y * row as usize + (w * 4) as usize]);
1297    }
1298    // a BGRA surface format (window targets) reads back as BGRA
1299    if matches!(gpu.format, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb) {
1300        out.chunks_exact_mut(4).for_each(|p| p.swap(0, 2));
1301    }
1302    Some(out)
1303}
1304
1305#[cfg(feature = "window")]
1306fn open_device(
1307    adapter: &wgpu::Adapter,
1308    choose: impl FnOnce(&[wgpu::TextureFormat]) -> wgpu::TextureFormat,
1309    surface: &wgpu::Surface,
1310) -> R<Gpu> {
1311    let (device, queue) =
1312        pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).map_err(|e| e.to_string())?;
1313    device.on_uncaptured_error(Arc::new(|e| eprintln!("[gasm] gpu error: {e}")));
1314    let caps = surface.get_capabilities(adapter);
1315    let format = choose(&caps.formats);
1316    Ok(Gpu { device, queue, format })
1317}
1318
1319#[cfg(feature = "window")]
1320/// Prefer a non-sRGB 8-bit format: browsers' canvas formats are non-sRGB, so
1321/// guests look the same on both runners.
1322fn pick_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
1323    use wgpu::TextureFormat as F;
1324    [F::Bgra8Unorm, F::Rgba8Unorm].into_iter().find(|f| formats.contains(f)).unwrap_or(formats[0])
1325}
1326
1327fn uint(v: &Value, key: &str) -> R<u32> {
1328    v.get(key).and_then(Value::as_u64).and_then(|n| u32::try_from(n).ok()).ok_or_else(|| format!("descriptor: missing number {key:?}"))
1329}
1330
1331/// An object handle in a descriptor.
1332fn handle(v: &Value) -> R<u32> {
1333    v.as_u64().and_then(|n| u32::try_from(n).ok()).ok_or_else(|| format!("descriptor: {v} is not a handle"))
1334}
1335
1336/// Byte size of a vertex format.
1337fn vertex_format_size(s: &str) -> R<u64> {
1338    Ok(match s {
1339        "float32" | "uint32" | "sint32" | "uint8x4" | "unorm8x4" | "uint16x2" | "float16x2" => 4,
1340        "float32x2" | "uint32x2" | "sint32x2" | "uint16x4" | "float16x4" => 8,
1341        "float32x3" | "uint32x3" | "sint32x3" => 12,
1342        "float32x4" | "uint32x4" | "sint32x4" => 16,
1343        o => return Err(format!("unsupported vertex format {o:?}")),
1344    })
1345}
1346
1347/// The vertex buffers of a pipeline's `vertex` stage, validated like WebGPU's
1348/// createRenderPipeline (strides, attribute offsets and locations).
1349fn vertex_slots(v: &Value) -> R<Vec<VertexSlot>> {
1350    let buffers = v.get("buffers").and_then(Value::as_array).cloned().unwrap_or_default();
1351    if buffers.len() > MAX_VERTEX_BUFFERS as usize {
1352        return Err(format!("pipeline: at most {MAX_VERTEX_BUFFERS} vertex buffers, got {}", buffers.len()));
1353    }
1354    let mut locations = Vec::new();
1355    let mut out = Vec::new();
1356    for (i, b) in buffers.iter().enumerate() {
1357        let stride = b.get("arrayStride").and_then(Value::as_u64).unwrap_or(0);
1358        if stride % 4 != 0 || stride > MAX_VERTEX_STRIDE {
1359            return Err(format!("pipeline: vertex buffer {i}: arrayStride {stride} must be a multiple of 4, at most {MAX_VERTEX_STRIDE}"));
1360        }
1361        let instance = match b.get("stepMode").and_then(Value::as_str).unwrap_or("vertex") {
1362            "vertex" => false,
1363            "instance" => true,
1364            o => return Err(format!("pipeline: vertex buffer {i}: unknown stepMode {o:?}")),
1365        };
1366        let mut last = 0;
1367        for a in b.get("attributes").and_then(Value::as_array).cloned().unwrap_or_default() {
1368            let size = vertex_format_size(string(&a, "format")?)?;
1369            let offset = a.get("offset").and_then(Value::as_u64).unwrap_or(0);
1370            let location = uint(&a, "shaderLocation")?;
1371            if offset % size.min(4) != 0 || (stride > 0 && offset + size > stride) || offset + size > MAX_VERTEX_STRIDE {
1372                return Err(format!("pipeline: vertex buffer {i}: attribute at offset {offset} doesn't fit (arrayStride {stride})"));
1373            }
1374            if location as usize >= MAX_VERTEX_ATTRIBUTES || locations.contains(&location) {
1375                return Err(format!("pipeline: shaderLocation {location} is used twice or is not below {MAX_VERTEX_ATTRIBUTES}"));
1376            }
1377            locations.push(location);
1378            last = last.max(offset + size);
1379        }
1380        out.push(VertexSlot { stride, instance, last });
1381    }
1382    Ok(out)
1383}
1384
1385fn string<'a>(v: &'a Value, key: &str) -> R<&'a str> {
1386    v.get(key).and_then(Value::as_str).ok_or_else(|| format!("descriptor: missing string {key:?}"))
1387}
1388
1389fn vertex_format(s: &str) -> R<wgpu::VertexFormat> {
1390    use wgpu::VertexFormat as V;
1391    Ok(match s {
1392        "float32" => V::Float32,
1393        "float32x2" => V::Float32x2,
1394        "float32x3" => V::Float32x3,
1395        "float32x4" => V::Float32x4,
1396        "uint32" => V::Uint32,
1397        "uint32x2" => V::Uint32x2,
1398        "uint32x3" => V::Uint32x3,
1399        "uint32x4" => V::Uint32x4,
1400        "sint32" => V::Sint32,
1401        "sint32x2" => V::Sint32x2,
1402        "sint32x3" => V::Sint32x3,
1403        "sint32x4" => V::Sint32x4,
1404        "uint8x4" => V::Uint8x4,
1405        "unorm8x4" => V::Unorm8x4,
1406        "uint16x2" => V::Uint16x2,
1407        "uint16x4" => V::Uint16x4,
1408        "float16x2" => V::Float16x2,
1409        "float16x4" => V::Float16x4,
1410        o => return Err(format!("unsupported vertex format {o:?}")),
1411    })
1412}
1413
1414fn blend_component(v: Option<&Value>) -> R<wgpu::BlendComponent> {
1415    let Some(v) = v else { return Ok(wgpu::BlendComponent::REPLACE) };
1416    let factor = |k: &str, default: wgpu::BlendFactor| -> R<wgpu::BlendFactor> {
1417        use wgpu::BlendFactor as B;
1418        Ok(match v.get(k).and_then(Value::as_str) {
1419            None => default,
1420            Some("zero") => B::Zero,
1421            Some("one") => B::One,
1422            Some("src") => B::Src,
1423            Some("one-minus-src") => B::OneMinusSrc,
1424            Some("src-alpha") => B::SrcAlpha,
1425            Some("one-minus-src-alpha") => B::OneMinusSrcAlpha,
1426            Some("dst") => B::Dst,
1427            Some("one-minus-dst") => B::OneMinusDst,
1428            Some("dst-alpha") => B::DstAlpha,
1429            Some("one-minus-dst-alpha") => B::OneMinusDstAlpha,
1430            Some("src-alpha-saturated") => B::SrcAlphaSaturated,
1431            Some("constant") => B::Constant,
1432            Some("one-minus-constant") => B::OneMinusConstant,
1433            Some(o) => return Err(format!("unsupported blend factor {o:?}")),
1434        })
1435    };
1436    use wgpu::BlendOperation as O;
1437    Ok(wgpu::BlendComponent {
1438        src_factor: factor("srcFactor", wgpu::BlendFactor::One)?,
1439        dst_factor: factor("dstFactor", wgpu::BlendFactor::Zero)?,
1440        operation: match v.get("operation").and_then(Value::as_str).unwrap_or("add") {
1441            "add" => O::Add,
1442            "subtract" => O::Subtract,
1443            "reverse-subtract" => O::ReverseSubtract,
1444            "min" => O::Min,
1445            "max" => O::Max,
1446            o => return Err(format!("unsupported blend operation {o:?}")),
1447        },
1448    })
1449}
1450
1451fn compare(s: &str) -> R<wgpu::CompareFunction> {
1452    use wgpu::CompareFunction as C;
1453    Ok(match s {
1454        "never" => C::Never,
1455        "less" => C::Less,
1456        "equal" => C::Equal,
1457        "less-equal" => C::LessEqual,
1458        "greater" => C::Greater,
1459        "not-equal" => C::NotEqual,
1460        "greater-equal" => C::GreaterEqual,
1461        "always" => C::Always,
1462        o => return Err(format!("unsupported compare function {o:?}")),
1463    })
1464}
1465
1466#[cfg(test)]
1467mod tests {
1468    use super::*;
1469
1470    /// One call of tests/gfx-cases.json on `g`.
1471    fn call(g: &mut Gfx, c: &[Value], shader: &str) -> R<()> {
1472        let n = |i: usize| c[i].as_u64().unwrap() as u32;
1473        let json = |i: usize| c[i].to_string();
1474        match c[0].as_str().unwrap() {
1475            "create_shader" => g.create_shader(shader).map(drop),
1476            "create_buffer" => g.create_buffer(n(1), n(2)).map(drop),
1477            "create_pipeline" => g.create_pipeline(&json(1)).map(drop),
1478            "create_bind_group" => g.create_bind_group(&json(1)).map(drop),
1479            "create_bind_group_layout" => g.create_bind_group_layout(&json(1)).map(drop),
1480            "create_texture" => g.create_texture(&json(1)).map(drop),
1481            "write_buffer" => g.write_buffer(n(1), n(2), &vec![0; n(3) as usize]),
1482            "write_texture" => g.write_texture(n(1), n(2), n(3), n(4), n(5), n(6), &vec![0; n(7) as usize]),
1483            "begin_frame" => g.begin_frame([0.0; 4], false).map(drop),
1484            "set_pipeline" => g.set_pipeline(n(1)),
1485            "set_bind_group" => g.set_bind_group(n(1), n(2)),
1486            "set_vertex_buffer" => g.set_vertex_buffer(n(1), n(2), n(3)),
1487            "set_index_buffer" => g.set_index_buffer(n(1), n(2), n(3)),
1488            "draw" => g.draw(n(1), n(2), n(3), n(4)),
1489            "draw_indexed" => g.draw_indexed(n(1), n(2), n(3), n(4) as i32, n(5)),
1490            "end_frame" => g.end_frame(),
1491            "destroy" => g.destroy(n(1)),
1492            other => panic!("unknown call {other}"),
1493        }
1494    }
1495
1496    /// The validation cases shared with the JS runner (scripts/gfx-model-test.mjs).
1497    #[test]
1498    fn shared_validation_cases() {
1499        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/gfx-cases.json");
1500        let doc: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
1501        let shader = doc["shader"].as_str().unwrap();
1502        for case in doc["cases"].as_array().unwrap() {
1503            let name = case["name"].as_str().unwrap();
1504            let mut g = Gfx::null();
1505            let mut failed = None;
1506            for (i, c) in case["calls"].as_array().unwrap().iter().enumerate() {
1507                if let Err(e) = call(&mut g, c.as_array().unwrap(), shader) {
1508                    failed = Some((i, e));
1509                    break;
1510                }
1511            }
1512            let want = case["fails"].as_u64().map(|i| i as usize);
1513            assert_eq!(failed.as_ref().map(|f| f.0), want, "{name}: {failed:?}");
1514        }
1515    }
1516}