Skip to main content

mouser_engine_wgpu/
lib.rs

1//! Mouser's wgpu renderer as an engine library (engine name `mouser`, ABI v2).
2//!
3//! The same pipeline as Mouser's built-in renderer (`crates/mouser-render/src/gpu.rs`):
4//! every rect and glyph is one instanced quad, glyph coverage lives in an R8
5//! atlas, images are textures, and blending matches the CPU canvas. The input
6//! is the C ABI frame, so glyph bitmaps come from Mouser (keyed by
7//! `MouserGlyph::key`) instead of a font stack. `tests/` and
8//! `crates/mouser-render/tests/wgpu_engine_parity.rs` check it pixel by pixel
9//! against the built-in renderer.
10#![allow(clippy::missing_safety_doc)]
11
12use mouser_engine_abi::*;
13use std::collections::HashMap;
14use std::ffi::{c_char, c_int, c_void};
15
16const SHADER: &str = r#"
17struct U { sa: vec4<f32>, flags: vec4<f32> };
18@group(0) @binding(0) var<uniform> u: U;
19@group(0) @binding(1) var tex: texture_2d<f32>;
20@group(0) @binding(2) var samp: sampler;
21struct VOut {
22    @builtin(position) pos: vec4<f32>,
23    @location(0) uv: vec2<f32>,
24    @location(1) color: vec4<f32>,
25};
26@vertex
27fn vs(@builtin(vertex_index) vi: u32, @location(0) rect: vec4<f32>, @location(1) uvr: vec4<f32>, @location(2) color: vec4<f32>) -> VOut {
28    let corner = vec2<f32>(f32(vi & 1u), f32((vi >> 1u) & 1u));
29    let p = rect.xy + corner * rect.zw;
30    var o: VOut;
31    o.pos = vec4<f32>(p.x / u.sa.x * 2.0 - 1.0, 1.0 - p.y / u.sa.y * 2.0, 0.0, 1.0);
32    o.uv = (uvr.xy + corner * uvr.zw) / u.sa.zw;
33    o.color = color;
34    return o;
35}
36@vertex
37fn vs_img(@builtin(vertex_index) vi: u32, @location(0) rect: vec4<f32>, @location(1) uvr: vec4<f32>, @location(2) color: vec4<f32>) -> VOut {
38    let corner = vec2<f32>(f32(vi & 1u), f32((vi >> 1u) & 1u));
39    let p = rect.xy + corner * rect.zw;
40    var o: VOut;
41    o.pos = vec4<f32>(p.x / u.sa.x * 2.0 - 1.0, 1.0 - p.y / u.sa.y * 2.0, 0.0, 1.0);
42    o.uv = uvr.xy + corner * uvr.zw;
43    o.color = color;
44    return o;
45}
46@fragment
47fn fs_img(i: VOut) -> @location(0) vec4<f32> {
48    return textureSample(tex, samp, i.uv);
49}
50@fragment
51fn fs(i: VOut) -> @location(0) vec4<f32> {
52    let a = textureSample(tex, samp, i.uv).r;
53    return vec4<f32>(i.color.rgb, i.color.a * a);
54}
55"#;
56
57/// Floats per instance: rect (4), atlas rect (4), colour (4).
58const INSTANCE_FLOATS: usize = 12;
59const ATLAS_START: u32 = 1024;
60const ATLAS_MAX: u32 = 4096;
61
62enum Segment {
63    Quads(std::ops::Range<u32>),
64    Image(u64, u32),
65}
66
67struct Atlas {
68    size: u32,
69    texture: wgpu::Texture,
70    x: u32,
71    y: u32,
72    row_h: u32,
73    /// glyph key -> (x, y, w, h) in texels.
74    slots: HashMap<u64, [u32; 4]>,
75}
76
77fn new_atlas(device: &wgpu::Device, queue: &wgpu::Queue, size: u32) -> Atlas {
78    let texture = device.create_texture(&wgpu::TextureDescriptor {
79        label: Some("mouser glyph atlas"),
80        size: wgpu::Extent3d { width: size, height: size, depth_or_array_layers: 1 },
81        mip_level_count: 1,
82        sample_count: 1,
83        dimension: wgpu::TextureDimension::D2,
84        format: wgpu::TextureFormat::R8Unorm,
85        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
86        view_formats: &[],
87    });
88    write_region(queue, &texture, 0, 0, 2, 2, 2, &[255; 4]);
89    Atlas { size, texture, x: 3, y: 0, row_h: 2, slots: HashMap::new() }
90}
91
92#[allow(clippy::too_many_arguments)]
93fn write_region(queue: &wgpu::Queue, texture: &wgpu::Texture, x: u32, y: u32, w: u32, h: u32, stride: u32, data: &[u8]) {
94    queue.write_texture(
95        wgpu::ImageCopyTexture { texture, mip_level: 0, origin: wgpu::Origin3d { x, y, z: 0 }, aspect: wgpu::TextureAspect::All },
96        data,
97        wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(stride), rows_per_image: Some(h) },
98        wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
99    );
100}
101
102fn srgb_to_linear(c: f32) -> f32 {
103    if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
104}
105
106/// Opacity from an ABI v2 colour: the top byte is `255 - opacity` (0 = opaque).
107fn opacity(c: u32) -> u32 {
108    255 - (c >> 24)
109}
110
111/// The renderer, on a window surface or an offscreen texture.
112pub struct Renderer {
113    surface: Option<wgpu::Surface<'static>>,
114    target: Option<wgpu::Texture>,
115    device: wgpu::Device,
116    queue: wgpu::Queue,
117    config: wgpu::SurfaceConfiguration,
118    pipeline: wgpu::RenderPipeline,
119    image_pipeline: wgpu::RenderPipeline,
120    layout: wgpu::BindGroupLayout,
121    sampler: wgpu::Sampler,
122    image_sampler: wgpu::Sampler,
123    uniforms: wgpu::Buffer,
124    bind_group: wgpu::BindGroup,
125    atlas: Atlas,
126    instances: wgpu::Buffer,
127    capacity: usize,
128    srgb: bool,
129    textures: HashMap<u64, (wgpu::Texture, wgpu::BindGroup)>,
130    /// Adapter and format, for logs.
131    pub description: String,
132}
133
134impl Renderer {
135    /// Draw into an offscreen `Rgba8Unorm` texture (the pixel host, tests).
136    pub fn offscreen(width: u32, height: u32) -> Result<Self, String> {
137        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() });
138        Self::with_target(&instance, None, width, height)
139    }
140
141    /// Present to a native window.
142    ///
143    /// # Safety
144    /// The handles must be valid and outlive the renderer.
145    pub unsafe fn on_window(
146        display: raw_window_handle::RawDisplayHandle,
147        window: raw_window_handle::RawWindowHandle,
148        width: u32,
149        height: u32,
150    ) -> Result<Self, String> {
151        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, ..Default::default() });
152        // SAFETY: the caller guarantees the handles.
153        let surface = unsafe {
154            instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { raw_display_handle: display, raw_window_handle: window })
155        }
156        .map_err(|e| format!("surface: {e}"))?;
157        Self::with_target(&instance, Some(surface), width, height)
158    }
159
160    fn with_target(instance: &wgpu::Instance, surface: Option<wgpu::Surface<'static>>, width: u32, height: u32) -> Result<Self, String> {
161        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
162            power_preference: wgpu::PowerPreference::LowPower,
163            compatible_surface: surface.as_ref(),
164            force_fallback_adapter: false,
165        }))
166        .ok_or("no compatible GPU adapter")?;
167        let info = adapter.get_info();
168        let (device, queue) = pollster::block_on(adapter.request_device(
169            &wgpu::DeviceDescriptor {
170                label: Some("mouser"),
171                required_features: wgpu::Features::empty(),
172                required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
173            },
174            None,
175        ))
176        .map_err(|e| format!("device: {e}"))?;
177        let (format, alpha_mode) = match &surface {
178            Some(surface) => {
179                let caps = surface.get_capabilities(&adapter);
180                let format = *caps.formats.iter().find(|f| !f.is_srgb()).or(caps.formats.first()).ok_or("surface has no formats")?;
181                (format, caps.alpha_modes.first().copied().unwrap_or(wgpu::CompositeAlphaMode::Auto))
182            }
183            None => (wgpu::TextureFormat::Rgba8Unorm, wgpu::CompositeAlphaMode::Auto),
184        };
185        let config = wgpu::SurfaceConfiguration {
186            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
187            format,
188            width: width.max(1),
189            height: height.max(1),
190            present_mode: wgpu::PresentMode::Fifo,
191            alpha_mode,
192            view_formats: vec![],
193            desired_maximum_frame_latency: 2,
194        };
195        if let Some(surface) = &surface {
196            surface.configure(&device, &config);
197        }
198        let target = surface.is_none().then(|| offscreen_target(&device, &config));
199        let shader =
200            device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("mouser"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
201        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
202            label: None,
203            entries: &[
204                wgpu::BindGroupLayoutEntry {
205                    binding: 0,
206                    visibility: wgpu::ShaderStages::VERTEX,
207                    ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
208                    count: None,
209                },
210                wgpu::BindGroupLayoutEntry {
211                    binding: 1,
212                    visibility: wgpu::ShaderStages::FRAGMENT,
213                    ty: wgpu::BindingType::Texture {
214                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
215                        view_dimension: wgpu::TextureViewDimension::D2,
216                        multisampled: false,
217                    },
218                    count: None,
219                },
220                wgpu::BindGroupLayoutEntry {
221                    binding: 2,
222                    visibility: wgpu::ShaderStages::FRAGMENT,
223                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
224                    count: None,
225                },
226            ],
227        });
228        let pipeline_layout =
229            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&layout], push_constant_ranges: &[] });
230        let make_pipeline = |vs: &str, fs: &str| {
231            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
232                label: Some("mouser quads"),
233                layout: Some(&pipeline_layout),
234                vertex: wgpu::VertexState {
235                    module: &shader,
236                    entry_point: vs,
237                    compilation_options: Default::default(),
238                    buffers: &[wgpu::VertexBufferLayout {
239                        array_stride: (INSTANCE_FLOATS * 4) as u64,
240                        step_mode: wgpu::VertexStepMode::Instance,
241                        attributes: &wgpu::vertex_attr_array![0 => Float32x4, 1 => Float32x4, 2 => Float32x4],
242                    }],
243                },
244                fragment: Some(wgpu::FragmentState {
245                    module: &shader,
246                    entry_point: fs,
247                    compilation_options: Default::default(),
248                    targets: &[Some(wgpu::ColorTargetState {
249                        format,
250                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
251                        write_mask: wgpu::ColorWrites::ALL,
252                    })],
253                }),
254                primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleStrip, ..Default::default() },
255                depth_stencil: None,
256                multisample: wgpu::MultisampleState::default(),
257                multiview: None,
258            })
259        };
260        let pipeline = make_pipeline("vs", "fs");
261        let image_pipeline = make_pipeline("vs_img", "fs_img");
262        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
263            mag_filter: wgpu::FilterMode::Nearest,
264            min_filter: wgpu::FilterMode::Nearest,
265            ..Default::default()
266        });
267        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
268            mag_filter: wgpu::FilterMode::Linear,
269            min_filter: wgpu::FilterMode::Linear,
270            ..Default::default()
271        });
272        let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
273            label: None,
274            size: 32,
275            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
276            mapped_at_creation: false,
277        });
278        let atlas = new_atlas(&device, &queue, ATLAS_START);
279        let bind_group = bind(&device, &layout, &uniforms, &atlas.texture, &sampler);
280        let capacity = 4096;
281        let instances = instance_buffer(&device, capacity);
282        let description = format!("{} ({:?}, {:?})", info.name, info.backend, format);
283        Ok(Self {
284            surface,
285            target,
286            device,
287            queue,
288            config,
289            pipeline,
290            image_pipeline,
291            layout,
292            sampler,
293            image_sampler,
294            uniforms,
295            bind_group,
296            atlas,
297            instances,
298            capacity,
299            srgb: format.is_srgb(),
300            textures: HashMap::new(),
301            description,
302        })
303    }
304
305    pub fn resize(&mut self, width: u32, height: u32) {
306        self.config.width = width.max(1);
307        self.config.height = height.max(1);
308        match &self.surface {
309            Some(s) => s.configure(&self.device, &self.config),
310            None => self.target = Some(offscreen_target(&self.device, &self.config)),
311        }
312    }
313
314    /// Drop a cached glyph or image upload.
315    pub fn evict(&mut self, key: u64) {
316        self.textures.remove(&key);
317        self.atlas.slots.remove(&key);
318    }
319
320    /// The offscreen pixels as `0x00RRGGBB` (`None` on a window).
321    pub fn read_pixels(&self) -> Option<Vec<u32>> {
322        Some(read_texture(&self.device, &self.queue, self.target.as_ref()?, self.config.width, self.config.height))
323    }
324
325    pub fn max_texture(&self) -> u32 {
326        self.device.limits().max_texture_dimension_2d
327    }
328
329    fn reset_atlas(&mut self, size: u32) {
330        self.atlas = new_atlas(&self.device, &self.queue, size);
331        self.bind_group = bind(&self.device, &self.layout, &self.uniforms, &self.atlas.texture, &self.sampler);
332    }
333
334    fn slot(&mut self, g: &MouserGlyph) -> Option<[u32; 4]> {
335        if let Some(s) = self.atlas.slots.get(&g.key) {
336            return Some(*s);
337        }
338        let (w, h) = (g.w, g.h);
339        let a = &mut self.atlas;
340        if w == 0 || h == 0 || g.coverage.is_null() {
341            a.slots.insert(g.key, [0, 0, 0, 0]);
342            return Some([0, 0, 0, 0]);
343        }
344        if a.x + w + 1 > a.size {
345            a.x = 0;
346            a.y += a.row_h + 1;
347            a.row_h = 0;
348        }
349        if a.y + h > a.size {
350            return None;
351        }
352        // SAFETY: coverage holds `stride * h` bytes during draw (ABI).
353        let data = unsafe { std::slice::from_raw_parts(g.coverage, (g.stride * h) as usize) };
354        // Repack to a tight `w`-byte stride (wgpu wants a whole row per stride).
355        let tight: Vec<u8> = data.chunks(g.stride as usize).flat_map(|r| &r[..w as usize]).copied().collect();
356        let slot = [a.x, a.y, w, h];
357        write_region(&self.queue, &a.texture, a.x, a.y, w, h, w, &tight);
358        a.x += w + 1;
359        a.row_h = a.row_h.max(h);
360        a.slots.insert(g.key, slot);
361        Some(slot)
362    }
363
364    fn color(&self, c: u32) -> [f32; 3] {
365        let ch = |s: u32| ((c >> s) & 0xFF) as f32 / 255.0;
366        let rgb = [ch(16), ch(8), ch(0)];
367        if self.srgb { rgb.map(srgb_to_linear) } else { rgb }
368    }
369
370    fn image_texture(&mut self, img: &MouserImage) {
371        if self.textures.contains_key(&img.key) {
372            return;
373        }
374        let format = if self.srgb { wgpu::TextureFormat::Rgba8UnormSrgb } else { wgpu::TextureFormat::Rgba8Unorm };
375        let size = wgpu::Extent3d { width: img.img_w, height: img.img_h, depth_or_array_layers: 1 };
376        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
377            label: Some("mouser image"),
378            size,
379            mip_level_count: 1,
380            sample_count: 1,
381            dimension: wgpu::TextureDimension::D2,
382            format,
383            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
384            view_formats: &[],
385        });
386        // SAFETY: rgba holds img_w * img_h * 4 bytes during draw (ABI).
387        let rgba = unsafe { std::slice::from_raw_parts(img.rgba, (img.img_w * img.img_h * 4) as usize) };
388        self.queue.write_texture(
389            wgpu::ImageCopyTexture { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
390            rgba,
391            wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(img.img_w * 4), rows_per_image: Some(img.img_h) },
392            size,
393        );
394        let bind = bind(&self.device, &self.layout, &self.uniforms, &texture, &self.image_sampler);
395        self.textures.insert(img.key, (texture, bind));
396    }
397
398    /// # Safety
399    /// The frame's pointers must be valid (true inside the ABI's `draw`).
400    unsafe fn build(&mut self, frame: &MouserFrame) -> Option<(Vec<f32>, Vec<Segment>)> {
401        // SAFETY: the caller guarantees the frame.
402        let (ops, rects, glyphs, images) = unsafe { frame.parts() };
403        let max = self.device.limits().max_texture_dimension_2d;
404        let mut data = Vec::with_capacity(ops.len() * INSTANCE_FLOATS);
405        let mut segs = Vec::new();
406        let mut start = 0usize;
407        for op in ops {
408            let i = op.index as usize;
409            if op.kind == OP_IMAGE {
410                let Some(img) = images.get(i) else { continue };
411                if img.img_w == 0 || img.img_h == 0 || img.img_w > max || img.img_h > max || img.rgba.is_null() {
412                    continue;
413                }
414                self.image_texture(img);
415                let n = data.len() / INSTANCE_FLOATS;
416                if n > start {
417                    segs.push(Segment::Quads(start as u32..n as u32));
418                }
419                let (iw, ih) = (img.img_w as f32, img.img_h as f32);
420                data.extend_from_slice(&[img.x as f32, img.y as f32, img.w as f32, img.h as f32]);
421                data.extend_from_slice(&[img.src_x as f32 / iw, img.src_y as f32 / ih, img.src_w as f32 / iw, img.src_h as f32 / ih]);
422                data.extend_from_slice(&[1.0, 1.0, 1.0, 1.0]);
423                segs.push(Segment::Image(img.key, n as u32));
424                start = n + 1;
425                continue;
426            }
427            let (rect, uv, color) = match op.kind {
428                OP_RECT => {
429                    let Some(r) = rects.get(i) else { continue };
430                    ([r.x as f32, r.y as f32, r.w as f32, r.h as f32], [0.5, 0.5, 0.0, 0.0], r.color)
431                }
432                OP_GLYPH => {
433                    let Some(g) = glyphs.get(i) else { continue };
434                    let s = self.slot(g)?;
435                    if s[2] == 0 {
436                        continue;
437                    }
438                    let f = s.map(|v| v as f32);
439                    ([g.x as f32, g.y as f32, f[2], f[3]], f, g.color)
440                }
441                _ => continue,
442            };
443            let c = self.color(color);
444            data.extend_from_slice(&rect);
445            data.extend_from_slice(&uv);
446            data.extend_from_slice(&[c[0], c[1], c[2], opacity(color) as f32 / 255.0]);
447        }
448        let n = data.len() / INSTANCE_FLOATS;
449        if n > start {
450            segs.push(Segment::Quads(start as u32..n as u32));
451        }
452        Some((data, segs))
453    }
454
455    /// Draw (and present on a window). `DRAW_AGAIN` when the surface was reset.
456    ///
457    /// # Safety
458    /// The frame's pointers must be valid for the call.
459    pub unsafe fn draw(&mut self, frame: &MouserFrame) -> c_int {
460        // SAFETY: forwarded.
461        let built = match unsafe { self.build(frame) } {
462            Some(d) => Some(d),
463            None => {
464                let size = (self.atlas.size * 2).min(ATLAS_MAX).min(self.device.limits().max_texture_dimension_2d);
465                self.reset_atlas(size);
466                // SAFETY: forwarded.
467                unsafe { self.build(frame) }
468            }
469        };
470        let Some((data, segs)) = built else { return DRAW_OK };
471        // SAFETY: forwarded.
472        let (_, _, _, images) = unsafe { frame.parts() };
473        let used: std::collections::HashSet<u64> = images.iter().map(|i| i.key).collect();
474        self.textures.retain(|k, _| used.contains(k));
475        let count = data.len() / INSTANCE_FLOATS;
476        if count > self.capacity {
477            self.capacity = count.next_power_of_two();
478            self.instances = instance_buffer(&self.device, self.capacity);
479        }
480        let bytes: Vec<u8> = data.iter().flat_map(|f| f.to_ne_bytes()).collect();
481        if !bytes.is_empty() {
482            self.queue.write_buffer(&self.instances, 0, &bytes);
483        }
484        let u = [self.config.width as f32, self.config.height as f32, self.atlas.size as f32, self.atlas.size as f32, 0.0, 0.0, 0.0, 0.0];
485        let ub: Vec<u8> = u.iter().flat_map(|f| f.to_ne_bytes()).collect();
486        self.queue.write_buffer(&self.uniforms, 0, &ub);
487        let out = match &self.surface {
488            None => None,
489            Some(surface) => match surface.get_current_texture() {
490                Ok(f) => Some(f),
491                Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
492                    surface.configure(&self.device, &self.config);
493                    return DRAW_AGAIN;
494                }
495                Err(_) => return DRAW_AGAIN,
496            },
497        };
498        let texture = match (&out, &self.target) {
499            (Some(f), _) => &f.texture,
500            (None, Some(t)) => t,
501            (None, None) => return DRAW_AGAIN,
502        };
503        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
504        let bg = self.color(frame.clear);
505        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
506        {
507            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
508                label: None,
509                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
510                    view: &view,
511                    resolve_target: None,
512                    ops: wgpu::Operations {
513                        load: wgpu::LoadOp::Clear(wgpu::Color { r: bg[0] as f64, g: bg[1] as f64, b: bg[2] as f64, a: 1.0 }),
514                        store: wgpu::StoreOp::Store,
515                    },
516                })],
517                depth_stencil_attachment: None,
518                timestamp_writes: None,
519                occlusion_query_set: None,
520            });
521            if count > 0 {
522                pass.set_vertex_buffer(0, self.instances.slice(..(count * INSTANCE_FLOATS * 4) as u64));
523                for seg in &segs {
524                    match seg {
525                        Segment::Quads(r) => {
526                            pass.set_pipeline(&self.pipeline);
527                            pass.set_bind_group(0, &self.bind_group, &[]);
528                            pass.draw(0..4, r.clone());
529                        }
530                        Segment::Image(key, i) => {
531                            if let Some((_, bind)) = self.textures.get(key) {
532                                pass.set_pipeline(&self.image_pipeline);
533                                pass.set_bind_group(0, bind, &[]);
534                                pass.draw(0..4, *i..*i + 1);
535                            }
536                        }
537                    }
538                }
539            }
540        }
541        self.queue.submit(Some(encoder.finish()));
542        if let Some(f) = out {
543            f.present();
544        }
545        DRAW_OK
546    }
547}
548
549fn bind(
550    device: &wgpu::Device,
551    layout: &wgpu::BindGroupLayout,
552    uniforms: &wgpu::Buffer,
553    texture: &wgpu::Texture,
554    sampler: &wgpu::Sampler,
555) -> wgpu::BindGroup {
556    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
557    device.create_bind_group(&wgpu::BindGroupDescriptor {
558        label: None,
559        layout,
560        entries: &[
561            wgpu::BindGroupEntry { binding: 0, resource: uniforms.as_entire_binding() },
562            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&view) },
563            wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(sampler) },
564        ],
565    })
566}
567
568fn instance_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
569    device.create_buffer(&wgpu::BufferDescriptor {
570        label: Some("mouser instances"),
571        size: (capacity * INSTANCE_FLOATS * 4) as u64,
572        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
573        mapped_at_creation: false,
574    })
575}
576
577fn offscreen_target(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> wgpu::Texture {
578    device.create_texture(&wgpu::TextureDescriptor {
579        label: Some("mouser offscreen"),
580        size: wgpu::Extent3d { width: config.width, height: config.height, depth_or_array_layers: 1 },
581        mip_level_count: 1,
582        sample_count: 1,
583        dimension: wgpu::TextureDimension::D2,
584        format: config.format,
585        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
586        view_formats: &[],
587    })
588}
589
590fn read_texture(device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture, w: u32, h: u32) -> Vec<u32> {
591    let row = (w * 4).div_ceil(256) * 256;
592    let buf = device.create_buffer(&wgpu::BufferDescriptor {
593        label: Some("mouser readback"),
594        size: (row * h) as u64,
595        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
596        mapped_at_creation: false,
597    });
598    let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
599    enc.copy_texture_to_buffer(
600        wgpu::ImageCopyTexture { texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
601        wgpu::ImageCopyBuffer { buffer: &buf, layout: wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(h) } },
602        wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
603    );
604    queue.submit(Some(enc.finish()));
605    let slice = buf.slice(..);
606    slice.map_async(wgpu::MapMode::Read, |_| {});
607    device.poll(wgpu::Maintain::Wait);
608    let data = slice.get_mapped_range();
609    let mut out = Vec::with_capacity((w * h) as usize);
610    for y in 0..h as usize {
611        let r = &data[y * row as usize..][..w as usize * 4];
612        out.extend(r.as_chunks::<4>().0.iter().map(|p| (p[0] as u32) << 16 | (p[1] as u32) << 8 | p[2] as u32));
613    }
614    out
615}
616
617// ---- C ABI -----------------------------------------------------------------
618
619/// An engine instance: the renderer, plus the pixel buffer when hosted.
620struct Engine {
621    r: Renderer,
622    pixels: Option<*mut MouserPixels>,
623}
624
625fn raw_handles(s: &MouserSurface) -> Result<(raw_window_handle::RawDisplayHandle, raw_window_handle::RawWindowHandle), String> {
626    use raw_window_handle::*;
627    use std::ptr::NonNull;
628    let nn = |p: *mut c_void, what: &str| NonNull::new(p).ok_or(format!("null {what}"));
629    Ok(match s.kind {
630        SURFACE_WAYLAND => (
631            RawDisplayHandle::Wayland(WaylandDisplayHandle::new(nn(s.display, "wl_display")?)),
632            RawWindowHandle::Wayland(WaylandWindowHandle::new(nn(s.window, "wl_surface")?)),
633        ),
634        SURFACE_XLIB => (
635            RawDisplayHandle::Xlib(XlibDisplayHandle::new(NonNull::new(s.display), 0)),
636            RawWindowHandle::Xlib(XlibWindowHandle::new(s.window as usize as _)),
637        ),
638        SURFACE_XCB => (
639            RawDisplayHandle::Xcb(XcbDisplayHandle::new(NonNull::new(s.display), 0)),
640            RawWindowHandle::Xcb(XcbWindowHandle::new(std::num::NonZeroU32::new(s.window as usize as u32).ok_or("null xcb window")?)),
641        ),
642        SURFACE_APPKIT => {
643            (RawDisplayHandle::AppKit(AppKitDisplayHandle::new()), RawWindowHandle::AppKit(AppKitWindowHandle::new(nn(s.window, "NSView")?)))
644        }
645        SURFACE_WIN32 => {
646            let mut w = Win32WindowHandle::new(std::num::NonZeroIsize::new(s.window as isize).ok_or("null HWND")?);
647            w.hinstance = std::num::NonZeroIsize::new(s.display as isize);
648            (RawDisplayHandle::Windows(WindowsDisplayHandle::new()), RawWindowHandle::Win32(w))
649        }
650        k => return Err(format!("unsupported surface kind {k}")),
651    })
652}
653
654unsafe extern "C" fn create(surface: *const MouserSurface, w: u32, h: u32, err: *mut c_char, err_len: usize) -> *mut c_void {
655    let fail = |m: String| {
656        // SAFETY: Mouser passes a valid error buffer.
657        unsafe { write_err(err, err_len, &m) };
658        std::ptr::null_mut()
659    };
660    // SAFETY: Mouser passes a valid surface.
661    let Some(s) = (unsafe { surface.as_ref() }) else { return fail("no surface".into()) };
662    let made = if s.kind == SURFACE_PIXELS {
663        Renderer::offscreen(w, h).map(|r| Engine { r, pixels: Some(s.window as *mut MouserPixels) })
664    } else {
665        match raw_handles(s) {
666            // SAFETY: the window outlives the engine (ABI).
667            Ok((d, win)) => unsafe { Renderer::on_window(d, win, w, h) }.map(|r| Engine { r, pixels: None }),
668            Err(e) => Err(e),
669        }
670    };
671    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| made)) {
672        Ok(Ok(e)) => Box::into_raw(Box::new(e)) as *mut c_void,
673        Ok(Err(e)) => fail(e),
674        Err(_) => fail("wgpu panicked while starting".into()),
675    }
676}
677
678unsafe fn engine<'a>(p: *mut c_void) -> Option<&'a mut Engine> {
679    // SAFETY: `p` came from `create`.
680    unsafe { (p as *mut Engine).as_mut() }
681}
682
683unsafe extern "C" fn resize(e: *mut c_void, w: u32, h: u32) {
684    if let Some(e) = unsafe { engine(e) } {
685        e.r.resize(w, h);
686    }
687}
688
689unsafe extern "C" fn draw(e: *mut c_void, frame: *const MouserFrame) -> c_int {
690    let (Some(e), Some(frame)) = (unsafe { engine(e) }, unsafe { frame.as_ref() }) else { return -1 };
691    let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
692        // SAFETY: valid during draw (ABI).
693        let r = unsafe { e.r.draw(frame) };
694        if let (Some(px), Some(out)) = (e.pixels, e.r.read_pixels()) {
695            // SAFETY: Mouser's pixel buffer, valid during draw.
696            if let Some(px) = unsafe { px.as_ref() } {
697                let (w, h) = (px.width.min(e.r.config.width) as usize, px.height.min(e.r.config.height) as usize);
698                for y in 0..h {
699                    // SAFETY: `stride * height` pixels.
700                    let dst = unsafe { std::slice::from_raw_parts_mut(px.data.add(y * px.stride as usize), w) };
701                    dst.copy_from_slice(&out[y * e.r.config.width as usize..][..w]);
702                }
703            }
704        }
705        r
706    }));
707    r.unwrap_or(-2)
708}
709
710unsafe extern "C" fn destroy(e: *mut c_void) {
711    if !e.is_null() {
712        // SAFETY: from `create`.
713        drop(unsafe { Box::from_raw(e as *mut Engine) });
714    }
715}
716
717unsafe extern "C" fn caps(e: *mut c_void, caps: *mut MouserCaps) {
718    if let (Some(e), Some(c)) = (unsafe { engine(e) }, unsafe { caps.as_mut() }) {
719        c.flags = CAP_DIRECT_SURFACE | CAP_PIXELS;
720        c.formats = FORMAT_XRGB8888;
721        c.max_texture = e.r.max_texture();
722    }
723}
724
725unsafe extern "C" fn evict(e: *mut c_void, key: u64) {
726    if let Some(e) = unsafe { engine(e) } {
727        e.r.evict(key);
728    }
729}
730
731static TABLE_V2: MouserEngineV2 = MouserEngineV2 {
732    base: MouserEngineV1 {
733        abi: MOUSER_ENGINE_ABI_V2,
734        size: std::mem::size_of::<MouserEngineV2>() as u32,
735        name: c"mouser".as_ptr(),
736        create,
737        resize,
738        draw,
739        destroy,
740    },
741    abi_min: MOUSER_ENGINE_ABI_V2,
742    abi_max: MOUSER_ENGINE_ABI_V2,
743    caps: Some(caps),
744    set_scale: None,
745    evict: Some(evict),
746    set_vsync: None,
747};
748
749/// The v2 entry point (this engine needs v2: translucent colours).
750#[unsafe(no_mangle)]
751pub extern "C" fn mouser_engine_v2() -> *const MouserEngineV2 {
752    &TABLE_V2
753}