Skip to main content

gasm_host/
gl.rs

1//! gasm:gl (OpenGL ES 3.0 with WebGL 2's rules, design/gasm-gl.md) on the native
2//! runner: the shared model and the null GL. Mirrors runners/web/lib/gl.js (its
3//! `ctx == null` paths) line by line: same names, same GL errors, same traps, same
4//! hashes. Natively there is no GL backend yet, so gasm:gl guests run headless only.
5//!
6//! The imports are linked with `func_new` from the generated signature table
7//! (gl_sigs.rs, from spec/abi.json) and dispatched by name in [`call`]. Every value
8//! is read as the i32 the guest passed, as the JS runner sees it, so signed checks
9//! (negative sizes, `end >= start`) agree.
10
11use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
12
13use std::mem::MaybeUninit;
14use wasmtime::{Caller, FuncType, Linker, Val, ValRaw, ValType, bail};
15
16use crate::host::{Host, guest_slice, guest_slice_mut, memory};
17
18#[path = "gl_sigs.rs"]
19mod sigs;
20
21#[path = "gl_backend.rs"]
22mod backend;
23pub use backend::Backend;
24
25const INVALID_ENUM: u32 = 0x0500;
26const INVALID_VALUE: u32 = 0x0501;
27const INVALID_OPERATION: u32 = 0x0502;
28const ARRAY_BUFFER: u32 = 0x8892;
29const ELEMENT_ARRAY_BUFFER: u32 = 0x8893;
30const COPY_READ_BUFFER: u32 = 0x8F36;
31const COPY_WRITE_BUFFER: u32 = 0x8F37;
32const PIXEL_PACK_BUFFER: u32 = 0x88EB;
33const PIXEL_UNPACK_BUFFER: u32 = 0x88EC;
34const TRANSFORM_FEEDBACK_BUFFER: u32 = 0x8C8E;
35const UNIFORM_BUFFER: u32 = 0x8A11;
36const TEXTURE_2D: u32 = 0x0DE1;
37const TEXTURE_CUBE_MAP: u32 = 0x8513;
38const TEXTURE_3D: u32 = 0x806F;
39const TEXTURE_2D_ARRAY: u32 = 0x8C1A;
40const CUBE_POSITIVE_X: u32 = 0x8515;
41const CUBE_NEGATIVE_Z: u32 = 0x851A;
42const TEXTURE0: u32 = 0x84C0;
43const FRAMEBUFFER: u32 = 0x8D40;
44const READ_FRAMEBUFFER: u32 = 0x8CA8;
45const DRAW_FRAMEBUFFER: u32 = 0x8CA9;
46const RENDERBUFFER: u32 = 0x8D41;
47const FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
48const VERTEX_SHADER: u32 = 0x8B31;
49const FRAGMENT_SHADER: u32 = 0x8B30;
50const COMPILE_STATUS: u32 = 0x8B81;
51const LINK_STATUS: u32 = 0x8B82;
52const VALIDATE_STATUS: u32 = 0x8B83;
53const SHADER_SOURCE_LENGTH: u32 = 0x8B88;
54const SHADER_TYPE: u32 = 0x8B4F;
55const TRANSFORM_FEEDBACK: u32 = 0x8E22;
56const SYNC_STATUS: u32 = 0x9114;
57const SIGNALED: u32 = 0x9119;
58const UNSIGNALED: u32 = 0x9118;
59const ALREADY_SIGNALED: u32 = 0x911A;
60const TIMEOUT_EXPIRED: u32 = 0x911B;
61const WAIT_FAILED: u32 = 0x911D;
62const SYNC_GPU_COMMANDS_COMPLETE: u32 = 0x9117;
63const QUERY_RESULT: u32 = 0x8866;
64const QUERY_RESULT_AVAILABLE: u32 = 0x8867;
65const CURRENT_QUERY: u32 = 0x8865;
66const INVALID_INDEX: u32 = 0xFFFF_FFFF;
67
68const BUFFER_TARGETS: &[u32] = &[
69    ARRAY_BUFFER,
70    ELEMENT_ARRAY_BUFFER,
71    COPY_READ_BUFFER,
72    COPY_WRITE_BUFFER,
73    PIXEL_PACK_BUFFER,
74    PIXEL_UNPACK_BUFFER,
75    TRANSFORM_FEEDBACK_BUFFER,
76    UNIFORM_BUFFER,
77];
78const TEXTURE_TARGETS: &[u32] = &[TEXTURE_2D, TEXTURE_CUBE_MAP, TEXTURE_3D, TEXTURE_2D_ARRAY];
79const VOLUME_TARGETS: &[u32] = &[TEXTURE_3D, TEXTURE_2D_ARRAY];
80const FB_TARGETS: &[u32] = &[FRAMEBUFFER, READ_FRAMEBUFFER, DRAW_FRAMEBUFFER];
81const QUERY_TARGETS: &[u32] = &[0x8C2F, 0x8D6A, 0x8C88];
82const INDEXED_TARGETS: &[u32] = &[UNIFORM_BUFFER, TRANSFORM_FEEDBACK_BUFFER];
83
84/// Bytes per pixel of a (format, type) pair; 0 if the pair is invalid.
85pub fn pixel_bytes(format: u32, ty: u32) -> u32 {
86    match ty {
87        0x8363 | 0x8033 | 0x8034 => return 2,
88        0x8368 | 0x8C3B | 0x8C3E | 0x84FA => return 4,
89        0x8DAD => return 8,
90        _ => {}
91    }
92    let size = match ty {
93        0x1400 | 0x1401 => 1,
94        0x1402 | 0x1403 | 0x140B => 2,
95        0x1404..=0x1406 => 4,
96        _ => 0,
97    };
98    let comps = match format {
99        0x1903 | 0x8D94 | 0x1906 | 0x1909 | 0x1902 => 1,
100        0x8227 | 0x8228 | 0x190A | 0x84F9 => 2,
101        0x1907 | 0x8D98 => 3,
102        0x1908 | 0x8D99 => 4,
103        _ => 0,
104    };
105    size * comps
106}
107
108#[derive(Clone, Copy)]
109struct Store {
110    alignment: i64,
111    row_length: i64,
112    image_height: i64,
113    skip_pixels: i64,
114    skip_rows: i64,
115    skip_images: i64,
116}
117
118const STORE: Store = Store {
119    alignment: 4,
120    row_length: 0,
121    image_height: 0,
122    skip_pixels: 0,
123    skip_rows: 0,
124    skip_images: 0,
125};
126
127/// Bytes an image of w×h×d needs under a pixel-store state (GLES 3.0, 3.7.1).
128fn image_bytes(w: i64, h: i64, d: i64, bpp: i64, s: &Store) -> i64 {
129    if w <= 0 || h <= 0 || d <= 0 {
130        return 0;
131    }
132    let row_len = if s.row_length > 0 { s.row_length } else { w };
133    let a = s.alignment;
134    let row = (row_len * bpp + a - 1) / a * a;
135    let img_h = if s.image_height > 0 {
136        s.image_height
137    } else {
138        h
139    };
140    let img = row * img_h;
141    s.skip_images * img
142        + (d - 1) * img
143        + s.skip_rows * row
144        + (h - 1) * row
145        + s.skip_pixels * bpp
146        + w * bpp
147}
148
149/// WebGL 2's guaranteed minimums (and fixed answers), reported by null GLs.
150fn null_limit(pname: u32) -> &'static [i64] {
151    // the same table as NULL_LIMITS in runners/web/lib/gl.js (gen-abi.mjs --check)
152    match pname {
153        0x0D33 | 0x851C | 0x84E8 => &[2048], // texture, cube map, renderbuffer size
154        0x8073 | 0x88FF => &[256],           // 3D texture size, array texture layers
155        0x8872 | 0x8B4C | 0x8869 => &[16],   // texture units (fragment, vertex), vertex attribs
156        0x8B4D => &[32],                     // combined texture units
157        0x8DFB => &[256],                    // vertex uniform vectors
158        0x8DFD => &[224],                    // fragment uniform vectors
159        0x8DFC => &[15],                     // varying vectors
160        0x8B4B | 0x9125 => &[60],            // varying components, fragment input components
161        0x9122 => &[64],                     // vertex output components
162        0x8B4A => &[1024],                   // vertex uniform components
163        0x8B49 => &[896],                    // fragment uniform components
164        0x8CDF | 0x8824 | 0x8D57 => &[4],    // color attachments, draw buffers, samples
165        0x8A2F => &[24],                     // uniform buffer bindings
166        0x8A30 => &[16384],                  // uniform block size
167        0x8A34 => &[256],                    // uniform buffer offset alignment
168        0x8A2B | 0x8A2D => &[12],            // vertex, fragment uniform blocks
169        0x8A2E => &[24],                     // combined uniform blocks
170        0x8A31 => &[50176],                  // combined vertex uniform components
171        0x8A33 => &[50048],                  // combined fragment uniform components
172        0x8904 => &[-8],                     // min program texel offset
173        0x8905 => &[7],                      // max program texel offset
174        0x84FD => &[2],                      // texture LOD bias
175        0x8D6B => &[16777215],               // element index (2^24 - 1)
176        0x8C8A => &[64],                     // transform feedback interleaved components
177        0x8C8B | 0x8C80 => &[4],             // transform feedback separate attribs, components
178        0x0D3A => &[4096, 4096],             // viewport dims
179        0x846D | 0x846E => &[1, 1],          // point size, line width ranges
180        0x821B => &[3],                      // major version
181        _ => &[0],
182    }
183}
184
185fn null_string(name: u32) -> Option<&'static str> {
186    Some(match name {
187        0x1F00 => "gasm",
188        0x1F01 => "gasm null GL",
189        0x1F02 => "OpenGL ES 3.0 (gasm null GL)",
190        0x8B8C => "OpenGL ES GLSL ES 3.00 (gasm null GL)",
191        0x1F03 => "",
192        _ => return None,
193    })
194}
195
196#[derive(Clone, Copy, PartialEq, Eq, Hash)]
197enum Kind {
198    Buffer,
199    Texture,
200    VertexArray,
201    Sampler,
202    Framebuffer,
203    Renderbuffer,
204    Shader,
205    Program,
206    Query,
207    Sync,
208    TransformFeedback,
209}
210
211#[derive(Default)]
212struct Uniforms {
213    by_name: HashMap<String, i32>,
214    count: i32,
215}
216
217/// The gasm:gl state of one guest (model + null GL).
218pub struct Gl {
219    /// `--gl-stats`: calls by name, printed when the game ends
220    pub stats: Option<GlStats>,
221    next: HashMap<Kind, u32>,
222    live: HashMap<Kind, HashSet<u32>>,
223    errors: Vec<u32>,
224    buffers: HashMap<u32, u32>,
225    textures: HashMap<(u32, u32), u32>,
226    /// the target each texture was first bound to (it can't be bound to another)
227    texture_targets: HashMap<u32, u32>,
228    unit: u32,
229    framebuffers: HashMap<u32, u32>,
230    renderbuffer: u32,
231    program: u32,
232    vertex_array: u32,
233    /// every live vertex array (0 included)
234    vaos: HashMap<u32, Vao>,
235    /// buffer -> the (vertex array, slot) pairs pointing at it, so deleting a buffer or a
236    /// vertex array touches only its own entries (Godot makes and frees hundreds a frame)
237    buffer_users: HashMap<u32, HashSet<(u32, u8)>>,
238    unpack: Store,
239    pack: Store,
240    shaders: HashMap<u32, (u32, String)>,
241    uniforms: HashMap<u32, Uniforms>,
242    active: HashMap<u32, u32>,
243    query_ended: HashMap<u32, u64>,
244    sync_made: HashMap<u32, u64>,
245    /// GL errors the model has recorded so far: a call that adds none reaches the backend
246    error_count: u64,
247    /// the GL that executes calls (ANGLE); None: the null GL
248    backend: Option<Box<Backend>>,
249}
250
251/// GL calls by name over the frames that made any (`--gl-stats`); printed on drop, when
252/// the game ends.
253#[derive(Default)]
254pub struct GlStats {
255    calls: HashMap<&'static str, u64>,
256    frames: Option<(u64, u64)>,
257}
258
259impl GlStats {
260    fn count(&mut self, name: &'static str, frame: u64) {
261        *self.calls.entry(name).or_default() += 1;
262        let (first, _) = self.frames.unwrap_or((frame, frame));
263        self.frames = Some((first, frame));
264    }
265
266    /// The report: total calls a frame, then each function's, most first.
267    pub fn report(&self) -> String {
268        let frames = self.frames.map_or(1, |(a, b)| b - a + 1) as f64;
269        let mut calls: Vec<_> = self.calls.iter().map(|(&n, &c)| (n, c)).collect();
270        calls.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
271        let total: u64 = calls.iter().map(|c| c.1).sum();
272        let mut out = format!("[gasm] gl stats: {:.1} calls a frame over {frames} frames", total as f64 / frames);
273        for (n, c) in calls {
274            out += &format!("\n  {:>9.1}  {n}", c as f64 / frames);
275        }
276        out
277    }
278}
279
280impl Drop for GlStats {
281    fn drop(&mut self) {
282        eprintln!("{}", self.report());
283    }
284}
285
286/// Vertex attributes (MAX_VERTEX_ATTRIBS, `null_limit(0x8869)`)
287const MAX_ATTRIBS: usize = 16;
288/// a vertex array's ELEMENT_ARRAY_BUFFER slot, after its attributes
289const ELEMENTS: usize = MAX_ATTRIBS;
290
291/// What the model keeps of a vertex array: its ELEMENT_ARRAY_BUFFER and, per attribute,
292/// (enabled, buffer). A draw with an enabled attribute that has no buffer is
293/// INVALID_OPERATION (WebGL: no client-side arrays).
294#[derive(Clone, Copy, Default)]
295struct Vao {
296    elements: u32,
297    attribs: [(bool, u32); MAX_ATTRIBS],
298}
299
300impl Default for Gl {
301    fn default() -> Self {
302        Gl {
303            stats: None,
304            next: HashMap::default(),
305            live: HashMap::default(),
306            errors: Vec::new(),
307            buffers: HashMap::default(),
308            textures: HashMap::default(),
309            texture_targets: HashMap::default(),
310            unit: 0,
311            framebuffers: HashMap::default(),
312            renderbuffer: 0,
313            program: 0,
314            vertex_array: 0,
315            vaos: HashMap::from_iter([(0, Vao::default())]),
316            buffer_users: HashMap::default(),
317            unpack: STORE,
318            pack: STORE,
319            shaders: HashMap::default(),
320            uniforms: HashMap::default(),
321            active: HashMap::default(),
322            query_ended: HashMap::default(),
323            sync_made: HashMap::default(),
324            error_count: 0,
325            backend: None,
326        }
327    }
328}
329
330impl Gl {
331    fn error(&mut self, e: u32) -> bool {
332        self.error_count += 1;
333        if self.errors.len() < 32 {
334            self.errors.push(e);
335        }
336        false
337    }
338    fn get_error(&mut self) -> u32 {
339        if self.errors.is_empty() {
340            0
341        } else {
342            self.errors.remove(0)
343        }
344    }
345    fn create(&mut self, k: Kind) -> u32 {
346        let next = self.next.entry(k).or_insert(1);
347        let n = *next;
348        *next += 1;
349        self.live.entry(k).or_default().insert(n);
350        n
351    }
352    fn has(&self, k: Kind, n: u32) -> bool {
353        self.live.get(&k).is_some_and(|s| s.contains(&n))
354    }
355    /// name 0 is "none" (valid where unbinding is); an unknown or deleted name is INVALID_OPERATION
356    fn valid(&mut self, k: Kind, n: u32, allow_zero: bool) -> bool {
357        if n == 0 {
358            return allow_zero || self.error(INVALID_VALUE);
359        }
360        self.has(k, n) || self.error(INVALID_OPERATION)
361    }
362    fn remove(&mut self, k: Kind, n: u32) {
363        if n != 0 {
364            if let Some(s) = self.live.get_mut(&k) {
365                s.remove(&n);
366            }
367        }
368    }
369    fn is(&self, k: Kind, n: u32) -> i32 {
370        (n != 0 && self.has(k, n)) as i32
371    }
372    fn target(&mut self, list: &[u32], t: u32) -> bool {
373        list.contains(&t) || self.error(INVALID_ENUM)
374    }
375    fn image_target(&mut self, t: u32) -> bool {
376        t == TEXTURE_2D
377            || (CUBE_POSITIVE_X..=CUBE_NEGATIVE_Z).contains(&t)
378            || self.error(INVALID_ENUM)
379    }
380    fn bound_buffer(&self, t: u32) -> u32 {
381        self.buffers.get(&t).copied().unwrap_or(0)
382    }
383    fn vao_element(&self) -> u32 {
384        self.vaos.get(&self.vertex_array).map_or(0, |v| v.elements)
385    }
386    /// Point a slot of a vertex array (an attribute, or ELEMENTS) at a buffer.
387    fn set_vao_buffer(&mut self, vao: u32, slot: usize, n: u32) {
388        let Some(v) = self.vaos.get_mut(&vao) else { return };
389        let old = std::mem::replace(if slot == ELEMENTS { &mut v.elements } else { &mut v.attribs[slot].1 }, n);
390        if old == n {
391            return;
392        }
393        if let Some(users) = self.buffer_users.get_mut(&old) {
394            users.remove(&(vao, slot as u8));
395            if users.is_empty() {
396                self.buffer_users.remove(&old);
397            }
398        }
399        if n != 0 {
400            self.buffer_users.entry(n).or_default().insert((vao, slot as u8));
401        }
402    }
403    fn bound_texture(&self, t: u32) -> u32 {
404        let t = if (CUBE_POSITIVE_X..=CUBE_NEGATIVE_Z).contains(&t) {
405            TEXTURE_CUBE_MAP
406        } else {
407            t
408        };
409        self.textures.get(&(self.unit, t)).copied().unwrap_or(0)
410    }
411    fn need_texture(&mut self, t: u32) -> bool {
412        self.bound_texture(t) != 0 || self.error(INVALID_OPERATION)
413    }
414    /// An attribute index of the bound vertex array (None: out of range, INVALID_VALUE).
415    fn attrib(&mut self, i: u32) -> Option<usize> {
416        if i as usize >= MAX_ATTRIBS {
417            self.error(INVALID_VALUE);
418            return None;
419        }
420        Some(i as usize)
421    }
422    /// A draw may go ahead: every enabled attribute of the bound vertex array has a buffer.
423    fn attribs_ready(&mut self) -> bool {
424        let missing = self.vaos.get(&self.vertex_array).is_some_and(|v| v.attribs.iter().any(|&(on, b)| on && b == 0));
425        !missing || self.error(INVALID_OPERATION)
426    }
427    fn non_negative(&mut self, v: &[i32]) -> bool {
428        v.iter().all(|&x| x >= 0) || self.error(INVALID_VALUE)
429    }
430    fn buffer_bound(&mut self, t: u32) -> bool {
431        if !self.target(BUFFER_TARGETS, t) {
432            return false;
433        }
434        let b = if t == ELEMENT_ARRAY_BUFFER {
435            self.vao_element()
436        } else {
437            self.bound_buffer(t)
438        };
439        b != 0 || self.error(INVALID_OPERATION)
440    }
441    fn need_renderbuffer(&mut self, t: u32) -> bool {
442        (self.target(&[RENDERBUFFER], t) && self.renderbuffer != 0)
443            || (t == RENDERBUFFER && self.error(INVALID_OPERATION))
444    }
445    fn pixel_store(&mut self, pname: u32, v: i32) -> bool {
446        let v = v as i64;
447        let (unpack, field): (bool, fn(&mut Store) -> &mut i64) = match pname {
448            0x0CF5 => (true, |s| &mut s.alignment),
449            0x0D05 => (false, |s| &mut s.alignment),
450            0x0CF2 => (true, |s| &mut s.row_length),
451            0x806E => (true, |s| &mut s.image_height),
452            0x0CF4 => (true, |s| &mut s.skip_pixels),
453            0x0CF3 => (true, |s| &mut s.skip_rows),
454            0x806D => (true, |s| &mut s.skip_images),
455            0x0D02 => (false, |s| &mut s.row_length),
456            0x0D04 => (false, |s| &mut s.skip_pixels),
457            0x0D03 => (false, |s| &mut s.skip_rows),
458            _ => return true,
459        };
460        let alignment = pname == 0x0CF5 || pname == 0x0D05;
461        if if alignment {
462            ![1, 2, 4, 8].contains(&v)
463        } else {
464            v < 0
465        } {
466            return self.error(INVALID_VALUE);
467        }
468        *field(if unpack {
469            &mut self.unpack
470        } else {
471            &mut self.pack
472        }) = v;
473        true
474    }
475    /// Queries every runner answers from the model (bindings and the pixel store).
476    fn model_param(&self, pname: u32) -> Option<i64> {
477        let tex = |t| self.bound_texture(t) as i64;
478        Some(match pname {
479            0x8894 => self.bound_buffer(ARRAY_BUFFER) as i64,
480            0x8895 => self.vao_element() as i64,
481            0x8F36 => self.bound_buffer(COPY_READ_BUFFER) as i64,
482            0x8F37 => self.bound_buffer(COPY_WRITE_BUFFER) as i64,
483            0x88ED => self.bound_buffer(PIXEL_PACK_BUFFER) as i64,
484            0x88EF => self.bound_buffer(PIXEL_UNPACK_BUFFER) as i64,
485            0x8A28 => self.bound_buffer(UNIFORM_BUFFER) as i64,
486            0x8C8F => self.bound_buffer(TRANSFORM_FEEDBACK_BUFFER) as i64,
487            0x8B8D => self.program as i64,
488            0x8069 => tex(TEXTURE_2D),
489            0x8514 => tex(TEXTURE_CUBE_MAP),
490            0x806A => tex(TEXTURE_3D),
491            0x8C1D => tex(TEXTURE_2D_ARRAY),
492            0x84E0 => (TEXTURE0 + self.unit) as i64,
493            0x8CA6 => self
494                .framebuffers
495                .get(&DRAW_FRAMEBUFFER)
496                .copied()
497                .unwrap_or(0) as i64,
498            0x8CAA => self
499                .framebuffers
500                .get(&READ_FRAMEBUFFER)
501                .copied()
502                .unwrap_or(0) as i64,
503            0x8CA7 => self.renderbuffer as i64,
504            0x85B5 => self.vertex_array as i64,
505            0x0CF5 => self.unpack.alignment,
506            0x0D05 => self.pack.alignment,
507            0x0CF2 => self.unpack.row_length,
508            0x806E => self.unpack.image_height,
509            0x0CF4 => self.unpack.skip_pixels,
510            0x0CF3 => self.unpack.skip_rows,
511            0x806D => self.unpack.skip_images,
512            0x0D02 => self.pack.row_length,
513            0x0D04 => self.pack.skip_pixels,
514            0x0D03 => self.pack.skip_rows,
515            _ => return None,
516        })
517    }
518    fn params(&self, pname: u32) -> Vec<i64> {
519        match self.model_param(pname) {
520            Some(v) => vec![v],
521            None => null_limit(pname).to_vec(),
522        }
523    }
524    /// The uniform a location names in the current program (None: ignored or an error).
525    fn uniform_target(&mut self, loc: i32) -> bool {
526        if loc == -1 {
527            return false;
528        }
529        if self.program == 0 {
530            return self.error(INVALID_OPERATION);
531        }
532        let known = self
533            .uniforms
534            .get(&self.program)
535            .is_some_and(|u| loc >= 0 && loc < u.count);
536        known || self.error(INVALID_OPERATION)
537    }
538}
539
540/// Link every gasm:gl import (signatures from abi.json, via gl_sigs.rs).
541pub(crate) fn add_gl_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
542    let engine = linker.engine().clone();
543    for &(name, params, result) in sigs::FUNCTIONS {
544        let ty = |c: char| match c {
545            'i' => ValType::I32,
546            'I' => ValType::I64,
547            'f' => ValType::F32,
548            _ => unreachable!("gl_sigs.rs: type {c}"),
549        };
550        let ft = FuncType::new(&engine, params.chars().map(ty), result.chars().map(ty));
551        let f32_result = result == "f";
552        assert!(params.len() <= MAX_PARAMS && result.len() <= 1, "gl_sigs.rs: {name}");
553        let zero = |c: u8| match c {
554            b'I' => Val::I64(0),
555            b'f' => Val::F32(0),
556            _ => Val::I32(0),
557        };
558        // Unchecked: arguments go from the raw slots to a stack array (func_new collects them
559        // into a heap vector per call, which was a good part of a GL call's cost).
560        // SAFETY: wasmtime calls this with `ft`'s parameters in the first slots, typed as
561        // `params` says, and room for the result in slot 0; we read exactly those and write
562        // the result with its declared type.
563        unsafe {
564            linker.func_new_unchecked("gasm:gl", name, ft, move |mut c, raw| {
565                let mut args = [Val::I32(0); MAX_PARAMS];
566                for (i, t) in params.bytes().enumerate() {
567                    let r = raw[i].assume_init();
568                    args[i] = match t {
569                        b'I' => Val::I64(r.get_i64()),
570                        b'f' => Val::F32(r.get_f32()),
571                        _ => Val::I32(r.get_i32()),
572                    };
573                }
574                let mut res = [result.bytes().next().map_or(Val::I32(0), zero)];
575                let results = &mut res[..result.len()];
576                call(&mut c, name, f32_result, &args[..params.len()], results)?;
577                if let Some(v) = results.first() {
578                    raw[0] = MaybeUninit::new(match *v {
579                        Val::I64(x) => ValRaw::i64(x),
580                        Val::F32(b) => ValRaw::f32(b),
581                        Val::I32(x) => ValRaw::i32(x),
582                        _ => unreachable!("gasm:gl results are numbers"),
583                    });
584                }
585                Ok(())
586            })?;
587        }
588    }
589    Ok(())
590}
591
592/// The longest gasm:gl parameter list (tex_sub_image_3d)
593const MAX_PARAMS: usize = 12;
594
595/// Write `values` to guest memory as i32 / f32 / i64 (up to `count` of them); returns how many there are.
596fn put(mem: &mut [u8], dst: u32, count: u32, values: &[i64], kind: char) -> wasmtime::Result<i32> {
597    let n = values.len().min(count as usize);
598    let size = if kind == 'I' { 8 } else { 4 };
599    if n > 0 {
600        let out = guest_slice_mut(mem, dst, (n * size) as u64)?;
601        for (i, &v) in values[..n].iter().enumerate() {
602            match kind {
603                'f' => out[i * 4..i * 4 + 4].copy_from_slice(&(v as f32).to_le_bytes()),
604                'I' => out[i * 8..i * 8 + 8].copy_from_slice(&v.to_le_bytes()),
605                _ => out[i * 4..i * 4 + 4].copy_from_slice(&(v as i32).to_le_bytes()),
606            }
607        }
608    }
609    Ok(values.len() as i32)
610}
611
612fn text(mem: &mut [u8], dst: u32, cap: u32, s: &str) -> wasmtime::Result<i32> {
613    if s.len() <= cap as usize {
614        guest_slice_mut(mem, dst, s.len() as u64)?.copy_from_slice(s.as_bytes());
615    }
616    Ok(s.len() as i32)
617}
618
619fn guest_str(mem: &[u8], ptr: u32, len: u32) -> wasmtime::Result<String> {
620    let b = guest_slice(mem, ptr, len as u64)?;
621    std::str::from_utf8(b)
622        .map(str::to_owned)
623        .map_err(|_| wasmtime::format_err!("string argument is not UTF-8"))
624}
625
626/// One gasm:gl call: the model checks it (and answers what it can); if it recorded
627/// no GL error and didn't trap, the backend (if any) executes it, as WebGL does in
628/// the browser runner.
629fn call(c: &mut Caller<'_, Host>, name: &'static str, f32_result: bool, args: &[Val], results: &mut [Val]) -> wasmtime::Result<()> {
630    let frame = c.data().frame_index;
631    if let Some(s) = &mut c.data_mut().gl.stats {
632        s.count(name, frame);
633    }
634    let before = c.data().gl.error_count;
635    model_call(c, name, f32_result, args, results)?;
636    if c.data().gl.backend.is_none() || c.data().gl.error_count != before {
637        return Ok(());
638    }
639    let mem = memory(c)?;
640    let (mem, host) = mem.data_and_store_mut(c);
641    let gl = &mut host.gl;
642    let mut be = gl.backend.take().expect("backend");
643    let r = backend::forward(&mut be, gl, name, args, results, mem);
644    gl.backend = Some(be);
645    r
646}
647
648/// The model's part of a call. `a(i)` is argument i as the i32 the guest passed (`u(i)` as u32).
649fn model_call(
650    c: &mut Caller<'_, Host>,
651    name: &str,
652    f32_result: bool,
653    args: &[Val],
654    results: &mut [Val],
655) -> wasmtime::Result<()> {
656    let mem = memory(c)?;
657    let (mem, host) = mem.data_and_store_mut(c);
658    let hashing = host.hashing;
659    let frame = host.frame_index;
660    let catch_up = host.catch_up;
661    let (gl, hash) = (&mut host.gl, &mut host.video_hash);
662    let a = |i: usize| -> i32 {
663        match args[i] {
664            Val::I32(v) => v,
665            Val::I64(v) => v as i32,
666            Val::F32(b) => b as i32,
667            _ => 0,
668        }
669    };
670    let u = |i: usize| a(i) as u32;
671    let mut fold = |header: &[u32], payload: Option<&[u8]>| {
672        if !hashing {
673            return;
674        }
675        for v in header {
676            hash.update(&v.to_le_bytes());
677        }
678        if let Some(p) = payload {
679            hash.update(p);
680        }
681    };
682    // the result: 0 unless set; an f32 for the f32 results
683    if let Some(r) = results.first_mut() {
684        *r = if f32_result { Val::F32(0) } else { Val::I32(0) };
685    }
686    let mut ret = |v: i64| {
687        if let Some(r) = results.first_mut() {
688            *r = match r {
689                Val::F32(_) => Val::F32((v as f32).to_bits()),
690                _ => Val::I32(v as i32),
691            };
692        }
693    };
694    use Kind::*;
695    match name {
696        // ---- frames, context ----
697        "width" => ret(1280),
698        "height" => ret(720),
699        // 0 only on catch-up frames: headless runs draw too (as in the JS runner)
700        "frame_shown" => ret(!catch_up as i64),
701        "get_error" => ret(gl.get_error() as i64),
702        "get_string" => match null_string(u(0)) {
703            Some(s) => ret(text(mem, u(1), u(2), s)? as i64),
704            None => {
705                gl.error(INVALID_ENUM);
706                ret(-1)
707            }
708        },
709        "enable_extension" => {
710            guest_str(mem, u(0), u(1))?;
711            ret(0)
712        }
713        "get_integerv" | "get_floatv" | "get_integer64v" => {
714            let v = gl.params(u(0));
715            let kind = match name {
716                "get_floatv" => 'f',
717                "get_integer64v" => 'I',
718                _ => 'i',
719            };
720            ret(put(mem, u(1), u(2), &v, kind)? as i64)
721        }
722        "get_integeri_v" => ret(put(mem, u(2), u(3), &[0], 'i')? as i64),
723        "get_internalformativ" => {
724            ret(put(mem, u(3), u(4), &[if u(2) == 0x9380 { 1 } else { 4 }], 'i')? as i64)
725        }
726        "get_shader_precision_format" => {
727            put(mem, u(2), 3, &[127, 127, 23], 'i')?;
728        }
729        "active_texture" => {
730            let unit = u(0).wrapping_sub(TEXTURE0);
731            if unit >= 32 {
732                gl.error(INVALID_ENUM);
733            } else {
734                gl.unit = unit;
735            }
736        }
737        "is_enabled" => ret(0),
738        "pixel_storei" => {
739            gl.pixel_store(u(0), a(1));
740        }
741        "scissor" | "viewport" => {
742            gl.non_negative(&[a(2), a(3)]);
743        }
744        // state that only a real GL keeps
745        "present"
746        | "blend_color"
747        | "blend_equation"
748        | "blend_equation_separate"
749        | "blend_func"
750        | "blend_func_separate"
751        | "clear"
752        | "clear_color"
753        | "clear_depthf"
754        | "clear_stencil"
755        | "color_mask"
756        | "cull_face"
757        | "depth_func"
758        | "depth_mask"
759        | "depth_rangef"
760        | "disable"
761        | "enable"
762        | "front_face"
763        | "hint"
764        | "line_width"
765        | "polygon_offset"
766        | "sample_coverage"
767        | "stencil_func"
768        | "stencil_func_separate"
769        | "stencil_mask"
770        | "stencil_mask_separate"
771        | "stencil_op"
772        | "stencil_op_separate"
773        | "finish"
774        | "flush"
775        | "vertex_attrib_divisor"
776        | "vertex_attrib4f"
777        | "vertex_attribi4i"
778        | "vertex_attribi4ui"
779        | "clear_bufferfi"
780        | "blit_framebuffer"
781        | "read_buffer"
782        | "begin_transform_feedback"
783        | "end_transform_feedback"
784        | "pause_transform_feedback"
785        | "resume_transform_feedback" => {}
786
787        // ---- buffers ----
788        "create_buffer" => ret(gl.create(Buffer) as i64),
789        "delete_buffer" => {
790            let n = u(0);
791            gl.buffers.retain(|_, b| *b != n);
792            // detached from every vertex array, as WebGL does
793            for (v, slot) in gl.buffer_users.remove(&n).unwrap_or_default() {
794                if let Some(v) = gl.vaos.get_mut(&v) {
795                    *(if slot as usize == ELEMENTS { &mut v.elements } else { &mut v.attribs[slot as usize].1 }) = 0;
796                }
797            }
798            gl.remove(Buffer, n);
799        }
800        "is_buffer" => ret(gl.is(Buffer, u(0)) as i64),
801        "bind_buffer" => {
802            let (t, n) = (u(0), u(1));
803            if gl.target(BUFFER_TARGETS, t) && gl.valid(Buffer, n, true) {
804                if t == ELEMENT_ARRAY_BUFFER {
805                    gl.set_vao_buffer(gl.vertex_array, ELEMENTS, n);
806                } else {
807                    gl.buffers.insert(t, n);
808                }
809            }
810        }
811        "bind_buffer_base" | "bind_buffer_range" => {
812            let (t, n) = (u(0), u(2));
813            if gl.target(INDEXED_TARGETS, t) && gl.valid(Buffer, n, true) {
814                gl.buffers.insert(t, n);
815            }
816        }
817        "buffer_data" => {
818            let (t, ptr, len) = (u(0), u(1), u(2));
819            let data = if ptr != 0 {
820                Some(guest_slice(mem, ptr, len as u64)?)
821            } else {
822                None
823            };
824            if gl.buffer_bound(t) {
825                fold(&[1, t, 0, len], data);
826            }
827        }
828        "buffer_sub_data" => {
829            let (t, off, ptr, len) = (u(0), u(1), u(2), u(3));
830            let data = guest_slice(mem, ptr, len as u64)?;
831            if gl.buffer_bound(t) {
832                fold(&[1, t, off, len], Some(data));
833            }
834        }
835        "copy_buffer_sub_data" => {
836            let _ = gl.buffer_bound(u(0)) && gl.buffer_bound(u(1));
837        }
838        "get_buffer_sub_data" => {
839            let out = guest_slice_mut(mem, u(2), u(3) as u64)?;
840            if gl.buffer_bound(u(0)) {
841                out.fill(0);
842            }
843        }
844        "get_buffer_parameteriv" => {
845            gl.buffer_bound(u(0));
846            ret(0)
847        }
848
849        // ---- vertex arrays ----
850        "create_vertex_array" => {
851            let n = gl.create(VertexArray);
852            gl.vaos.insert(n, Vao::default());
853            ret(n as i64)
854        }
855        "delete_vertex_array" => {
856            let n = u(0);
857            if gl.vertex_array == n {
858                gl.vertex_array = 0;
859            }
860            if n != 0 && gl.vaos.contains_key(&n) {
861                for slot in 0..=ELEMENTS {
862                    gl.set_vao_buffer(n, slot, 0);
863                }
864                gl.vaos.remove(&n);
865            }
866            gl.remove(VertexArray, n);
867        }
868        "is_vertex_array" => ret(gl.is(VertexArray, u(0)) as i64),
869        "bind_vertex_array" => {
870            if gl.valid(VertexArray, u(0), true) {
871                gl.vertex_array = u(0);
872            }
873        }
874        "vertex_attrib_pointer" | "vertex_attrib_ipointer" => {
875            let off = if name == "vertex_attrib_pointer" {
876                a(5)
877            } else {
878                a(4)
879            };
880            let buffer = gl.bound_buffer(ARRAY_BUFFER);
881            if let Some(i) = gl.attrib(u(0)) {
882                if buffer == 0 && off != 0 {
883                    gl.error(INVALID_OPERATION);
884                } else {
885                    gl.set_vao_buffer(gl.vertex_array, i, buffer);
886                }
887            }
888        }
889        "enable_vertex_attrib_array" | "disable_vertex_attrib_array" => {
890            let on = name == "enable_vertex_attrib_array";
891            if let Some(i) = gl.attrib(u(0)) {
892                if let Some(v) = gl.vaos.get_mut(&gl.vertex_array) {
893                    v.attribs[i].0 = on;
894                }
895            }
896        }
897        "get_vertex_attribiv" | "get_vertex_attrib_offset" => ret(0),
898        "get_vertex_attribfv" => ret(put(mem, u(2), u(3), &[0, 0, 0, 1], 'f')? as i64),
899
900        // ---- drawing ----
901        "draw_arrays" => {
902            let _ = gl.non_negative(&[a(1), a(2)]) && gl.attribs_ready();
903        }
904        "draw_elements" => {
905            let _ = gl.non_negative(&[a(1)]) && gl.attribs_ready();
906        }
907        "draw_arrays_instanced" => {
908            let _ = gl.non_negative(&[a(1), a(2), a(3)]) && gl.attribs_ready();
909        }
910        "draw_elements_instanced" => {
911            let _ = gl.non_negative(&[a(1), a(4)]) && gl.attribs_ready();
912        }
913        "draw_range_elements" => {
914            let _ = gl.non_negative(&[a(3)]) && (a(2) >= a(1) || gl.error(INVALID_VALUE)) && gl.attribs_ready();
915        }
916        "draw_buffers" => {
917            guest_slice(mem, u(0), u(1) as u64 * 4)?;
918        }
919        "clear_bufferiv" | "clear_bufferuiv" | "clear_bufferfv" => {
920            guest_slice(mem, u(2), u(3) as u64 * 4)?;
921        }
922
923        // ---- textures ----
924        "create_texture" => ret(gl.create(Texture) as i64),
925        "delete_texture" => {
926            let n = u(0);
927            gl.textures.retain(|_, t| *t != n);
928            gl.texture_targets.remove(&n);
929            gl.remove(Texture, n);
930        }
931        "is_texture" => ret(gl.is(Texture, u(0)) as i64),
932        "bind_texture" => {
933            let (t, n) = (u(0), u(1));
934            if gl.target(TEXTURE_TARGETS, t) && gl.valid(Texture, n, true) {
935                if n != 0 && *gl.texture_targets.entry(n).or_insert(t) != t {
936                    gl.error(INVALID_OPERATION);
937                } else {
938                    gl.textures.insert((gl.unit, t), n);
939                }
940            }
941        }
942        "tex_parameteri"
943        | "tex_parameterf"
944        | "get_tex_parameteriv"
945        | "get_tex_parameterfv"
946        | "generate_mipmap" => {
947            let _ = gl.target(TEXTURE_TARGETS, u(0)) && gl.need_texture(u(0));
948            ret(0)
949        }
950        "tex_image_2d" | "tex_image_3d" | "tex_sub_image_2d" | "tex_sub_image_3d" => {
951            // (target, level, [ifmt], [x, y, [z]], w, h, [d], [border], format, type, ptr, len)
952            let t = u(0);
953            let sub = name.starts_with("tex_sub");
954            let three = name.ends_with("3d");
955            let (ifmt, x, y, z, w, h, d, fi) = match (sub, three) {
956                (false, false) => (u(2), 0, 0, 0, a(3), a(4), 1, 6),
957                (false, true) => (u(2), 0, 0, 0, a(3), a(4), a(5), 7),
958                (true, false) => (0, a(2), a(3), 0, a(4), a(5), 1, 6),
959                (true, true) => (0, a(2), a(3), a(4), a(5), a(6), a(7), 8),
960            };
961            let (format, ty, ptr, len) = (u(fi), u(fi + 1), u(fi + 2), u(fi + 3));
962            let level = a(1);
963            let ok = if three {
964                gl.target(VOLUME_TARGETS, t)
965            } else {
966                gl.image_target(t)
967            } && gl.need_texture(t)
968                && gl.non_negative(&[level, x, y, z, w, h, d]);
969            if !ok {
970                return Ok(());
971            }
972            // pixels: none, guest bytes, or (unpack buffer bound) an offset into it
973            let unpack = gl.bound_buffer(PIXEL_UNPACK_BUFFER) != 0;
974            let mut data = None;
975            if !unpack && ptr != 0 {
976                let bpp = pixel_bytes(format, ty);
977                if bpp == 0 {
978                    gl.error(INVALID_ENUM);
979                    return Ok(());
980                }
981                let need = image_bytes(w as i64, h as i64, d as i64, bpp as i64, &gl.unpack);
982                if (len as i64) < need {
983                    bail!(
984                        "gasm:gl: {len} bytes of pixels for a {w}x{h}x{d} image that needs {need}"
985                    );
986                }
987                data = Some(guest_slice(mem, ptr, len as u64)?);
988            }
989            if sub && data.is_none() && !unpack {
990                gl.error(INVALID_VALUE);
991                return Ok(());
992            }
993            let n = if data.is_some() { len } else { 0 };
994            fold(
995                &[
996                    2,
997                    t,
998                    level as u32,
999                    ifmt,
1000                    x as u32,
1001                    y as u32,
1002                    z as u32,
1003                    w as u32,
1004                    h as u32,
1005                    d as u32,
1006                    format,
1007                    ty,
1008                    n,
1009                ],
1010                data,
1011            );
1012        }
1013        "tex_storage_2d" | "tex_storage_3d" => {
1014            let t = u(0);
1015            let three = name.ends_with("3d");
1016            let list: &[u32] = if three {
1017                VOLUME_TARGETS
1018            } else {
1019                &[TEXTURE_2D, TEXTURE_CUBE_MAP]
1020            };
1021            let positive = a(1) > 0 && a(3) > 0 && a(4) > 0 && (!three || a(5) > 0);
1022            let _ =
1023                gl.target(list, t) && gl.need_texture(t) && (positive || gl.error(INVALID_VALUE));
1024        }
1025        "compressed_tex_image_2d"
1026        | "compressed_tex_image_3d"
1027        | "compressed_tex_sub_image_2d"
1028        | "compressed_tex_sub_image_3d" => {
1029            let t = u(0);
1030            let sub = name.contains("sub");
1031            let three = name.ends_with("3d");
1032            // header [2, t, level, ifmt|0, x, y, z, w, h, d, ifmt|format, 0, len]
1033            let (ifmt, x, y, z, w, h, d, fmt, pi) = match (sub, three) {
1034                (false, false) => (u(2), 0, 0, 0, a(3), a(4), 1, u(2), 6),
1035                (false, true) => (u(2), 0, 0, 0, a(3), a(4), a(5), u(2), 7),
1036                (true, false) => (0, a(2), a(3), 0, a(4), a(5), 1, u(6), 7),
1037                (true, true) => (0, a(2), a(3), a(4), a(5), a(6), a(7), u(8), 9),
1038            };
1039            let (ptr, len) = (u(pi), u(pi + 1));
1040            let data = guest_slice(mem, ptr, len as u64)?;
1041            let level = a(1);
1042            let ok = if three {
1043                gl.target(VOLUME_TARGETS, t)
1044            } else {
1045                gl.image_target(t)
1046            } && gl.need_texture(t)
1047                && {
1048                    let v: &[i32] = if sub {
1049                        &[level, x, y, z, w, h, d]
1050                    } else {
1051                        &[level, w, h, d]
1052                    };
1053                    gl.non_negative(v)
1054                };
1055            if ok {
1056                fold(
1057                    &[
1058                        2,
1059                        t,
1060                        level as u32,
1061                        ifmt,
1062                        x as u32,
1063                        y as u32,
1064                        z as u32,
1065                        w as u32,
1066                        h as u32,
1067                        d as u32,
1068                        fmt,
1069                        0,
1070                        len,
1071                    ],
1072                    Some(data),
1073                );
1074            }
1075        }
1076        "copy_tex_image_2d" | "copy_tex_sub_image_2d" => {
1077            let _ = gl.image_target(u(0)) && gl.need_texture(u(0));
1078        }
1079        "copy_tex_sub_image_3d" => {
1080            gl.need_texture(u(0));
1081        }
1082
1083        // ---- samplers ----
1084        "create_sampler" => ret(gl.create(Sampler) as i64),
1085        "delete_sampler" => gl.remove(Sampler, u(0)),
1086        "is_sampler" => ret(gl.is(Sampler, u(0)) as i64),
1087        "bind_sampler" => {
1088            gl.valid(Sampler, u(1), true);
1089        }
1090        "sampler_parameteri"
1091        | "sampler_parameterf"
1092        | "get_sampler_parameteriv"
1093        | "get_sampler_parameterfv" => {
1094            gl.valid(Sampler, u(0), false);
1095            ret(0)
1096        }
1097
1098        // ---- framebuffers, renderbuffers ----
1099        "create_framebuffer" => ret(gl.create(Framebuffer) as i64),
1100        "delete_framebuffer" => {
1101            let n = u(0);
1102            gl.framebuffers.retain(|_, f| *f != n);
1103            gl.remove(Framebuffer, n);
1104        }
1105        "is_framebuffer" => ret(gl.is(Framebuffer, u(0)) as i64),
1106        "bind_framebuffer" => {
1107            let (t, n) = (u(0), u(1));
1108            if gl.target(FB_TARGETS, t) && gl.valid(Framebuffer, n, true) {
1109                if t == FRAMEBUFFER {
1110                    gl.framebuffers.insert(DRAW_FRAMEBUFFER, n);
1111                    gl.framebuffers.insert(READ_FRAMEBUFFER, n);
1112                } else {
1113                    gl.framebuffers.insert(t, n);
1114                }
1115            }
1116        }
1117        "check_framebuffer_status" => ret(if gl.target(FB_TARGETS, u(0)) {
1118            FRAMEBUFFER_COMPLETE as i64
1119        } else {
1120            0
1121        }),
1122        "framebuffer_texture_2d" => {
1123            let _ = gl.target(FB_TARGETS, u(0)) && gl.valid(Texture, u(3), true);
1124        }
1125        "framebuffer_texture_layer" => {
1126            let _ = gl.target(FB_TARGETS, u(0)) && gl.valid(Texture, u(2), true);
1127        }
1128        "framebuffer_renderbuffer" => {
1129            let _ = gl.target(FB_TARGETS, u(0)) && gl.valid(Renderbuffer, u(3), true);
1130        }
1131        "get_framebuffer_attachment_parameteriv" => {
1132            gl.target(FB_TARGETS, u(0));
1133            ret(0)
1134        }
1135        "invalidate_framebuffer" | "invalidate_sub_framebuffer" => {
1136            guest_slice(mem, u(1), u(2) as u64 * 4)?;
1137        }
1138        "read_pixels" => {
1139            let (w, h, format, ty, dst, len) = (a(2), a(3), u(4), u(5), u(6), u(7));
1140            if !gl.non_negative(&[w, h]) || gl.bound_buffer(PIXEL_PACK_BUFFER) != 0 {
1141                return Ok(());
1142            }
1143            let bpp = pixel_bytes(format, ty);
1144            if bpp == 0 {
1145                gl.error(INVALID_ENUM);
1146                return Ok(());
1147            }
1148            let need = image_bytes(w as i64, h as i64, 1, bpp as i64, &gl.pack);
1149            if (len as i64) < need {
1150                bail!("gasm:gl: read_pixels: {len} bytes for {w}x{h} that needs {need}");
1151            }
1152            guest_slice_mut(mem, dst, len as u64)?[..need as usize].fill(0);
1153        }
1154        "create_renderbuffer" => ret(gl.create(Renderbuffer) as i64),
1155        "delete_renderbuffer" => {
1156            if gl.renderbuffer == u(0) {
1157                gl.renderbuffer = 0;
1158            }
1159            gl.remove(Renderbuffer, u(0));
1160        }
1161        "is_renderbuffer" => ret(gl.is(Renderbuffer, u(0)) as i64),
1162        "bind_renderbuffer" => {
1163            if gl.target(&[RENDERBUFFER], u(0)) && gl.valid(Renderbuffer, u(1), true) {
1164                gl.renderbuffer = u(1);
1165            }
1166        }
1167        "renderbuffer_storage" => {
1168            let _ = gl.need_renderbuffer(u(0)) && gl.non_negative(&[a(2), a(3)]);
1169        }
1170        "renderbuffer_storage_multisample" => {
1171            let _ = gl.need_renderbuffer(u(0)) && gl.non_negative(&[a(1), a(3), a(4)]);
1172        }
1173        "get_renderbuffer_parameteriv" => {
1174            gl.need_renderbuffer(u(0));
1175            ret(0)
1176        }
1177
1178        // ---- shaders and programs ----
1179        "create_shader" => {
1180            let ty = u(0);
1181            if ty != VERTEX_SHADER && ty != FRAGMENT_SHADER {
1182                gl.error(INVALID_ENUM);
1183                ret(0);
1184                return Ok(());
1185            }
1186            let n = gl.create(Shader);
1187            gl.shaders.insert(n, (ty, String::new()));
1188            ret(n as i64)
1189        }
1190        "delete_shader" => gl.remove(Shader, u(0)),
1191        "is_shader" => ret(gl.is(Shader, u(0)) as i64),
1192        "shader_source" => {
1193            let src = guest_str(mem, u(1), u(2))?;
1194            if gl.valid(Shader, u(0), false) {
1195                if let Some(s) = gl.shaders.get_mut(&u(0)) {
1196                    s.1 = src;
1197                }
1198            }
1199        }
1200        "compile_shader" => {
1201            gl.valid(Shader, u(0), false);
1202        }
1203        "get_shaderiv" => {
1204            if !gl.valid(Shader, u(0), false) {
1205                ret(0);
1206                return Ok(());
1207            }
1208            let (ty, src) = gl.shaders.get(&u(0)).cloned().unwrap_or_default();
1209            ret(match u(1) {
1210                SHADER_TYPE => ty as i64,
1211                SHADER_SOURCE_LENGTH if !src.is_empty() => src.len() as i64 + 1,
1212                COMPILE_STATUS => 1,
1213                _ => 0,
1214            })
1215        }
1216        "get_shader_info_log" => ret(if gl.valid(Shader, u(0), false) {
1217            text(mem, u(1), u(2), "")? as i64
1218        } else {
1219            -1
1220        }),
1221        "get_shader_source" => {
1222            if gl.valid(Shader, u(0), false) {
1223                let src = gl
1224                    .shaders
1225                    .get(&u(0))
1226                    .map(|s| s.1.clone())
1227                    .unwrap_or_default();
1228                ret(text(mem, u(1), u(2), &src)? as i64)
1229            } else {
1230                ret(-1)
1231            }
1232        }
1233        "create_program" => ret(gl.create(Program) as i64),
1234        "delete_program" => {
1235            let n = u(0);
1236            if gl.program == n {
1237                gl.program = 0;
1238            }
1239            gl.uniforms.remove(&n);
1240            gl.remove(Program, n);
1241        }
1242        "is_program" => ret(gl.is(Program, u(0)) as i64),
1243        "attach_shader" | "detach_shader" => {
1244            let _ = gl.valid(Program, u(0), false) && gl.valid(Shader, u(1), false);
1245        }
1246        "link_program" => {
1247            if gl.valid(Program, u(0), false) {
1248                gl.uniforms.remove(&u(0)); // locations are the link's
1249            }
1250        }
1251        "use_program" => {
1252            if gl.valid(Program, u(0), true) {
1253                gl.program = u(0);
1254            }
1255        }
1256        "validate_program" | "uniform_block_binding" => {
1257            gl.valid(Program, u(0), false);
1258        }
1259        "get_programiv" => {
1260            let ok = gl.valid(Program, u(0), false);
1261            ret((ok && (u(1) == LINK_STATUS || u(1) == VALIDATE_STATUS)) as i64)
1262        }
1263        "get_program_info_log" | "get_active_uniform_block_name" => {
1264            let (dst, cap) = if name == "get_program_info_log" {
1265                (u(1), u(2))
1266            } else {
1267                (u(2), u(3))
1268            };
1269            ret(if gl.valid(Program, u(0), false) {
1270                text(mem, dst, cap, "")? as i64
1271            } else {
1272                -1
1273            })
1274        }
1275        "get_attached_shaders" => {
1276            if gl.valid(Program, u(0), false) {
1277                put(mem, u(1), u(2), &[], 'i')?;
1278            }
1279            ret(0)
1280        }
1281        "bind_attrib_location" => {
1282            guest_str(mem, u(2), u(3))?;
1283            gl.valid(Program, u(0), false);
1284        }
1285        "get_attrib_location" | "get_frag_data_location" => {
1286            guest_str(mem, u(1), u(2))?;
1287            ret(if gl.valid(Program, u(0), false) {
1288                0
1289            } else {
1290                -1
1291            })
1292        }
1293        "get_active_attrib" | "get_active_uniform" | "get_transform_feedback_varying" => {
1294            gl.valid(Program, u(0), false);
1295            ret(-1)
1296        }
1297        // locations are numbered per program in the order the guest asks for them
1298        "get_uniform_location" => {
1299            let name = guest_str(mem, u(1), u(2))?;
1300            if !gl.valid(Program, u(0), false) {
1301                ret(-1);
1302                return Ok(());
1303            }
1304            let table = gl.uniforms.entry(u(0)).or_default();
1305            let loc = match table.by_name.get(&name) {
1306                Some(&l) => l,
1307                None => {
1308                    let l = table.count;
1309                    table.count += 1;
1310                    table.by_name.insert(name, l);
1311                    l
1312                }
1313            };
1314            ret(loc as i64)
1315        }
1316        "get_uniform_index" | "get_uniform_block_index" => {
1317            guest_str(mem, u(1), u(2))?;
1318            ret(if gl.valid(Program, u(0), false) {
1319                0
1320            } else {
1321                INVALID_INDEX as i32 as i64
1322            })
1323        }
1324        "get_active_uniformsiv" => {
1325            let count = u(2);
1326            guest_slice(mem, u(1), count as u64 * 4)?;
1327            if gl.valid(Program, u(0), false) {
1328                put(mem, u(4), count, &vec![0; count as usize], 'i')?;
1329            }
1330        }
1331        "get_active_uniform_blockiv" => ret(if gl.valid(Program, u(0), false) {
1332            put(mem, u(3), u(4), &[0], 'i')? as i64
1333        } else {
1334            -1
1335        }),
1336        "get_uniformfv" | "get_uniformiv" | "get_uniformuiv" => {
1337            if !gl.valid(Program, u(0), false) {
1338                ret(-1);
1339                return Ok(());
1340            }
1341            let loc = a(1);
1342            if !gl
1343                .uniforms
1344                .get(&u(0))
1345                .is_some_and(|t| loc >= 0 && loc < t.count)
1346            {
1347                gl.error(INVALID_OPERATION);
1348                ret(-1);
1349                return Ok(());
1350            }
1351            ret(put(
1352                mem,
1353                u(2),
1354                u(3),
1355                &[0],
1356                if name == "get_uniformfv" { 'f' } else { 'i' },
1357            )? as i64)
1358        }
1359        "transform_feedback_varyings" => {
1360            let bytes = guest_slice(mem, u(1), u(2) as u64)?;
1361            let pieces = bytes.iter().filter(|&&b| b == 0).count() as i64 + 1;
1362            if a(3) as i64 > pieces {
1363                bail!("gasm:gl: transform_feedback_varyings: fewer names than count");
1364            }
1365            gl.valid(Program, u(0), false);
1366        }
1367
1368        // ---- uniforms: header [3, location, kind, count(, transpose)] + the values ----
1369        n if n.starts_with("uniform") && n != "uniform_block_binding" => {
1370            let (kind, comps, vector, matrix) = uniform_kind(n);
1371            let loc = a(0);
1372            if !vector {
1373                if !gl.uniform_target(loc) {
1374                    return Ok(());
1375                }
1376                let mut bytes = Vec::with_capacity(16);
1377                for v in &args[1..] {
1378                    match *v {
1379                        Val::F32(b) => bytes.extend_from_slice(&b.to_le_bytes()),
1380                        Val::I32(i) => bytes.extend_from_slice(&i.to_le_bytes()),
1381                        _ => {}
1382                    }
1383                }
1384                fold(&[3, loc as u32, kind, 1], Some(&bytes));
1385                return Ok(());
1386            }
1387            let count = a(1);
1388            if count < 0 {
1389                gl.error(INVALID_VALUE);
1390                return Ok(());
1391            }
1392            let ptr = if matrix { u(3) } else { u(2) };
1393            let bytes = guest_slice(mem, ptr, count as u64 * comps as u64 * 4)?;
1394            if !gl.uniform_target(loc) {
1395                return Ok(());
1396            }
1397            if matrix {
1398                fold(
1399                    &[3, loc as u32, kind, count as u32, (a(2) != 0) as u32],
1400                    Some(bytes),
1401                );
1402            } else {
1403                fold(&[3, loc as u32, kind, count as u32], Some(bytes));
1404            }
1405        }
1406
1407        // ---- queries, sync (results from the next frame on), transform feedback ----
1408        "create_query" => ret(gl.create(Query) as i64),
1409        "delete_query" => gl.remove(Query, u(0)),
1410        "is_query" => ret(gl.is(Query, u(0)) as i64),
1411        "begin_query" => {
1412            let (t, n) = (u(0), u(1));
1413            if gl.target(QUERY_TARGETS, t) && gl.valid(Query, n, false) {
1414                gl.active.insert(t, n);
1415                gl.query_ended.remove(&n);
1416            }
1417        }
1418        "end_query" => {
1419            let t = u(0);
1420            if gl.target(QUERY_TARGETS, t) {
1421                match gl.active.remove(&t) {
1422                    Some(n) if n != 0 => {
1423                        gl.query_ended.insert(n, frame);
1424                    }
1425                    _ => {
1426                        gl.error(INVALID_OPERATION);
1427                    }
1428                }
1429            }
1430        }
1431        "get_queryiv" => {
1432            let ok = gl.target(QUERY_TARGETS, u(0)) && u(1) == CURRENT_QUERY;
1433            ret(if ok {
1434                gl.active.get(&u(0)).copied().unwrap_or(0) as i64
1435            } else {
1436                0
1437            })
1438        }
1439        "get_query_objectuiv" => {
1440            let n = u(0);
1441            if !gl.valid(Query, n, false) {
1442                ret(0);
1443                return Ok(());
1444            }
1445            let ready = gl.query_ended.get(&n).is_some_and(|&e| frame > e);
1446            ret(match u(1) {
1447                QUERY_RESULT_AVAILABLE => ready as i64,
1448                QUERY_RESULT => ready as i64, // occlusion "passed"
1449                _ => {
1450                    gl.error(INVALID_ENUM);
1451                    0
1452                }
1453            })
1454        }
1455        "fence_sync" => {
1456            let (cond, flags) = (u(0), u(1));
1457            if cond != SYNC_GPU_COMMANDS_COMPLETE || flags != 0 {
1458                gl.error(if cond != SYNC_GPU_COMMANDS_COMPLETE {
1459                    INVALID_ENUM
1460                } else {
1461                    INVALID_VALUE
1462                });
1463                ret(0);
1464                return Ok(());
1465            }
1466            let n = gl.create(Sync);
1467            gl.sync_made.insert(n, frame);
1468            ret(n as i64)
1469        }
1470        "is_sync" => ret(gl.is(Sync, u(0)) as i64),
1471        "delete_sync" => {
1472            gl.sync_made.remove(&u(0));
1473            gl.remove(Sync, u(0));
1474        }
1475        "client_wait_sync" => {
1476            let n = u(0);
1477            if !gl.valid(Sync, n, false) {
1478                ret(WAIT_FAILED as i64);
1479                return Ok(());
1480            }
1481            let done = gl.sync_made.get(&n).is_some_and(|&m| frame > m);
1482            ret((if done {
1483                ALREADY_SIGNALED
1484            } else {
1485                TIMEOUT_EXPIRED
1486            }) as i64)
1487        }
1488        "wait_sync" => {
1489            gl.valid(Sync, u(0), false);
1490        }
1491        "get_synciv" => {
1492            let n = u(0);
1493            if !gl.valid(Sync, n, false) {
1494                ret(0);
1495                return Ok(());
1496            }
1497            let done = gl.sync_made.get(&n).is_some_and(|&m| frame > m);
1498            ret(if u(1) != SYNC_STATUS {
1499                0
1500            } else if done {
1501                SIGNALED as i64
1502            } else {
1503                UNSIGNALED as i64
1504            })
1505        }
1506        "create_transform_feedback" => ret(gl.create(TransformFeedback) as i64),
1507        "delete_transform_feedback" => gl.remove(TransformFeedback, u(0)),
1508        "is_transform_feedback" => ret(gl.is(TransformFeedback, u(0)) as i64),
1509        "bind_transform_feedback" => {
1510            let _ =
1511                gl.target(&[TRANSFORM_FEEDBACK], u(0)) && gl.valid(TransformFeedback, u(1), true);
1512        }
1513        _ => bail!("gasm:gl.{name}: not implemented by this runner"),
1514    }
1515    Ok(())
1516}
1517
1518/// A uniform import's hash kind (1-4 float, 11-14 int, 21-24 uint, 32-40 matrices),
1519/// components per element, whether it takes (count, ptr), whether it is a matrix.
1520fn uniform_kind(name: &str) -> (u32, u32, bool, bool) {
1521    const MATRICES: [(&str, u32, u32); 9] = [
1522        ("2fv", 32, 4),
1523        ("3fv", 33, 9),
1524        ("4fv", 34, 16),
1525        ("2x3fv", 35, 6),
1526        ("3x2fv", 36, 6),
1527        ("2x4fv", 37, 8),
1528        ("4x2fv", 38, 8),
1529        ("3x4fv", 39, 12),
1530        ("4x3fv", 40, 12),
1531    ];
1532    if let Some(m) = name.strip_prefix("uniform_matrix") {
1533        let &(_, kind, comps) = MATRICES
1534            .iter()
1535            .find(|(s, _, _)| *s == m)
1536            .expect("matrix uniform");
1537        return (kind, comps, true, true);
1538    }
1539    let rest = &name["uniform".len()..];
1540    let n: u32 = rest[..1].parse().unwrap_or(1);
1541    let suffix = &rest[1..];
1542    let base = match suffix.trim_end_matches('v') {
1543        "f" => 0,
1544        "i" => 10,
1545        _ => 20, // "ui"
1546    };
1547    (base + n, n, suffix.ends_with('v'), false)
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552    use super::*;
1553
1554    #[test]
1555    fn max_attribs_is_the_null_limit() {
1556        assert_eq!(null_limit(0x8869)[0], MAX_ATTRIBS as i64);
1557    }
1558}