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