Skip to main content

denise_wgpu/
lib.rs

1//! A GPU painter for Denise, on wgpu.
2//!
3//! The second implementation of [`Painter`], and the reason the trait exists:
4//! `denise-render` turns the same calls into pixels on a CPU, this crate turns
5//! them into instanced triangles on whatever wgpu can find. Widgets cannot tell
6//! which one they are drawing through, and that is the whole point.
7//!
8//! **This is for the desktop.** A kiosk on a Pi draws with the software
9//! rasteriser and always will: it needs no Mesa, no compositor and no window
10//! system, and at the sizes a panel runs it was never the bottleneck. A GPU
11//! earns its keep where the designer runs — a Retina display, a large window, a
12//! canvas that zooms — and that is the workload this crate is shaped for.
13//!
14//! # How a frame is drawn
15//!
16//! Every call on the painter appends vertices. Rectangles are plain triangles;
17//! everything with a curve — rounded corners, circles, arcs, lines — is a
18//! bounding quad whose fragment shader evaluates a signed distance and turns it
19//! into one pixel of anti-aliasing. A polygon has more edges than a vertex can
20//! carry, so its quad carries a range of a buffer that holds them and its
21//! fragment shader walks that range for the nearest edge and for which side of
22//! the outline it is on. Images and glyph masks are the same quads with a
23//! texture bound. The clip is carried per vertex and applied per fragment, so a
24//! frame is one pipeline and as few draws as the textures force: a whole widget
25//! tree with no images is a single draw call.
26//!
27//! [`GpuPainter::finish`] then encodes the frame into a texture view — a
28//! swapchain's, or an offscreen one — clearing it first.
29//! [`GpuPainter::finish_onto`] is the incremental form: it keeps what is
30//! already on the target and scissors to the damage, for a caller that owns its
31//! target between frames. [`GpuPainter::finish_to_pixels`]
32//! renders offscreen and reads the result back as `0xAARRGGBB` words, which is
33//! how the parity tests compare it to the software rasteriser.
34//!
35//! ```no_run
36//! use denise::{BufferAge, Pen, Size};
37//! use denise_wgpu::Gpu;
38//!
39//! # fn paint(ui: &mut denise_ui::Ui<()>) -> Result<(), denise_wgpu::Error> {
40//! let gpu = Gpu::headless()?;
41//! let mut painter = gpu.painter(Size::new(640, 400));
42//! ui.paint_with(&mut Pen::new(&mut painter), BufferAge::Undefined);
43//! let pixels: Vec<u32> = painter.finish_to_pixels()?;
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! # Glyphs
49//!
50//! Text arrives through [`blit_glyph`](Painter::blit_glyph) as a rectangle of
51//! an atlas page with an id and a version. The page is uploaded once per
52//! version — that is, whenever the text engine packs a glyph it has not seen —
53//! and every glyph after that is six vertices sampling it. A label costs what a
54//! rectangle costs.
55//!
56//! # Images
57//!
58//! Pictures arrive through [`blit_image`](Painter::blit_image) with an id and a
59//! version, the same way a glyph page does, and are cached the same way: one
60//! upload when the pixels change, a quad every time after. A photo in a
61//! carousel costs what a rectangle costs.
62//!
63//! # What it does not do yet
64//!
65//! The raw [`blit`](Painter::blit) family — a `PixelView` with no identity —
66//! still uploads per call. Nothing in the widget set uses it any more; it is
67//! there for a caller with pixels that genuinely are different every frame,
68//! which is what an upload per call is the honest price of.
69
70#![forbid(unsafe_code)]
71
72use std::cell::{Cell, RefCell};
73use std::collections::HashMap;
74use std::ops::Range;
75
76use denise::angle::{ONE, TURN};
77use denise::painter::ClipToken;
78use denise::{
79    AtlasPage, Color, ImageRef, Mask, Paint, Painter, PixelFormat, PixelView, Point, Rect, Size,
80};
81pub use wgpu;
82
83use wgpu::util::DeviceExt as _;
84
85/// What can go wrong between asking for a GPU and reading pixels back.
86#[derive(Debug, thiserror::Error)]
87pub enum Error {
88    /// wgpu found no adapter at all. Headless CI runners without a software
89    /// Vulkan are the usual reason.
90    #[error("no GPU adapter is available")]
91    NoAdapter,
92    /// The adapter refused to hand out a device.
93    #[error("requesting a device")]
94    Device(#[from] wgpu::RequestDeviceError),
95    /// Mapping the readback buffer failed.
96    #[error("mapping the readback buffer")]
97    Map(#[from] wgpu::BufferAsyncError),
98    /// The device did not finish the work it was asked to wait for.
99    #[error("waiting for the GPU")]
100    Poll(#[from] wgpu::PollError),
101    /// The readback buffer could not be read once mapped.
102    #[error("reading the readback buffer")]
103    Read(#[from] wgpu::MapRangeError),
104}
105
106/// One vertex. Eighty-eight bytes, all of them `f32` or `u32`, so it is `Pod`.
107#[repr(C)]
108#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
109struct Vertex {
110    pos: [f32; 2],
111    clip: [f32; 4],
112    color: [f32; 4],
113    a: [f32; 4],
114    b: [f32; 4],
115    kind: u32,
116    /// Where this polygon's edges start in the frame's edge buffer, and how
117    /// many there are. Zero for every other kind, and it costs nothing: the
118    /// vertex was padded to a multiple of sixteen bytes anyway.
119    poly: [u32; 2],
120    _pad: u32,
121}
122
123#[repr(C)]
124#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
125struct Globals {
126    size: [f32; 2],
127    srgb: u32,
128    _pad: u32,
129}
130
131const KIND_SOLID: u32 = 0;
132const KIND_ROUNDED_FILL: u32 = 1;
133const KIND_ROUNDED_STROKE: u32 = 2;
134const KIND_CIRCLE_FILL: u32 = 3;
135const KIND_CIRCLE_STROKE: u32 = 4;
136const KIND_ARC: u32 = 5;
137const KIND_LINE: u32 = 6;
138const KIND_TEXTURED: u32 = 7;
139const KIND_MASK: u32 = 8;
140const KIND_TEXTURED_ROUNDED: u32 = 9;
141const KIND_POLYGON: u32 = 10;
142
143/// The UV rectangle that samples a whole texture.
144const WHOLE: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
145
146/// A device, a queue, and the one pipeline every frame is drawn with.
147///
148/// Built once per device and kept; [`Gpu::painter`] hands out a painter per
149/// frame.
150pub struct Gpu {
151    device: wgpu::Device,
152    queue: wgpu::Queue,
153    format: wgpu::TextureFormat,
154    pipeline: wgpu::RenderPipeline,
155    globals_layout: wgpu::BindGroupLayout,
156    texture_layout: wgpu::BindGroupLayout,
157    edges_layout: wgpu::BindGroupLayout,
158    sampler: wgpu::Sampler,
159    /// A one-pixel white texture bound while drawing shapes, so the pipeline
160    /// never has to change.
161    white: wgpu::BindGroup,
162    /// One empty edge, bound by every frame that draws no polygon. The
163    /// pipeline layout says the group exists, so something must fill it, and
164    /// most frames draw no polygon at all: this keeps them from allocating a
165    /// buffer to say so.
166    no_edges: wgpu::BindGroup,
167    /// Glyph atlas pages, by atlas id: the version uploaded and its texture.
168    ///
169    /// One entry per atlas, replaced when its version moves on. Interior
170    /// mutability because painters borrow the `Gpu` shared, and a cache that
171    /// only a `&mut Gpu` could fill would never fill.
172    pages: RefCell<HashMap<u64, (u64, wgpu::BindGroup)>>,
173    /// How many atlas pages have been uploaded, ever. The number a profile
174    /// wants, and the number the tests hold to "once per version".
175    page_uploads: Cell<u64>,
176    /// Images, by image id: the version uploaded and its texture. The same
177    /// arrangement as `pages`, for the same reason.
178    images: RefCell<HashMap<u64, (u64, wgpu::BindGroup)>>,
179    /// How many images have been uploaded, ever.
180    image_uploads: Cell<u64>,
181    /// A texture rows pass through when a frame scrolls them: a copy within
182    /// one texture is not allowed, so the rows go out and come back. Kept
183    /// between frames and grown when a taller move needs it.
184    scratch: RefCell<Option<wgpu::Texture>>,
185    /// The globals buffer and its bind group, with the size they describe.
186    /// They change only when the target does, so they are built on a resize
187    /// rather than on a frame.
188    globals: RefCell<Option<(Size, wgpu::Buffer, wgpu::BindGroup)>>,
189}
190
191impl Gpu {
192    /// Wraps a device the caller already has, drawing into textures of `format`.
193    ///
194    /// `format` is what [`GpuPainter::finish`] will be handed views of — a
195    /// swapchain's, typically. Prefer a non-sRGB format: Denise's colours are
196    /// bytes meant for the screen, and an sRGB target forces a conversion that
197    /// the software rasteriser never does.
198    pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self {
199        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
200            label: Some("denise shapes"),
201            source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
202        });
203
204        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
205            label: Some("denise globals"),
206            entries: &[wgpu::BindGroupLayoutEntry {
207                binding: 0,
208                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
209                ty: wgpu::BindingType::Buffer {
210                    ty: wgpu::BufferBindingType::Uniform,
211                    has_dynamic_offset: false,
212                    min_binding_size: None,
213                },
214                count: None,
215            }],
216        });
217
218        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
219            label: Some("denise texture"),
220            entries: &[
221                wgpu::BindGroupLayoutEntry {
222                    binding: 0,
223                    visibility: wgpu::ShaderStages::FRAGMENT,
224                    ty: wgpu::BindingType::Texture {
225                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
226                        view_dimension: wgpu::TextureViewDimension::D2,
227                        multisampled: false,
228                    },
229                    count: None,
230                },
231                wgpu::BindGroupLayoutEntry {
232                    binding: 1,
233                    visibility: wgpu::ShaderStages::FRAGMENT,
234                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
235                    count: None,
236                },
237            ],
238        });
239
240        let edges_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
241            label: Some("denise polygon edges"),
242            entries: &[wgpu::BindGroupLayoutEntry {
243                binding: 0,
244                visibility: wgpu::ShaderStages::FRAGMENT,
245                ty: wgpu::BindingType::Buffer {
246                    ty: wgpu::BufferBindingType::Storage { read_only: true },
247                    has_dynamic_offset: false,
248                    min_binding_size: None,
249                },
250                count: None,
251            }],
252        });
253
254        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
255            label: Some("denise"),
256            bind_group_layouts: &[
257                Some(&globals_layout),
258                Some(&texture_layout),
259                Some(&edges_layout),
260            ],
261            ..Default::default()
262        });
263
264        let vertex_layout = wgpu::VertexBufferLayout {
265            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
266            step_mode: wgpu::VertexStepMode::Vertex,
267            attributes: &wgpu::vertex_attr_array![
268                0 => Float32x2,
269                1 => Float32x4,
270                2 => Float32x4,
271                3 => Float32x4,
272                4 => Float32x4,
273                5 => Uint32,
274                6 => Uint32x2,
275            ],
276        };
277
278        // Premultiplied source-over, the one blend mode the software rasteriser
279        // has, so a translucent fill composites the same way on both.
280        let blend = wgpu::BlendState {
281            color: wgpu::BlendComponent {
282                src_factor: wgpu::BlendFactor::One,
283                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
284                operation: wgpu::BlendOperation::Add,
285            },
286            alpha: wgpu::BlendComponent {
287                src_factor: wgpu::BlendFactor::One,
288                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
289                operation: wgpu::BlendOperation::Add,
290            },
291        };
292
293        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
294            label: Some("denise"),
295            layout: Some(&layout),
296            vertex: wgpu::VertexState {
297                module: &shader,
298                entry_point: Some("vs"),
299                compilation_options: Default::default(),
300                buffers: &[Some(vertex_layout)],
301            },
302            fragment: Some(wgpu::FragmentState {
303                module: &shader,
304                entry_point: Some("fs"),
305                compilation_options: Default::default(),
306                targets: &[Some(wgpu::ColorTargetState {
307                    format,
308                    blend: Some(blend),
309                    write_mask: wgpu::ColorWrites::ALL,
310                })],
311            }),
312            primitive: wgpu::PrimitiveState {
313                topology: wgpu::PrimitiveTopology::TriangleList,
314                cull_mode: None,
315                ..Default::default()
316            },
317            depth_stencil: None,
318            multisample: wgpu::MultisampleState::default(),
319            multiview_mask: None,
320            cache: None,
321        });
322
323        // Nearest, because the software blitter is nearest: a scaled image looks
324        // the same through either painter.
325        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
326            label: Some("denise nearest"),
327            address_mode_u: wgpu::AddressMode::ClampToEdge,
328            address_mode_v: wgpu::AddressMode::ClampToEdge,
329            address_mode_w: wgpu::AddressMode::ClampToEdge,
330            mag_filter: wgpu::FilterMode::Nearest,
331            min_filter: wgpu::FilterMode::Nearest,
332            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
333            ..Default::default()
334        });
335
336        let white = upload(
337            &device,
338            &queue,
339            &texture_layout,
340            &sampler,
341            1,
342            1,
343            wgpu::TextureFormat::Rgba8Unorm,
344            &[255, 255, 255, 255],
345        );
346
347        let no_edges = {
348            let empty = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
349                label: Some("denise polygon edges (none)"),
350                contents: &[0u8; std::mem::size_of::<[f32; 4]>()],
351                usage: wgpu::BufferUsages::STORAGE,
352            });
353            device.create_bind_group(&wgpu::BindGroupDescriptor {
354                label: Some("denise polygon edges (none)"),
355                layout: &edges_layout,
356                entries: &[wgpu::BindGroupEntry {
357                    binding: 0,
358                    resource: empty.as_entire_binding(),
359                }],
360            })
361        };
362
363        Self {
364            device,
365            queue,
366            format,
367            pipeline,
368            globals_layout,
369            texture_layout,
370            edges_layout,
371            sampler,
372            white,
373            no_edges,
374            pages: RefCell::new(HashMap::new()),
375            page_uploads: Cell::new(0),
376            images: RefCell::new(HashMap::new()),
377            image_uploads: Cell::new(0),
378            globals: RefCell::new(None),
379            scratch: RefCell::new(None),
380        }
381    }
382
383    /// The scratch texture, at least `width` by `height`.
384    fn scratch(&self, width: u32, height: u32) -> wgpu::Texture {
385        let mut slot = self.scratch.borrow_mut();
386        if let Some(texture) = slot.as_ref()
387            && texture.width() >= width
388            && texture.height() >= height
389        {
390            return texture.clone();
391        }
392        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
393            label: Some("denise scroll scratch"),
394            size: wgpu::Extent3d {
395                width: width.max(slot.as_ref().map_or(1, wgpu::Texture::width)),
396                height: height.max(slot.as_ref().map_or(1, wgpu::Texture::height)),
397                depth_or_array_layers: 1,
398            },
399            mip_level_count: 1,
400            sample_count: 1,
401            dimension: wgpu::TextureDimension::D2,
402            format: self.format,
403            usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
404            view_formats: &[],
405        });
406        *slot = Some(texture.clone());
407        texture
408    }
409
410    /// Any adapter wgpu can find, drawing into `Rgba8Unorm`.
411    ///
412    /// For tests, tools and `--snapshot` paths: no window, no surface. Fails
413    /// with [`Error::NoAdapter`] where there is nothing to draw with, which a
414    /// test should treat as "skip", not "fail".
415    pub fn headless() -> Result<Self, Error> {
416        let instance = wgpu::Instance::default();
417        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
418            power_preference: wgpu::PowerPreference::None,
419            force_fallback_adapter: false,
420            compatible_surface: None,
421            ..Default::default()
422        }))
423        .map_err(|_| Error::NoAdapter)?;
424        let (device, queue) =
425            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
426                label: Some("denise headless"),
427                ..Default::default()
428            }))?;
429        Ok(Self::new(device, queue, wgpu::TextureFormat::Rgba8Unorm))
430    }
431
432    /// The device frames are drawn with.
433    pub fn device(&self) -> &wgpu::Device {
434        &self.device
435    }
436
437    /// The queue frames are submitted to.
438    pub fn queue(&self) -> &wgpu::Queue {
439        &self.queue
440    }
441
442    /// The globals buffer and bind group for a target of `size`, built only if
443    /// the last pair was for a different size.
444    ///
445    /// Both are reference-counted handles, so the clones are pointer bumps and
446    /// a render pass can hold them without keeping the cache borrowed.
447    fn globals_for(&self, size: Size) -> wgpu::BindGroup {
448        if let Some((cached, _, group)) = self.globals.borrow().as_ref()
449            && *cached == size
450        {
451            return group.clone();
452        }
453        let buffer = self
454            .device
455            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
456                label: Some("denise globals"),
457                contents: bytemuck::bytes_of(&Globals {
458                    size: [size.width as f32, size.height as f32],
459                    srgb: u32::from(self.format.is_srgb()),
460                    _pad: 0,
461                }),
462                usage: wgpu::BufferUsages::UNIFORM,
463            });
464        let group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
465            label: Some("denise globals"),
466            layout: &self.globals_layout,
467            entries: &[wgpu::BindGroupEntry {
468                binding: 0,
469                resource: buffer.as_entire_binding(),
470            }],
471        });
472        *self.globals.borrow_mut() = Some((size, buffer, group.clone()));
473        group
474    }
475
476    /// Reads a texture back as `0xAARRGGBB` words, row after row with no
477    /// padding — the layout a [`denise::Frame`] uses.
478    ///
479    /// The texture must carry [`COPY_SRC`](wgpu::TextureUsages::COPY_SRC) and
480    /// this device's [`format`](Gpu::format). Blocks until the GPU is done; for
481    /// tests, snapshots and tools.
482    pub fn read_texture(&self, texture: &wgpu::Texture) -> Result<Vec<u32>, Error> {
483        let (width, height) = (texture.width().max(1), texture.height().max(1));
484        // Buffer rows must be 256-byte aligned for a texture-to-buffer copy.
485        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
486        let unpadded = width * 4;
487        let padded = unpadded.div_ceil(align) * align;
488        let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
489            label: Some("denise readback"),
490            size: u64::from(padded) * u64::from(height),
491            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
492            mapped_at_creation: false,
493        });
494        let mut encoder = self
495            .device
496            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
497                label: Some("denise readback"),
498            });
499        encoder.copy_texture_to_buffer(
500            wgpu::TexelCopyTextureInfo {
501                texture,
502                mip_level: 0,
503                origin: wgpu::Origin3d::ZERO,
504                aspect: wgpu::TextureAspect::All,
505            },
506            wgpu::TexelCopyBufferInfo {
507                buffer: &readback,
508                layout: wgpu::TexelCopyBufferLayout {
509                    offset: 0,
510                    bytes_per_row: Some(padded),
511                    rows_per_image: Some(height),
512                },
513            },
514            wgpu::Extent3d {
515                width,
516                height,
517                depth_or_array_layers: 1,
518            },
519        );
520        self.queue.submit([encoder.finish()]);
521
522        let slice = readback.slice(..);
523        let (tx, rx) = std::sync::mpsc::channel();
524        slice.map_async(wgpu::MapMode::Read, move |result| {
525            let _ = tx.send(result);
526        });
527        self.device.poll(wgpu::PollType::wait_indefinitely())?;
528        rx.recv().map_err(|_| Error::NoAdapter)??;
529
530        let bgra = matches!(
531            self.format,
532            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
533        );
534        let data = slice.get_mapped_range()?;
535        let mut pixels = Vec::with_capacity((width * height) as usize);
536        for row in data.chunks_exact(padded as usize) {
537            for &[c0, c1, c2, c3] in row[..unpadded as usize].as_chunks::<4>().0 {
538                let (r, g, b, a) = if bgra {
539                    (c2, c1, c0, c3)
540                } else {
541                    (c0, c1, c2, c3)
542                };
543                pixels.push(u32::from_be_bytes([a, r, g, b]));
544            }
545        }
546        drop(data);
547        readback.unmap();
548        Ok(pixels)
549    }
550
551    /// How many glyph atlas pages this device has uploaded, in total.
552    ///
553    /// A page is uploaded once per [`AtlasPage::version`], so for a text engine
554    /// whose glyphs have all been seen this stops moving; a number that keeps
555    /// climbing means an atlas too small for its working set, which the
556    /// engine's own `resets` will confirm.
557    pub fn page_uploads(&self) -> u64 {
558        self.page_uploads.get()
559    }
560
561    /// How many images this device has uploaded, in total.
562    ///
563    /// An image is uploaded once per [`ImageRef::version`]: a picture that has
564    /// been drawn before costs a quad, and only replacing its pixels costs an
565    /// upload.
566    pub fn image_uploads(&self) -> u64 {
567        self.image_uploads.get()
568    }
569
570    /// The texture format [`GpuPainter::finish`] expects its target to have.
571    pub fn format(&self) -> wgpu::TextureFormat {
572        self.format
573    }
574
575    /// A painter for one frame of `size` pixels.
576    pub fn painter(&self, size: Size) -> GpuPainter<'_> {
577        GpuPainter {
578            gpu: self,
579            size,
580            clip: Rect::from_size(size),
581            vertices: Vec::with_capacity(4096),
582            draws: Vec::new(),
583            textures: Vec::new(),
584            edges: Vec::new(),
585            scrolls: Vec::new(),
586        }
587    }
588
589    /// The texture holding `page`, uploaded now if this version has not been.
590    fn page_texture(&self, page: &AtlasPage<'_>) -> wgpu::BindGroup {
591        if let Some((version, group)) = self.pages.borrow().get(&page.id)
592            && *version == page.version
593        {
594            return group.clone();
595        }
596        let mask = &page.mask;
597        let (w, h) = (mask.width().max(1) as u32, mask.height().max(1) as u32);
598        let mut bytes = Vec::with_capacity((w * h) as usize);
599        for y in 0..mask.height() {
600            bytes.extend_from_slice(mask.row(y));
601        }
602        bytes.resize((w * h) as usize, 0);
603        let group = self.upload(w, h, wgpu::TextureFormat::R8Unorm, &bytes);
604        self.page_uploads.set(self.page_uploads.get() + 1);
605        self.pages
606            .borrow_mut()
607            .insert(page.id, (page.version, group.clone()));
608        group
609    }
610
611    /// The texture holding `src`, uploaded now if this version has not been.
612    fn image_texture(&self, src: &ImageRef<'_>) -> wgpu::BindGroup {
613        if let Some((version, group)) = self.images.borrow().get(&src.id)
614            && *version == src.version
615        {
616            return group.clone();
617        }
618        let size = src.view.size();
619        let bytes = rgba_bytes(&src.view);
620        let group = self.upload(
621            size.width.max(1),
622            size.height.max(1),
623            wgpu::TextureFormat::Rgba8Unorm,
624            &bytes,
625        );
626        self.image_uploads.set(self.image_uploads.get() + 1);
627        self.images
628            .borrow_mut()
629            .insert(src.id, (src.version, group.clone()));
630        group
631    }
632
633    fn upload(
634        &self,
635        width: u32,
636        height: u32,
637        format: wgpu::TextureFormat,
638        bytes: &[u8],
639    ) -> wgpu::BindGroup {
640        upload(
641            &self.device,
642            &self.queue,
643            &self.texture_layout,
644            &self.sampler,
645            width,
646            height,
647            format,
648            bytes,
649        )
650    }
651}
652
653/// Uploads one texture and binds it with the nearest sampler.
654#[allow(clippy::too_many_arguments)]
655fn upload(
656    device: &wgpu::Device,
657    queue: &wgpu::Queue,
658    layout: &wgpu::BindGroupLayout,
659    sampler: &wgpu::Sampler,
660    width: u32,
661    height: u32,
662    format: wgpu::TextureFormat,
663    bytes: &[u8],
664) -> wgpu::BindGroup {
665    let bytes_per_pixel = match format {
666        wgpu::TextureFormat::R8Unorm => 1,
667        _ => 4,
668    };
669    debug_assert_eq!(bytes.len(), (width * height * bytes_per_pixel) as usize);
670    let texture = device.create_texture_with_data(
671        queue,
672        &wgpu::TextureDescriptor {
673            label: None,
674            size: wgpu::Extent3d {
675                width,
676                height,
677                depth_or_array_layers: 1,
678            },
679            mip_level_count: 1,
680            sample_count: 1,
681            dimension: wgpu::TextureDimension::D2,
682            format,
683            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
684            view_formats: &[],
685        },
686        wgpu::util::TextureDataOrder::LayerMajor,
687        bytes,
688    );
689    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
690    device.create_bind_group(&wgpu::BindGroupDescriptor {
691        label: None,
692        layout,
693        entries: &[
694            wgpu::BindGroupEntry {
695                binding: 0,
696                resource: wgpu::BindingResource::TextureView(&view),
697            },
698            wgpu::BindGroupEntry {
699                binding: 1,
700                resource: wgpu::BindingResource::Sampler(sampler),
701            },
702        ],
703    })
704}
705
706/// Which bind group a run of vertices draws with.
707#[derive(Debug)]
708enum Draw {
709    /// Shapes: the white texture, never sampled.
710    Shapes(Range<u32>),
711    /// A blit: the texture at this index in the painter's list.
712    Textured { texture: usize, range: Range<u32> },
713}
714
715/// One frame's worth of drawing, recorded and then encoded by
716/// [`GpuPainter::finish`].
717///
718/// Obtained from [`Gpu::painter`]. Implements [`Painter`], so it is what a
719/// [`Pen`](denise::Pen) wraps and what `denise-ui`'s `Ui::paint_with`
720/// draws through.
721pub struct GpuPainter<'g> {
722    gpu: &'g Gpu,
723    size: Size,
724    clip: Rect,
725    vertices: Vec<Vertex>,
726    draws: Vec<Draw>,
727    textures: Vec<wgpu::BindGroup>,
728    /// Every edge of every polygon in the frame, as `x0, y0, x1, y1`. A
729    /// polygon's vertices carry where its own run begins and how long it is,
730    /// and the fragment shader reads the run back out.
731    edges: Vec<[f32; 4]>,
732    /// Rows to move before anything is drawn, as the rectangle and how far up
733    /// (down when negative). Only [`finish_onto`](GpuPainter::finish_onto)
734    /// can honour them: a cleared target has no rows to move.
735    scrolls: Vec<(Rect, i32)>,
736}
737
738impl GpuPainter<'_> {
739    /// Encodes and submits the frame into `target`, which must have the
740    /// format the [`Gpu`] was built for. The target is cleared first.
741    pub fn finish(self, target: &wgpu::TextureView) {
742        let gpu = self.gpu;
743        let mut encoder = gpu
744            .device
745            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
746                label: Some("denise frame"),
747            });
748        self.encode(
749            &mut encoder,
750            target,
751            wgpu::LoadOp::Clear(wgpu::Color::BLACK),
752            None,
753        );
754        gpu.queue.submit([encoder.finish()]);
755    }
756
757    /// Draws onto `target` **without clearing it**, restricted to `damage`.
758    ///
759    /// For a target the caller keeps between frames. What `damage` does not
760    /// cover is left exactly as the previous frame left it, which is what makes
761    /// an incremental repaint possible: a swapchain image cannot be used this
762    /// way because it rotates and its age cannot be trusted, so the caller owns
763    /// a texture of its own and copies from it.
764    ///
765    /// `damage` is scissored to its **union**, not rectangle by rectangle. Two
766    /// distant rectangles therefore cost their bounding box in fragments — the
767    /// per-vertex clip still makes each region exact, so this decides how much
768    /// is skipped rather than what is drawn. One pass is worth more than the
769    /// tightest possible scissor, because doing better means replaying the
770    /// vertices once per region.
771    ///
772    /// An empty `damage` draws nothing at all.
773    ///
774    /// Takes the texture rather than a view of it because a frame may first
775    /// move rows the previous frame drew ([`Pen::scroll_rows`](denise::Pen::scroll_rows)), and a copy
776    /// needs the texture; it must therefore be a copy source and destination
777    /// as well as an attachment. The rows are moved before anything is drawn,
778    /// so the strip a scroll exposes is painted over what the move left there.
779    pub fn finish_onto(self, target: &wgpu::Texture, damage: &[Rect]) {
780        let union = damage
781            .iter()
782            .filter(|r| !r.is_empty())
783            .copied()
784            .reduce(|a, b| a.union(&b));
785        if union.is_none() && self.scrolls.is_empty() {
786            return;
787        }
788        let gpu = self.gpu;
789        let mut encoder = gpu
790            .device
791            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
792                label: Some("denise damaged frame"),
793            });
794        for &(rect, dy) in &self.scrolls {
795            self.encode_scroll(&mut encoder, target, rect, dy);
796        }
797        if let Some(union) = union {
798            let view = target.create_view(&wgpu::TextureViewDescriptor::default());
799            self.encode(&mut encoder, &view, wgpu::LoadOp::Load, Some(union));
800        }
801        gpu.queue.submit([encoder.finish()]);
802    }
803
804    /// Moves the rows of `rect` that survive a scroll by `dy`: out to the
805    /// scratch texture and back in at their new place, because wgpu will not
806    /// copy a texture onto itself.
807    fn encode_scroll(
808        &self,
809        encoder: &mut wgpu::CommandEncoder,
810        target: &wgpu::Texture,
811        rect: Rect,
812        dy: i32,
813    ) {
814        let shift = dy.unsigned_abs();
815        let height = (rect.height as u32).saturating_sub(shift);
816        let width = rect.width as u32;
817        if height == 0 || width == 0 {
818            return;
819        }
820        // Up: the surviving rows start `shift` below the top and land at the
821        // top. Down: they start at the top and land `shift` below it.
822        let (from_y, to_y) = if dy > 0 {
823            (rect.y as u32 + shift, rect.y as u32)
824        } else {
825            (rect.y as u32, rect.y as u32 + shift)
826        };
827        let scratch = self.gpu.scratch(width, height);
828        let extent = wgpu::Extent3d {
829            width,
830            height,
831            depth_or_array_layers: 1,
832        };
833        fn at(texture: &wgpu::Texture, x: u32, y: u32) -> wgpu::TexelCopyTextureInfo<'_> {
834            wgpu::TexelCopyTextureInfo {
835                texture,
836                mip_level: 0,
837                origin: wgpu::Origin3d { x, y, z: 0 },
838                aspect: wgpu::TextureAspect::All,
839            }
840        }
841        encoder.copy_texture_to_texture(
842            at(target, rect.x as u32, from_y),
843            at(&scratch, 0, 0),
844            extent,
845        );
846        encoder.copy_texture_to_texture(
847            at(&scratch, 0, 0),
848            at(target, rect.x as u32, to_y),
849            extent,
850        );
851    }
852
853    /// Renders offscreen and reads the frame back as `0xAARRGGBB` words, row
854    /// after row with no padding — the layout a [`denise::Frame`] uses.
855    ///
856    /// Blocks until the GPU is done. For tests, snapshots and tools; a window
857    /// should use [`finish`](GpuPainter::finish).
858    pub fn finish_to_pixels(self) -> Result<Vec<u32>, Error> {
859        let gpu = self.gpu;
860        let (width, height) = (self.size.width.max(1), self.size.height.max(1));
861        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
862            label: Some("denise offscreen"),
863            size: wgpu::Extent3d {
864                width,
865                height,
866                depth_or_array_layers: 1,
867            },
868            mip_level_count: 1,
869            sample_count: 1,
870            dimension: wgpu::TextureDimension::D2,
871            format: gpu.format,
872            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
873            view_formats: &[],
874        });
875        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
876
877        let mut encoder = gpu
878            .device
879            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
880                label: Some("denise offscreen frame"),
881            });
882        self.encode(
883            &mut encoder,
884            &view,
885            wgpu::LoadOp::Clear(wgpu::Color::BLACK),
886            None,
887        );
888        gpu.queue.submit([encoder.finish()]);
889        gpu.read_texture(&texture)
890    }
891
892    fn encode(
893        &self,
894        encoder: &mut wgpu::CommandEncoder,
895        target: &wgpu::TextureView,
896        load: wgpu::LoadOp<wgpu::Color>,
897        scissor: Option<Rect>,
898    ) {
899        let gpu = self.gpu;
900        let globals_group = gpu.globals_for(self.size);
901        // An empty frame still clears; a zero-sized buffer is not allowed.
902        let vertex_bytes: &[u8] = if self.vertices.is_empty() {
903            &[0u8; std::mem::size_of::<Vertex>()]
904        } else {
905            bytemuck::cast_slice(&self.vertices)
906        };
907        // Allocated per frame, not written into a kept buffer: `write_buffer`
908        // stages the copy, and for the small payload a damaged frame carries
909        // that machinery costs more than the allocation it saves. Measured, in
910        // both directions -- see the crate README.
911        let vertices = gpu
912            .device
913            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
914                label: Some("denise vertices"),
915                contents: vertex_bytes,
916                usage: wgpu::BufferUsages::VERTEX,
917            });
918
919        // Most frames draw no polygon and bind the empty group the device was
920        // built with; the rest pay for a buffer, per frame and for the same
921        // reason the vertices are allocated per frame.
922        let edges_group = if self.edges.is_empty() {
923            None
924        } else {
925            let edges = gpu
926                .device
927                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
928                    label: Some("denise polygon edges"),
929                    contents: bytemuck::cast_slice(&self.edges),
930                    usage: wgpu::BufferUsages::STORAGE,
931                });
932            Some(gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
933                label: Some("denise polygon edges"),
934                layout: &gpu.edges_layout,
935                entries: &[wgpu::BindGroupEntry {
936                    binding: 0,
937                    resource: edges.as_entire_binding(),
938                }],
939            }))
940        };
941
942        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
943            label: Some("denise"),
944            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
945                view: target,
946                depth_slice: None,
947                resolve_target: None,
948                ops: wgpu::Operations {
949                    load,
950                    store: wgpu::StoreOp::Store,
951                },
952            })],
953            ..Default::default()
954        });
955        pass.set_pipeline(&gpu.pipeline);
956        pass.set_bind_group(0, &globals_group, &[]);
957        pass.set_bind_group(2, edges_group.as_ref().unwrap_or(&gpu.no_edges), &[]);
958        pass.set_vertex_buffer(0, vertices.slice(..));
959        // The rasteriser skips everything outside the damage, so those
960        // fragments never run; the per-vertex clip is what makes each region
961        // exact within it.
962        if let Some(r) = scissor {
963            let x = r.x.clamp(0, self.size.width as i32) as u32;
964            let y = r.y.clamp(0, self.size.height as i32) as u32;
965            let w = (r.right().clamp(0, self.size.width as i32) as u32).saturating_sub(x);
966            let h = (r.bottom().clamp(0, self.size.height as i32) as u32).saturating_sub(y);
967            if w == 0 || h == 0 {
968                return;
969            }
970            pass.set_scissor_rect(x, y, w, h);
971        }
972        for draw in &self.draws {
973            match draw {
974                Draw::Shapes(range) => {
975                    pass.set_bind_group(1, &gpu.white, &[]);
976                    pass.draw(range.clone(), 0..1);
977                }
978                Draw::Textured { texture, range } => {
979                    pass.set_bind_group(1, &self.textures[*texture], &[]);
980                    pass.draw(range.clone(), 0..1);
981                }
982            }
983        }
984    }
985
986    // ---- recording ----------------------------------------------------------
987
988    fn clip_f(&self) -> [f32; 4] {
989        [
990            self.clip.x as f32,
991            self.clip.y as f32,
992            self.clip.right() as f32,
993            self.clip.bottom() as f32,
994        ]
995    }
996
997    /// Appends one triangle to the current shape run, opening one if the last
998    /// draw was a blit.
999    fn triangle(
1000        &mut self,
1001        kind: u32,
1002        color: [f32; 4],
1003        a: [f32; 4],
1004        b: [f32; 4],
1005        pts: [[f32; 2]; 3],
1006    ) {
1007        let clip = self.clip_f();
1008        let start = self.vertices.len() as u32;
1009        debug_assert!(
1010            !is_textured(kind),
1011            "textured triangles go through `textured_quad`"
1012        );
1013        for pos in pts {
1014            self.vertices.push(Vertex {
1015                pos,
1016                clip,
1017                color,
1018                a,
1019                b,
1020                kind,
1021                poly: [0; 2],
1022                _pad: 0,
1023            });
1024        }
1025        let end = start + 3;
1026        match self.draws.last_mut() {
1027            Some(Draw::Shapes(range)) if range.end == start => range.end = end,
1028            _ => self.draws.push(Draw::Shapes(start..end)),
1029        }
1030    }
1031
1032    /// A quad from `x0,y0` to `x1,y1` in pixels, as two shape triangles.
1033    fn quad(&mut self, kind: u32, color: [f32; 4], a: [f32; 4], b: [f32; 4], bounds: [f32; 4]) {
1034        let [x0, y0, x1, y1] = bounds;
1035        self.triangle(kind, color, a, b, [[x0, y0], [x1, y0], [x1, y1]]);
1036        self.triangle(kind, color, a, b, [[x0, y0], [x1, y1], [x0, y1]]);
1037    }
1038
1039    /// The quad a polygon is drawn over, carrying its run of the frame's edge
1040    /// buffer. The only shape whose vertices say anything the fragment shader
1041    /// has to go and look up.
1042    fn polygon_quad(&mut self, color: [f32; 4], bounds: [f32; 4], run: [u32; 2]) {
1043        let clip = self.clip_f();
1044        let [x0, y0, x1, y1] = bounds;
1045        let start = self.vertices.len() as u32;
1046        for pos in [[x0, y0], [x1, y0], [x1, y1], [x0, y0], [x1, y1], [x0, y1]] {
1047            self.vertices.push(Vertex {
1048                pos,
1049                clip,
1050                color,
1051                a: [0.0; 4],
1052                b: [0.0; 4],
1053                kind: KIND_POLYGON,
1054                poly: run,
1055                _pad: 0,
1056            });
1057        }
1058        let end = start + 6;
1059        match self.draws.last_mut() {
1060            Some(Draw::Shapes(range)) if range.end == start => range.end = end,
1061            _ => self.draws.push(Draw::Shapes(start..end)),
1062        }
1063    }
1064
1065    /// A textured quad covering `dest`, sampling the whole of texture `index`.
1066    /// A textured quad covering `dest`, sampling `uv` (`u0, v0, u1, v1`) of
1067    /// texture `index` — the whole of it for a blit, a glyph's rectangle for
1068    /// an atlas page.
1069    fn textured_quad(
1070        &mut self,
1071        kind: u32,
1072        color: [f32; 4],
1073        index: usize,
1074        dest: Rect,
1075        uv: [f32; 4],
1076        radius_box: ([f32; 4], f32),
1077    ) {
1078        let clip = self.clip_f();
1079        let (x0, y0) = (dest.x as f32, dest.y as f32);
1080        let (x1, y1) = (dest.right() as f32, dest.bottom() as f32);
1081        let [u0, v0, u1, v1] = uv;
1082        let corners = [
1083            ([x0, y0], [u0, v0]),
1084            ([x1, y0], [u1, v0]),
1085            ([x1, y1], [u1, v1]),
1086            ([x0, y1], [u0, v1]),
1087        ];
1088        let (b, radius) = radius_box;
1089        let start = self.vertices.len() as u32;
1090        for i in [0usize, 1, 2, 0, 2, 3] {
1091            let (pos, uv) = corners[i];
1092            self.vertices.push(Vertex {
1093                pos,
1094                clip,
1095                color,
1096                a: [uv[0], uv[1], radius, 0.0],
1097                b,
1098                kind,
1099                poly: [0; 2],
1100                _pad: 0,
1101            });
1102        }
1103        self.draws.push(Draw::Textured {
1104            texture: index,
1105            range: start..start + 6,
1106        });
1107    }
1108
1109    fn upload_view(&mut self, src: &PixelView<'_>) -> usize {
1110        let size = src.size();
1111        let bytes = rgba_bytes(src);
1112        self.textures.push(self.gpu.upload(
1113            size.width,
1114            size.height,
1115            wgpu::TextureFormat::Rgba8Unorm,
1116            &bytes,
1117        ));
1118        self.textures.len() - 1
1119    }
1120}
1121
1122/// A premultiplied `0xAARRGGBB` view as the R G B A bytes a texture wants.
1123fn rgba_bytes(src: &PixelView<'_>) -> Vec<u8> {
1124    let size = src.size();
1125    let mut bytes = Vec::with_capacity((size.width * size.height * 4) as usize);
1126    for y in 0..size.height as i32 {
1127        let row = src.row(y, 0, size.width as i32).unwrap_or(&[]);
1128        for &word in row {
1129            bytes.extend_from_slice(&[
1130                (word >> 16) as u8,
1131                (word >> 8) as u8,
1132                word as u8,
1133                (word >> 24) as u8,
1134            ]);
1135        }
1136    }
1137    // A view narrower than it claims still fills its texture.
1138    bytes.resize((size.width.max(1) * size.height.max(1) * 4) as usize, 0);
1139    bytes
1140}
1141
1142fn is_textured(kind: u32) -> bool {
1143    matches!(kind, KIND_TEXTURED | KIND_MASK | KIND_TEXTURED_ROUNDED)
1144}
1145
1146/// A premultiplied `Paint` as the `[r, g, b, a]` floats the shader blends with.
1147fn rgba(paint: Paint) -> [f32; 4] {
1148    let w = paint.premultiplied();
1149    [
1150        ((w >> 16) & 0xFF) as f32 / 255.0,
1151        ((w >> 8) & 0xFF) as f32 / 255.0,
1152        (w & 0xFF) as f32 / 255.0,
1153        ((w >> 24) & 0xFF) as f32 / 255.0,
1154    ]
1155}
1156
1157/// Centre and half-extents of `rect`, in continuous pixel coordinates.
1158fn box_of(rect: Rect) -> [f32; 4] {
1159    let hw = rect.width as f32 / 2.0;
1160    let hh = rect.height as f32 / 2.0;
1161    [rect.x as f32 + hw, rect.y as f32 + hh, hw, hh]
1162}
1163
1164impl Painter for GpuPainter<'_> {
1165    fn size(&self) -> Size {
1166        self.size
1167    }
1168
1169    fn format(&self) -> PixelFormat {
1170        PixelFormat::Argb8888
1171    }
1172
1173    fn clip(&self) -> Rect {
1174        self.clip
1175    }
1176
1177    fn push_clip(&mut self, rect: Rect) -> ClipToken {
1178        let previous = self.clip;
1179        self.clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
1180        ClipToken::restoring(previous)
1181    }
1182
1183    fn pop_clip(&mut self, token: ClipToken) {
1184        self.clip = token.previous();
1185    }
1186
1187    fn clear(&mut self, color: Color) {
1188        let clip = self.clip;
1189        self.fill_rect(clip, Paint::new(Color::rgb(color.r, color.g, color.b)));
1190    }
1191
1192    fn fill_rect(&mut self, rect: Rect, paint: Paint) {
1193        if paint.is_invisible() || rect.is_empty() || self.clip.is_empty() {
1194            return;
1195        }
1196        let c = rgba(paint);
1197        self.quad(
1198            KIND_SOLID,
1199            c,
1200            [0.0; 4],
1201            [0.0; 4],
1202            [
1203                rect.x as f32,
1204                rect.y as f32,
1205                rect.right() as f32,
1206                rect.bottom() as f32,
1207            ],
1208        );
1209    }
1210
1211    fn fill_rounded_rect(&mut self, rect: Rect, radius: i32, paint: Paint) {
1212        if paint.is_invisible() || rect.is_empty() || self.clip.is_empty() {
1213            return;
1214        }
1215        let r = radius.clamp(0, rect.width.min(rect.height) / 2);
1216        if r == 0 {
1217            return self.fill_rect(rect, paint);
1218        }
1219        let c = rgba(paint);
1220        self.quad(
1221            KIND_ROUNDED_FILL,
1222            c,
1223            box_of(rect),
1224            [r as f32, 0.0, 0.0, 0.0],
1225            [
1226                rect.x as f32,
1227                rect.y as f32,
1228                rect.right() as f32,
1229                rect.bottom() as f32,
1230            ],
1231        );
1232    }
1233
1234    fn stroke_rounded_rect(&mut self, rect: Rect, radius: i32, thickness: i32, paint: Paint) {
1235        let t = thickness.max(0);
1236        if t == 0 || paint.is_invisible() || rect.is_empty() || self.clip.is_empty() {
1237            return;
1238        }
1239        if t * 2 >= rect.width.min(rect.height) {
1240            return self.fill_rounded_rect(rect, radius, paint);
1241        }
1242        let r = radius.clamp(0, rect.width.min(rect.height) / 2);
1243        let c = rgba(paint);
1244        self.quad(
1245            KIND_ROUNDED_STROKE,
1246            c,
1247            box_of(rect),
1248            [r as f32, t as f32, 0.0, 0.0],
1249            [
1250                rect.x as f32,
1251                rect.y as f32,
1252                rect.right() as f32,
1253                rect.bottom() as f32,
1254            ],
1255        );
1256    }
1257
1258    fn fill_circle(&mut self, centre: Point, radius: i32, paint: Paint) {
1259        if radius <= 0 || paint.is_invisible() || self.clip.is_empty() {
1260            return;
1261        }
1262        let (cx, cy, r) = (centre.x as f32, centre.y as f32, radius as f32);
1263        let c = rgba(paint);
1264        self.quad(
1265            KIND_CIRCLE_FILL,
1266            c,
1267            [cx, cy, r, 0.0],
1268            [0.0; 4],
1269            [cx - r - 1.0, cy - r - 1.0, cx + r + 1.0, cy + r + 1.0],
1270        );
1271    }
1272
1273    fn stroke_circle(&mut self, centre: Point, radius: i32, thickness: i32, paint: Paint) {
1274        let t = thickness.max(0);
1275        if t == 0 || radius <= 0 || paint.is_invisible() || self.clip.is_empty() {
1276            return;
1277        }
1278        if t >= radius {
1279            return self.fill_circle(centre, radius, paint);
1280        }
1281        let (cx, cy, r) = (centre.x as f32, centre.y as f32, radius as f32);
1282        let c = rgba(paint);
1283        self.quad(
1284            KIND_CIRCLE_STROKE,
1285            c,
1286            [cx, cy, r, t as f32],
1287            [0.0; 4],
1288            [cx - r - 1.0, cy - r - 1.0, cx + r + 1.0, cy + r + 1.0],
1289        );
1290    }
1291
1292    fn stroke_arc(
1293        &mut self,
1294        centre: Point,
1295        radius: i32,
1296        thickness: i32,
1297        start: i32,
1298        sweep: i32,
1299        paint: Paint,
1300    ) {
1301        let t = thickness.max(0);
1302        if t == 0 || radius <= 0 || sweep == 0 || paint.is_invisible() || self.clip.is_empty() {
1303            return;
1304        }
1305        // Negative sweeps go anticlockwise: the same arc, described from its
1306        // other end.
1307        let (start, sweep) = if sweep < 0 {
1308            (start.wrapping_add(sweep), -(sweep as i64))
1309        } else {
1310            (start, sweep as i64)
1311        };
1312        if sweep >= TURN as i64 {
1313            return self.stroke_circle(centre, radius, thickness, paint);
1314        }
1315        let start = start.rem_euclid(TURN) as f32 / TURN as f32;
1316        let sweep = sweep as f32 / TURN as f32;
1317        let (cx, cy, r) = (centre.x as f32, centre.y as f32, radius as f32);
1318        let c = rgba(paint);
1319        self.quad(
1320            KIND_ARC,
1321            c,
1322            [cx, cy, r, t.min(radius) as f32],
1323            [start, sweep, 0.0, 0.0],
1324            [cx - r - 1.0, cy - r - 1.0, cx + r + 1.0, cy + r + 1.0],
1325        );
1326    }
1327
1328    fn draw_line(&mut self, a: Point, b: Point, paint: Paint) {
1329        if paint.is_invisible() || self.clip.is_empty() {
1330            return;
1331        }
1332        if a == b {
1333            return self.fill_rect(Rect::new(a.x, a.y, 1, 1), paint);
1334        }
1335        // Endpoints at pixel centres, a half-pixel wide capsule: the fragment
1336        // whose centre a Bresenham line would light is exactly the one the
1337        // distance says is covered.
1338        let (ax, ay) = (a.x as f32 + 0.5, a.y as f32 + 0.5);
1339        let (bx, by) = (b.x as f32 + 0.5, b.y as f32 + 0.5);
1340        let (dx, dy) = (bx - ax, by - ay);
1341        let len = (dx * dx + dy * dy).sqrt();
1342        let (ux, uy) = (dx / len, dy / len);
1343        let (px, py) = (-uy, ux);
1344        let c = rgba(paint);
1345        let pa = [ax, ay, bx, by];
1346        let pb = [0.5, 0.0, 0.0, 0.0];
1347        let corners = [
1348            [ax - ux - px, ay - uy - py],
1349            [bx + ux - px, by + uy - py],
1350            [bx + ux + px, by + uy + py],
1351            [ax - ux + px, ay - uy + py],
1352        ];
1353        self.triangle(KIND_LINE, c, pa, pb, [corners[0], corners[1], corners[2]]);
1354        self.triangle(KIND_LINE, c, pa, pb, [corners[0], corners[2], corners[3]]);
1355    }
1356
1357    fn fill_polygon_fx(&mut self, points: &[(i32, i32)], paint: Paint) {
1358        if points.len() < 3 || paint.is_invisible() || self.clip.is_empty() {
1359            return;
1360        }
1361        let first = self.edges.len() as u32;
1362        let (mut left, mut top) = (f32::MAX, f32::MAX);
1363        let (mut right, mut bottom) = (f32::MIN, f32::MIN);
1364        let at = |(x, y): (i32, i32)| [x as f32 / ONE as f32, y as f32 / ONE as f32];
1365        for i in 0..points.len() {
1366            let p = at(points[i]);
1367            let q = at(points[(i + 1) % points.len()]);
1368            self.edges.push([p[0], p[1], q[0], q[1]]);
1369            left = left.min(p[0]);
1370            top = top.min(p[1]);
1371            right = right.max(p[0]);
1372            bottom = bottom.max(p[1]);
1373        }
1374        // One quad over the bounds, grown by a pixel so the fragments the
1375        // outline passes through are inside it and can be partly covered. The
1376        // shader does the rest: nothing here decides what is filled.
1377        let bounds = [left - 1.0, top - 1.0, right + 1.0, bottom + 1.0];
1378        let run = [first, points.len() as u32];
1379        self.polygon_quad(rgba(paint), bounds, run);
1380    }
1381
1382    fn blit_mask(&mut self, at: Point, mask: &Mask<'_>, paint: Paint) {
1383        if paint.is_invisible() || self.clip.is_empty() {
1384            return;
1385        }
1386        let (w, h) = (mask.width(), mask.height());
1387        if w <= 0 || h <= 0 {
1388            return;
1389        }
1390        let mut bytes = Vec::with_capacity((w * h) as usize);
1391        for y in 0..h {
1392            bytes.extend_from_slice(mask.row(y));
1393        }
1394        self.textures.push(self.gpu.upload(
1395            w as u32,
1396            h as u32,
1397            wgpu::TextureFormat::R8Unorm,
1398            &bytes,
1399        ));
1400        let index = self.textures.len() - 1;
1401        self.textured_quad(
1402            KIND_MASK,
1403            rgba(paint),
1404            index,
1405            mask.bounds_at(at),
1406            WHOLE,
1407            ([0.0; 4], 0.0),
1408        );
1409    }
1410
1411    fn blit_glyph(&mut self, at: Point, page: &AtlasPage<'_>, rect: Rect, paint: Paint) {
1412        if paint.is_invisible() || rect.is_empty() || self.clip.is_empty() {
1413            return;
1414        }
1415        let (pw, ph) = (page.mask.width() as f32, page.mask.height() as f32);
1416        if pw <= 0.0 || ph <= 0.0 {
1417            return;
1418        }
1419        // The page is uploaded once per version; this is six vertices.
1420        let group = self.gpu.page_texture(page);
1421        self.textures.push(group);
1422        let index = self.textures.len() - 1;
1423        let uv = [
1424            rect.x as f32 / pw,
1425            rect.y as f32 / ph,
1426            rect.right() as f32 / pw,
1427            rect.bottom() as f32 / ph,
1428        ];
1429        let dest = Rect::new(at.x, at.y, rect.width, rect.height);
1430        self.textured_quad(KIND_MASK, rgba(paint), index, dest, uv, ([0.0; 4], 0.0));
1431    }
1432
1433    fn blit_image(&mut self, src: &ImageRef<'_>, dest: Rect) {
1434        if src.view.size().is_empty() || dest.is_empty() || self.clip.is_empty() {
1435            return;
1436        }
1437        // Uploaded once per version; this is six vertices.
1438        let group = self.gpu.image_texture(src);
1439        self.textures.push(group);
1440        let index = self.textures.len() - 1;
1441        self.textured_quad(KIND_TEXTURED, [1.0; 4], index, dest, WHOLE, ([0.0; 4], 0.0));
1442    }
1443
1444    fn blit_image_rounded(&mut self, src: &ImageRef<'_>, dest: Rect, shape: Rect, radius: i32) {
1445        if src.view.size().is_empty() || dest.is_empty() || self.clip.is_empty() {
1446            return;
1447        }
1448        let group = self.gpu.image_texture(src);
1449        self.textures.push(group);
1450        let index = self.textures.len() - 1;
1451        let r = radius.clamp(0, shape.width.min(shape.height) / 2) as f32;
1452        self.textured_quad(
1453            KIND_TEXTURED_ROUNDED,
1454            [1.0; 4],
1455            index,
1456            dest,
1457            WHOLE,
1458            (box_of(shape), r),
1459        );
1460    }
1461
1462    fn blit(&mut self, src: &PixelView<'_>, at: Point) {
1463        let size = src.size();
1464        if size.is_empty() || self.clip.is_empty() {
1465            return;
1466        }
1467        let index = self.upload_view(src);
1468        let dest = Rect::new(at.x, at.y, size.width as i32, size.height as i32);
1469        self.textured_quad(KIND_TEXTURED, [1.0; 4], index, dest, WHOLE, ([0.0; 4], 0.0));
1470    }
1471
1472    fn blit_scaled(&mut self, src: &PixelView<'_>, dest: Rect) {
1473        if src.size().is_empty() || dest.is_empty() || self.clip.is_empty() {
1474            return;
1475        }
1476        let index = self.upload_view(src);
1477        self.textured_quad(KIND_TEXTURED, [1.0; 4], index, dest, WHOLE, ([0.0; 4], 0.0));
1478    }
1479
1480    fn scroll_rows(&mut self, rect: Rect, dy: i32) -> bool {
1481        let Some(rect) = rect
1482            .intersect(&self.clip)
1483            .and_then(|r| r.intersect(&Rect::from_size(self.size)))
1484        else {
1485            return false;
1486        };
1487        if dy == 0 || dy.unsigned_abs() as i32 >= rect.height {
1488            return false;
1489        }
1490        self.scrolls.push((rect, dy));
1491        true
1492    }
1493
1494    fn blit_rounded(&mut self, src: &PixelView<'_>, dest: Rect, shape: Rect, radius: i32) {
1495        if src.size().is_empty() || dest.is_empty() || self.clip.is_empty() {
1496            return;
1497        }
1498        let index = self.upload_view(src);
1499        let r = radius.clamp(0, shape.width.min(shape.height) / 2) as f32;
1500        self.textured_quad(
1501            KIND_TEXTURED_ROUNDED,
1502            [1.0; 4],
1503            index,
1504            dest,
1505            WHOLE,
1506            (box_of(shape), r),
1507        );
1508    }
1509}
1510
1511/// Compiles the examples in this crate's README, so they cannot drift from the API
1512/// they claim to demonstrate. Never built except under `cargo test --doc`.
1513#[cfg(doctest)]
1514#[doc = include_str!("../README.md")]
1515struct Readme;
1516
1517#[cfg(test)]
1518mod tests {
1519    use super::*;
1520
1521    #[test]
1522    fn paint_converts_to_premultiplied_floats() {
1523        let c = rgba(Paint::new(Color::rgba(255, 0, 0, 128)));
1524        assert!((c[3] - 128.0 / 255.0).abs() < 1e-6);
1525        assert!(c[0] > 0.49 && c[0] < 0.51, "red is premultiplied: {}", c[0]);
1526        assert_eq!(c[1], 0.0);
1527    }
1528}