Skip to main content

cranpose_render_pixels/
sprites.rs

1//! The scene as sprites for a GPU compositor other than wgpu: SpriteKit on
2//! watchOS, which gives an app no Metal of its own.
3//!
4//! Every item becomes one quad in device pixels, drawn in order, cut to the
5//! whole pixels its clip leaves, as the rasterizer cuts it. The shapes the
6//! compositor's shader covers -- filled and stroked rounded rects, arcs and
7//! lines, painted with a solid colour or a two-colour gradient -- travel as
8//! numbers. Everything else is drawn here, once, by the rasterizer into a
9//! texture of its own, which is kept while the item stays the same: moving it
10//! by whole pixels moves the quad and keeps the texture.
11//!
12//! A sprite is [`SPRITE_FLOATS`] numbers; [`field`] names where each part
13//! starts. Positions are device pixels with the origin at the top left.
14
15use std::{
16    collections::HashMap,
17    fmt::Write as _,
18    hash::{DefaultHasher, Hasher},
19};
20
21use cranpose_render_common::{brush_sampling::color_to_rgba, graph_scene::RenderDiagnostics};
22use cranpose_ui_graphics::{
23    BlendMode, Brush, Color, Point, Rect, StrokeCap, StrokeJoin, TileMode, whole,
24};
25
26use crate::{
27    draw::{
28        ClipBounds, RenderItem, ShapeCoverage, Target, TextRaster, clip_rect_to_window, draw_item,
29        place_shape, snap_delta_for_anchor,
30    },
31    scene::{DrawShape, RasterScene},
32};
33
34/// How many numbers make one sprite.
35pub const SPRITE_FLOATS: usize = 36;
36
37/// Where each part of a sprite starts, and what it holds.
38pub mod field {
39    /// The quad: x, y, width, height.
40    pub const FRAME: usize = 0;
41    /// What the quad holds ([`super::SpriteKind`]), the texture it shows
42    /// (0 for none), how it blends (0 source over, 1 destination out) and its
43    /// brush (0 solid, 1 linear, 2 radial, 3 sweep).
44    pub const KIND: usize = 4;
45    /// The geometry, by kind. A texture: the rect the whole texture covers.
46    /// A fill or a stroke: the rect. An arc: centre x and y, inner and outer
47    /// radius. A line: start x and y, end x and y.
48    pub const GEOMETRY: usize = 8;
49    /// A fill or a stroke: the corner radii, top-left, top-right,
50    /// bottom-right, bottom-left. An arc: start angle, sweep, cap (0 butt,
51    /// 1 round, 2 square). A line: half width, cap.
52    pub const DETAIL: usize = 12;
53    /// A fill: whether its corners are rounded (1) or not (0). A stroke: half
54    /// width and join (0 miter, 1 round, 2 bevel). Third: a gradient's tile
55    /// mode (0 clamp, 1 repeated, 2 mirror, 3 decal).
56    pub const STROKE: usize = 16;
57    /// The brush's first colour, straight RGBA.
58    pub const COLOR: usize = 20;
59    /// A gradient's second colour, straight RGBA.
60    pub const COLOR_END: usize = 24;
61    /// A gradient's geometry: linear start x, y and end x, y; radial centre
62    /// x, y and radius; sweep centre x, y.
63    pub const GRADIENT: usize = 28;
64    /// A gradient's two stops, then the origin its dither is measured from.
65    pub const STOPS: usize = 32;
66}
67
68/// What one sprite holds.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70#[repr(u8)]
71pub enum SpriteKind {
72    /// A texture that the rasterizer drew for the item.
73    Texture = 0,
74    /// A filled rounded rectangle.
75    Fill = 1,
76    /// A stroked rounded rectangle.
77    Stroke = 2,
78    /// A circular arc band.
79    Arc = 3,
80    /// A stroked line segment.
81    Line = 4,
82}
83
84/// A texture the compositor has not seen yet: premultiplied RGBA8, row by
85/// row from the top.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct TextureUpload {
88    /// Handle of the texture. Sprites name it, and `releases` lists it once it is dropped.
89    pub id: u32,
90    /// Texture width in pixels.
91    pub width: u32,
92    /// Texture height in pixels.
93    pub height: u32,
94    /// Premultiplied RGBA8 pixels, row by row from the top: `width` times `height` times four
95    /// bytes.
96    pub pixels: Vec<u8>,
97}
98
99/// One frame of sprites, and the textures they use. It keeps its textures
100/// from frame to frame: compose into the same one every frame.
101#[derive(Default)]
102pub struct SpriteFrame {
103    /// The scene the last frame composed, whose storage the next one reuses.
104    pub(crate) scene: RasterScene,
105    sprites: Vec<f32>,
106    uploads: Vec<TextureUpload>,
107    releases: Vec<u32>,
108    textures: HashMap<u64, CachedTexture>,
109    next_texture: u32,
110    generation: u64,
111}
112
113struct CachedTexture {
114    id: u32,
115    /// Where the texture sits for the item at its canonical place.
116    placement: Rect,
117    last_used: u64,
118}
119
120/// Frames a texture outlives the last item that showed it, so an item that
121/// blinks does not draw itself again each time it comes back.
122const TEXTURE_RETENTION_FRAMES: u64 = 60;
123
124impl SpriteFrame {
125    /// The sprites, [`SPRITE_FLOATS`] numbers each, in drawing order.
126    pub fn sprites(&self) -> &[f32] {
127        &self.sprites
128    }
129
130    /// Number of sprites in the frame.
131    pub fn sprite_count(&self) -> usize {
132        self.sprites.len() / SPRITE_FLOATS
133    }
134
135    /// Textures first used this frame.
136    pub fn uploads(&self) -> &[TextureUpload] {
137        &self.uploads
138    }
139
140    /// Textures no sprite has used for a while; the compositor drops them.
141    pub fn releases(&self) -> &[u32] {
142        &self.releases
143    }
144
145    pub(crate) fn compose(
146        &mut self,
147        scene: &RasterScene,
148        width: u32,
149        height: u32,
150        diagnostics: &RenderDiagnostics,
151        text: &TextRaster,
152    ) {
153        self.sprites.clear();
154        self.uploads.clear();
155        self.releases.clear();
156        self.generation += 1;
157        let screen = ClipBounds {
158            min_x: 0,
159            min_y: 0,
160            max_x: width as i32,
161            max_y: height as i32,
162        };
163        for &item in &scene.order {
164            if let RenderItem::Shape(index) = item
165                && let Some(sprite) = shape_sprite(&scene.shapes[index], screen)
166            {
167                self.sprites.extend_from_slice(&sprite);
168                continue;
169            }
170            self.texture_sprite(scene, item, screen, diagnostics, text);
171        }
172        let generation = self.generation;
173        let releases = &mut self.releases;
174        self.textures.retain(|_, texture| {
175            let keep = generation - texture.last_used <= TEXTURE_RETENTION_FRAMES;
176            if !keep {
177                releases.push(texture.id);
178            }
179            keep
180        });
181    }
182
183    /// Shows `item` from a texture the rasterizer drew it into, drawing it
184    /// first if no texture holds it at its canonical place.
185    fn texture_sprite(
186        &mut self,
187        scene: &RasterScene,
188        item: RenderItem,
189        screen: ClipBounds,
190        diagnostics: &RenderDiagnostics,
191        text: &TextRaster,
192    ) {
193        let Some((canonical, offset, clip)) = Canonical::of(scene, item) else {
194            return;
195        };
196        let key = canonical.key();
197        let generation = self.generation;
198        let (id, placement) = match self.textures.get_mut(&key) {
199            Some(texture) => {
200                texture.last_used = generation;
201                (texture.id, texture.placement)
202            }
203            None => {
204                let Some((placement, pixels)) = canonical.rasterize(diagnostics, text) else {
205                    return;
206                };
207                self.next_texture = self.next_texture.wrapping_add(1).max(1);
208                let id = self.next_texture;
209                self.uploads.push(TextureUpload {
210                    id,
211                    width: placement.width as u32,
212                    height: placement.height as u32,
213                    pixels,
214                });
215                self.textures.insert(
216                    key,
217                    CachedTexture {
218                        id,
219                        placement,
220                        last_used: generation,
221                    },
222                );
223                (id, placement)
224            }
225        };
226        let placement = placement.translate(offset.x, offset.y);
227        let Some(frame) = clip_rect_to_window(placement, clip, screen) else {
228            return;
229        };
230        let mut sprite = [0.0; SPRITE_FLOATS];
231        put_frame(&mut sprite, frame);
232        sprite[field::KIND] = SpriteKind::Texture as u8 as f32;
233        sprite[field::KIND + 1] = id as f32;
234        put_rect(&mut sprite, field::GEOMETRY, placement);
235        self.sprites.extend_from_slice(&sprite);
236    }
237}
238
239fn put_frame(sprite: &mut [f32; SPRITE_FLOATS], frame: ClipBounds) {
240    sprite[field::FRAME] = frame.min_x as f32;
241    sprite[field::FRAME + 1] = frame.min_y as f32;
242    sprite[field::FRAME + 2] = (frame.max_x - frame.min_x) as f32;
243    sprite[field::FRAME + 3] = (frame.max_y - frame.min_y) as f32;
244}
245
246fn put_rect(sprite: &mut [f32; SPRITE_FLOATS], at: usize, rect: Rect) {
247    sprite[at..at + 4].copy_from_slice(&[rect.x, rect.y, rect.width, rect.height]);
248}
249
250fn put_color(sprite: &mut [f32; SPRITE_FLOATS], at: usize, color: Color) {
251    sprite[at..at + 4].copy_from_slice(&color_to_rgba(color));
252}
253
254fn cap_code(cap: StrokeCap) -> f32 {
255    match cap {
256        StrokeCap::Butt => 0.0,
257        StrokeCap::Round => 1.0,
258        StrokeCap::Square => 2.0,
259    }
260}
261
262fn join_code(join: StrokeJoin) -> f32 {
263    match join {
264        StrokeJoin::Miter => 0.0,
265        StrokeJoin::Round => 1.0,
266        StrokeJoin::Bevel => 2.0,
267    }
268}
269
270fn tile_code(tile_mode: TileMode) -> f32 {
271    match tile_mode {
272        TileMode::Clamp => 0.0,
273        TileMode::Repeated => 1.0,
274        TileMode::Mirror => 2.0,
275        TileMode::Decal => 3.0,
276    }
277}
278
279/// The sprite the compositor's shader draws `draw` with, or `None` when the
280/// shader does not cover it and a texture must.
281fn shape_sprite(draw: &DrawShape, screen: ClipBounds) -> Option<[f32; SPRITE_FLOATS]> {
282    let blend = match draw.blend_mode {
283        BlendMode::DstOut => 1.0,
284        _ => 0.0,
285    };
286    let placed = place_shape(draw)?;
287    let mut sprite = [0.0; SPRITE_FLOATS];
288    match placed.coverage {
289        ShapeCoverage::Fill { rect, radii } => {
290            sprite[field::KIND] = SpriteKind::Fill as u8 as f32;
291            put_rect(&mut sprite, field::GEOMETRY, rect);
292            if let Some(radii) = radii {
293                sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
294                    radii.top_left,
295                    radii.top_right,
296                    radii.bottom_right,
297                    radii.bottom_left,
298                ]);
299                sprite[field::STROKE] = 1.0;
300            }
301        }
302        ShapeCoverage::Stroke {
303            rect,
304            radii,
305            half_width,
306            join,
307        } => {
308            sprite[field::KIND] = SpriteKind::Stroke as u8 as f32;
309            put_rect(&mut sprite, field::GEOMETRY, rect);
310            let radii = radii.unwrap_or_default();
311            sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
312                radii.top_left,
313                radii.top_right,
314                radii.bottom_right,
315                radii.bottom_left,
316            ]);
317            sprite[field::STROKE] = half_width;
318            sprite[field::STROKE + 1] = join_code(join);
319        }
320        ShapeCoverage::Arc(arc) => {
321            sprite[field::KIND] = SpriteKind::Arc as u8 as f32;
322            sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
323                arc.center.x,
324                arc.center.y,
325                arc.inner_radius,
326                arc.outer_radius,
327            ]);
328            sprite[field::DETAIL..field::DETAIL + 3].copy_from_slice(&[
329                arc.start_angle,
330                arc.sweep_angle,
331                cap_code(arc.cap),
332            ]);
333        }
334        ShapeCoverage::Line(line) => {
335            sprite[field::KIND] = SpriteKind::Line as u8 as f32;
336            sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
337                line.start.x,
338                line.start.y,
339                line.end.x,
340                line.end.y,
341            ]);
342            sprite[field::DETAIL] = line.half_width;
343            sprite[field::DETAIL + 1] = cap_code(line.cap);
344        }
345        ShapeCoverage::Trapezoid(_) => return None,
346    }
347    put_brush(&mut sprite, &draw.brush, placed.rect)?;
348    sprite[field::KIND + 2] = blend;
349    let frame = clip_rect_to_window(placed.coverage.reach(placed.rect), draw.clip, screen)?;
350    put_frame(&mut sprite, frame);
351    // The dither's origin, which the shader measures pixels from.
352    sprite[field::STOPS + 2] = placed.dither_origin.x;
353    sprite[field::STOPS + 3] = placed.dither_origin.y;
354    Some(sprite)
355}
356
357/// Writes the brush the shader paints with, resolved against `rect` as the
358/// rasterizer resolves it; `None` for a gradient of more than two colours.
359fn put_brush(sprite: &mut [f32; SPRITE_FLOATS], brush: &Brush, rect: Rect) -> Option<()> {
360    let two_colours = |colors: &[Color], stops: Option<&[f32]>| match (colors, stops) {
361        ([only], _) => Some((*only, *only, [0.0, 1.0])),
362        ([start, end], Some([from, to])) => Some((*start, *end, [*from, *to])),
363        ([start, end], _) => Some((*start, *end, [0.0, 1.0])),
364        _ => None,
365    };
366    let resolve = |origin: f32, extent: f32, value: f32| {
367        if value.is_finite() {
368            origin + value
369        } else if value.is_sign_positive() {
370            origin + extent
371        } else {
372            origin
373        }
374    };
375    let (code, (start, end, stops), gradient, tile) = match brush {
376        Brush::Solid(color) => (0.0, (*color, *color, [0.0, 1.0]), [0.0; 4], TileMode::Clamp),
377        Brush::LinearGradient {
378            colors,
379            stops,
380            start,
381            end,
382            tile_mode,
383        } => (
384            1.0,
385            two_colours(colors, stops.as_deref())?,
386            [
387                resolve(rect.x, rect.width, start.x),
388                resolve(rect.y, rect.height, start.y),
389                resolve(rect.x, rect.width, end.x),
390                resolve(rect.y, rect.height, end.y),
391            ],
392            *tile_mode,
393        ),
394        Brush::RadialGradient {
395            colors,
396            stops,
397            center,
398            radius,
399            tile_mode,
400        } => (
401            2.0,
402            two_colours(colors, stops.as_deref())?,
403            [
404                rect.x + center.x,
405                rect.y + center.y,
406                radius.max(f32::EPSILON),
407                0.0,
408            ],
409            *tile_mode,
410        ),
411        Brush::SweepGradient {
412            colors,
413            stops,
414            center,
415        } => (
416            3.0,
417            two_colours(colors, stops.as_deref())?,
418            [rect.x + center.x, rect.y + center.y, 0.0, 0.0],
419            TileMode::Clamp,
420        ),
421    };
422    sprite[field::KIND + 3] = code;
423    put_color(sprite, field::COLOR, start);
424    put_color(sprite, field::COLOR_END, end);
425    sprite[field::GRADIENT..field::GRADIENT + 4].copy_from_slice(&gradient);
426    sprite[field::STOPS] = stops[0];
427    sprite[field::STOPS + 1] = stops[1];
428    sprite[field::STROKE + 2] = tile_code(tile);
429    Some(())
430}
431
432/// An item moved to its canonical place: shifted by whole pixels so its own
433/// rect starts within the first pixel, with no clip. Two frames that show the
434/// same item at whole-pixel distances share one canonical item.
435struct Canonical {
436    scene: RasterScene,
437    item: RenderItem,
438}
439
440impl Canonical {
441    /// The canonical item, the whole-pixel shift back to where `item` is,
442    /// and its clip.
443    fn of(scene: &RasterScene, item: RenderItem) -> Option<(Self, Point, Option<Rect>)> {
444        let mut canonical = RasterScene::new();
445        let (origin, clip) = match item {
446            RenderItem::Shape(index) => {
447                let draw = &scene.shapes[index];
448                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
449            }
450            RenderItem::Image(index) => {
451                let draw = &scene.images[index];
452                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
453            }
454            RenderItem::Text(index) => {
455                let draw = &scene.texts[index];
456                (Point::new(draw.rect.x, draw.rect.y), draw.clip)
457            }
458            RenderItem::Shadow(index) => {
459                let draw = &scene.shadows[index];
460                let snap = draw
461                    .snap_anchor
462                    .map(snap_delta_for_anchor)
463                    .unwrap_or_default();
464                (
465                    Point::new(draw.shadow.bounds.x, draw.shadow.bounds.y),
466                    draw.clip.map(|clip| clip.translate(snap.x, snap.y)),
467                )
468            }
469        };
470        if !(origin.x.is_finite() && origin.y.is_finite()) {
471            return None;
472        }
473        let shift = Point::new(whole::floor(origin.x), whole::floor(origin.y));
474        let item = match item {
475            RenderItem::Shape(index) => {
476                canonical
477                    .shapes
478                    .push(scene.shapes[index].translated(-shift.x, -shift.y));
479                RenderItem::Shape(0)
480            }
481            RenderItem::Image(index) => {
482                canonical
483                    .images
484                    .push(scene.images[index].translated(-shift.x, -shift.y));
485                RenderItem::Image(0)
486            }
487            RenderItem::Text(index) => {
488                canonical
489                    .texts
490                    .push(scene.texts[index].translated(-shift.x, -shift.y));
491                RenderItem::Text(0)
492            }
493            RenderItem::Shadow(index) => {
494                canonical
495                    .shadows
496                    .push(scene.shadows[index].translated(-shift.x, -shift.y));
497                RenderItem::Shadow(0)
498            }
499        };
500        Some((
501            Self {
502                scene: canonical,
503                item,
504            },
505            shift,
506            clip,
507        ))
508    }
509
510    /// A hash of everything that decides the canonical item's pixels.
511    fn key(&self) -> u64 {
512        let Self { scene, item } = self;
513        let mut hasher = HashWriter(DefaultHasher::new());
514        // The `Debug` forms name every field, positions included; an image's
515        // pixels are named by the bitmap's id instead of printed.
516        let _ = match item {
517            RenderItem::Shape(_) => write!(hasher, "shape {:?}", scene.shapes[0]),
518            RenderItem::Text(_) => write!(hasher, "text {:?}", scene.texts[0]),
519            RenderItem::Shadow(_) => write!(hasher, "shadow {:?}", scene.shadows[0]),
520            RenderItem::Image(_) => {
521                let draw = &scene.images[0];
522                write!(
523                    hasher,
524                    "image {} {:?} {:?} {} {:?} {:?} {:?} {:?}",
525                    draw.image.id(),
526                    draw.rect,
527                    draw.snap_anchor,
528                    draw.alpha,
529                    draw.color_filter,
530                    draw.sampling,
531                    draw.blend_mode,
532                    draw.src_rect,
533                )
534            }
535        };
536        hasher.0.finish()
537    }
538
539    /// Draws the canonical item into a texture of the whole pixels it
540    /// reaches: where the texture sits and its premultiplied pixels.
541    fn rasterize(
542        &self,
543        diagnostics: &RenderDiagnostics,
544        text: &TextRaster,
545    ) -> Option<(Rect, Vec<u8>)> {
546        let Self { scene, item } = self;
547        if let RenderItem::Text(_) = item {
548            return rasterize_text(&scene.texts[0], text);
549        }
550        let reach = scene.item_reach(*item)?;
551        let window = clip_rect_to_window(
552            reach,
553            None,
554            ClipBounds {
555                min_x: i32::MIN / 4,
556                min_y: i32::MIN / 4,
557                max_x: i32::MAX / 4,
558                max_y: i32::MAX / 4,
559            },
560        )?;
561        let width = (window.max_x - window.min_x) as u32;
562        let height = (window.max_y - window.min_y) as u32;
563        let mut pixels = vec![0u8; width as usize * height as usize * 4];
564        let mut target = Target {
565            pixels: &mut pixels,
566            left: window.min_x,
567            top: window.min_y,
568            width,
569            height,
570        };
571        draw_item(&mut target, scene, *item, diagnostics, text);
572        Some((
573            Rect {
574                x: window.min_x as f32,
575                y: window.min_y as f32,
576                width: width as f32,
577                height: height as f32,
578            },
579            pixels,
580        ))
581    }
582}
583
584/// Text drawn as the rasterizer draws it, into its own premultiplied image.
585fn rasterize_text(draw: &crate::scene::TextDraw, text: &TextRaster) -> Option<(Rect, Vec<u8>)> {
586    let (text, rect) = crate::draw::rasterize_text(draw, text)?;
587    let mut pixels = text.pixels;
588    for pixel in pixels.as_chunks_mut::<4>().0 {
589        let alpha = u32::from(pixel[3]);
590        for channel in &mut pixel[..3] {
591            *channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
592        }
593    }
594    Some((rect, pixels))
595}
596
597struct HashWriter(DefaultHasher);
598
599impl std::fmt::Write for HashWriter {
600    fn write_str(&mut self, text: &str) -> std::fmt::Result {
601        self.0.write(text.as_bytes());
602        Ok(())
603    }
604}