1use std::{fmt::Write as _, hash::Hasher};
16
17use cranpose_render_common::{
18 brush_sampling::color_to_rgba,
19 graph_scene::RenderDiagnostics,
20 layer_shadow::{ShadowHole, ShadowRRect},
21};
22use cranpose_ui_graphics::{
23 BlendMode, Brush, Color, FxHashMap, FxHasher, Point, Rect, StrokeCap, StrokeJoin, TileMode,
24 whole,
25};
26
27use crate::{
28 draw::{
29 ClipBounds, RenderItem, ShapeCoverage, Target, TextRaster, clip_rect_to_window, draw_item,
30 place_shape, snap_delta_for_anchor,
31 },
32 scene::{DrawShape, RasterScene, ShadowDraw, TextDraw},
33};
34
35pub const SPRITE_FLOATS: usize = 36;
37
38pub mod field {
40 pub const FRAME: usize = 0;
42 pub const KIND: usize = 4;
46 pub const GEOMETRY: usize = 8;
50 pub const DETAIL: usize = 12;
54 pub const STROKE: usize = 16;
58 pub const COLOR: usize = 20;
60 pub const COLOR_END: usize = 24;
62 pub const GRADIENT: usize = 28;
65 pub const STOPS: usize = 32;
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[repr(u8)]
72pub enum SpriteKind {
73 Texture = 0,
75 Fill = 1,
77 Stroke = 2,
79 Arc = 3,
81 Line = 4,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct TextureUpload {
89 pub id: u32,
91 pub width: u32,
93 pub height: u32,
95 pub pixels: Vec<u8>,
98}
99
100#[derive(Default)]
103pub struct SpriteFrame {
104 pub(crate) scene: RasterScene,
106 sprites: Vec<f32>,
107 uploads: Vec<TextureUpload>,
108 releases: Vec<u32>,
109 textures: FxHashMap<u64, CachedTexture>,
110 next_texture: u32,
111 generation: u64,
112}
113
114struct CachedTexture {
115 id: u32,
116 placement: Rect,
118 last_used: u64,
119}
120
121const TEXTURE_RETENTION_FRAMES: u64 = 60;
124
125impl SpriteFrame {
126 pub fn sprites(&self) -> &[f32] {
128 &self.sprites
129 }
130
131 pub fn sprite_count(&self) -> usize {
133 self.sprites.len() / SPRITE_FLOATS
134 }
135
136 pub fn uploads(&self) -> &[TextureUpload] {
138 &self.uploads
139 }
140
141 pub fn releases(&self) -> &[u32] {
143 &self.releases
144 }
145
146 pub(crate) fn compose(
147 &mut self,
148 scene: &RasterScene,
149 width: u32,
150 height: u32,
151 diagnostics: &RenderDiagnostics,
152 text: &TextRaster,
153 ) {
154 self.sprites.clear();
155 self.uploads.clear();
156 self.releases.clear();
157 self.generation += 1;
158 let screen = ClipBounds {
159 min_x: 0,
160 min_y: 0,
161 max_x: width as i32,
162 max_y: height as i32,
163 };
164 for &item in &scene.order {
165 if let RenderItem::Shape(index) = item
166 && let Some(sprite) = shape_sprite(&scene.shapes[index], screen)
167 {
168 self.sprites.extend_from_slice(&sprite);
169 continue;
170 }
171 self.texture_sprite(scene, item, screen, diagnostics, text);
172 }
173 let generation = self.generation;
174 let releases = &mut self.releases;
175 self.textures.retain(|_, texture| {
176 let keep = generation - texture.last_used <= TEXTURE_RETENTION_FRAMES;
177 if !keep {
178 releases.push(texture.id);
179 }
180 keep
181 });
182 }
183
184 fn texture_sprite(
187 &mut self,
188 scene: &RasterScene,
189 item: RenderItem,
190 screen: ClipBounds,
191 diagnostics: &RenderDiagnostics,
192 text: &TextRaster,
193 ) {
194 let Some((canonical, offset, clip)) = Canonical::of(scene, item) else {
195 return;
196 };
197 let key = canonical.key();
198 let generation = self.generation;
199 let (id, placement) = match self.textures.get_mut(&key) {
200 Some(texture) => {
201 texture.last_used = generation;
202 (texture.id, texture.placement)
203 }
204 None => {
205 let Some((placement, pixels)) = canonical.rasterize(diagnostics, text) else {
206 return;
207 };
208 self.next_texture = self.next_texture.wrapping_add(1).max(1);
209 let id = self.next_texture;
210 self.uploads.push(TextureUpload {
211 id,
212 width: placement.width as u32,
213 height: placement.height as u32,
214 pixels,
215 });
216 self.textures.insert(
217 key,
218 CachedTexture {
219 id,
220 placement,
221 last_used: generation,
222 },
223 );
224 (id, placement)
225 }
226 };
227 let placement = placement.translate(offset.x, offset.y);
228 let Some(frame) = clip_rect_to_window(placement, clip, screen) else {
229 return;
230 };
231 let mut sprite = [0.0; SPRITE_FLOATS];
232 put_frame(&mut sprite, frame);
233 sprite[field::KIND] = SpriteKind::Texture as u8 as f32;
234 sprite[field::KIND + 1] = id as f32;
235 put_rect(&mut sprite, field::GEOMETRY, placement);
236 self.sprites.extend_from_slice(&sprite);
237 }
238}
239
240fn put_frame(sprite: &mut [f32; SPRITE_FLOATS], frame: ClipBounds) {
241 sprite[field::FRAME] = frame.min_x as f32;
242 sprite[field::FRAME + 1] = frame.min_y as f32;
243 sprite[field::FRAME + 2] = (frame.max_x - frame.min_x) as f32;
244 sprite[field::FRAME + 3] = (frame.max_y - frame.min_y) as f32;
245}
246
247fn put_rect(sprite: &mut [f32; SPRITE_FLOATS], at: usize, rect: Rect) {
248 sprite[at..at + 4].copy_from_slice(&[rect.x, rect.y, rect.width, rect.height]);
249}
250
251fn put_color(sprite: &mut [f32; SPRITE_FLOATS], at: usize, color: Color) {
252 sprite[at..at + 4].copy_from_slice(&color_to_rgba(color));
253}
254
255fn cap_code(cap: StrokeCap) -> f32 {
256 match cap {
257 StrokeCap::Butt => 0.0,
258 StrokeCap::Round => 1.0,
259 StrokeCap::Square => 2.0,
260 }
261}
262
263fn join_code(join: StrokeJoin) -> f32 {
264 match join {
265 StrokeJoin::Miter => 0.0,
266 StrokeJoin::Round => 1.0,
267 StrokeJoin::Bevel => 2.0,
268 }
269}
270
271fn tile_code(tile_mode: TileMode) -> f32 {
272 match tile_mode {
273 TileMode::Clamp => 0.0,
274 TileMode::Repeated => 1.0,
275 TileMode::Mirror => 2.0,
276 TileMode::Decal => 3.0,
277 }
278}
279
280fn shape_sprite(draw: &DrawShape, screen: ClipBounds) -> Option<[f32; SPRITE_FLOATS]> {
283 let blend = match draw.blend_mode {
284 BlendMode::DstOut => 1.0,
285 _ => 0.0,
286 };
287 let placed = place_shape(draw)?;
288 let mut sprite = [0.0; SPRITE_FLOATS];
289 match placed.coverage {
290 ShapeCoverage::Fill { rect, radii } => {
291 sprite[field::KIND] = SpriteKind::Fill as u8 as f32;
292 put_rect(&mut sprite, field::GEOMETRY, rect);
293 if let Some(radii) = radii {
294 sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
295 radii.top_left,
296 radii.top_right,
297 radii.bottom_right,
298 radii.bottom_left,
299 ]);
300 sprite[field::STROKE] = 1.0;
301 }
302 }
303 ShapeCoverage::Stroke {
304 rect,
305 radii,
306 half_width,
307 join,
308 } => {
309 sprite[field::KIND] = SpriteKind::Stroke as u8 as f32;
310 put_rect(&mut sprite, field::GEOMETRY, rect);
311 let radii = radii.unwrap_or_default();
312 sprite[field::DETAIL..field::DETAIL + 4].copy_from_slice(&[
313 radii.top_left,
314 radii.top_right,
315 radii.bottom_right,
316 radii.bottom_left,
317 ]);
318 sprite[field::STROKE] = half_width;
319 sprite[field::STROKE + 1] = join_code(join);
320 }
321 ShapeCoverage::Arc(arc) => {
322 sprite[field::KIND] = SpriteKind::Arc as u8 as f32;
323 sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
324 arc.center.x,
325 arc.center.y,
326 arc.inner_radius,
327 arc.outer_radius,
328 ]);
329 sprite[field::DETAIL..field::DETAIL + 3].copy_from_slice(&[
330 arc.start_angle,
331 arc.sweep_angle,
332 cap_code(arc.cap),
333 ]);
334 }
335 ShapeCoverage::Line(line) => {
336 sprite[field::KIND] = SpriteKind::Line as u8 as f32;
337 sprite[field::GEOMETRY..field::GEOMETRY + 4].copy_from_slice(&[
338 line.start.x,
339 line.start.y,
340 line.end.x,
341 line.end.y,
342 ]);
343 sprite[field::DETAIL] = line.half_width;
344 sprite[field::DETAIL + 1] = cap_code(line.cap);
345 }
346 ShapeCoverage::Trapezoid(_) => return None,
347 }
348 put_brush(&mut sprite, &draw.brush, placed.rect)?;
349 sprite[field::KIND + 2] = blend;
350 let frame = clip_rect_to_window(placed.coverage.reach(placed.rect), draw.clip, screen)?;
351 put_frame(&mut sprite, frame);
352 sprite[field::STOPS + 2] = placed.dither_origin.x;
354 sprite[field::STOPS + 3] = placed.dither_origin.y;
355 Some(sprite)
356}
357
358fn put_brush(sprite: &mut [f32; SPRITE_FLOATS], brush: &Brush, rect: Rect) -> Option<()> {
361 let two_colours = |colors: &[Color], stops: Option<&[f32]>| match (colors, stops) {
362 ([only], _) => Some((*only, *only, [0.0, 1.0])),
363 ([start, end], Some([from, to])) => Some((*start, *end, [*from, *to])),
364 ([start, end], _) => Some((*start, *end, [0.0, 1.0])),
365 _ => None,
366 };
367 let resolve = |origin: f32, extent: f32, value: f32| {
368 if value.is_finite() {
369 origin + value
370 } else if value.is_sign_positive() {
371 origin + extent
372 } else {
373 origin
374 }
375 };
376 let (code, (start, end, stops), gradient, tile) = match brush {
377 Brush::Solid(color) => (0.0, (*color, *color, [0.0, 1.0]), [0.0; 4], TileMode::Clamp),
378 Brush::LinearGradient {
379 colors,
380 stops,
381 start,
382 end,
383 tile_mode,
384 } => (
385 1.0,
386 two_colours(colors, stops.as_deref())?,
387 [
388 resolve(rect.x, rect.width, start.x),
389 resolve(rect.y, rect.height, start.y),
390 resolve(rect.x, rect.width, end.x),
391 resolve(rect.y, rect.height, end.y),
392 ],
393 *tile_mode,
394 ),
395 Brush::RadialGradient {
396 colors,
397 stops,
398 center,
399 radius,
400 tile_mode,
401 } => (
402 2.0,
403 two_colours(colors, stops.as_deref())?,
404 [
405 rect.x + center.x,
406 rect.y + center.y,
407 radius.max(f32::EPSILON),
408 0.0,
409 ],
410 *tile_mode,
411 ),
412 Brush::SweepGradient {
413 colors,
414 stops,
415 center,
416 } => (
417 3.0,
418 two_colours(colors, stops.as_deref())?,
419 [rect.x + center.x, rect.y + center.y, 0.0, 0.0],
420 TileMode::Clamp,
421 ),
422 };
423 sprite[field::KIND + 3] = code;
424 put_color(sprite, field::COLOR, start);
425 put_color(sprite, field::COLOR_END, end);
426 sprite[field::GRADIENT..field::GRADIENT + 4].copy_from_slice(&gradient);
427 sprite[field::STOPS] = stops[0];
428 sprite[field::STOPS + 1] = stops[1];
429 sprite[field::STROKE + 2] = tile_code(tile);
430 Some(())
431}
432
433struct Canonical {
437 scene: RasterScene,
438 item: RenderItem,
439}
440
441impl Canonical {
442 fn of(scene: &RasterScene, item: RenderItem) -> Option<(Self, Point, Option<Rect>)> {
445 let mut canonical = RasterScene::new();
446 let (origin, clip) = match item {
447 RenderItem::Shape(index) => {
448 let draw = &scene.shapes[index];
449 (Point::new(draw.rect.x, draw.rect.y), draw.clip)
450 }
451 RenderItem::Image(index) => {
452 let draw = &scene.images[index];
453 (Point::new(draw.rect.x, draw.rect.y), draw.clip)
454 }
455 RenderItem::Text(index) => {
456 let draw = &scene.texts[index];
457 (Point::new(draw.rect.x, draw.rect.y), draw.clip)
458 }
459 RenderItem::Shadow(index) => {
460 let draw = &scene.shadows[index];
461 let snap = draw
462 .snap_anchor
463 .map(snap_delta_for_anchor)
464 .unwrap_or_default();
465 (
466 Point::new(draw.shadow.bounds.x, draw.shadow.bounds.y),
467 draw.clip.map(|clip| clip.translate(snap.x, snap.y)),
468 )
469 }
470 };
471 if !(origin.x.is_finite() && origin.y.is_finite()) {
472 return None;
473 }
474 let shift = Point::new(whole::floor(origin.x), whole::floor(origin.y));
475 let item = match item {
476 RenderItem::Shape(index) => {
477 canonical
478 .shapes
479 .push(scene.shapes[index].translated(-shift.x, -shift.y));
480 RenderItem::Shape(0)
481 }
482 RenderItem::Image(index) => {
483 canonical
484 .images
485 .push(scene.images[index].translated(-shift.x, -shift.y));
486 RenderItem::Image(0)
487 }
488 RenderItem::Text(index) => {
489 canonical
490 .texts
491 .push(scene.texts[index].translated(-shift.x, -shift.y));
492 RenderItem::Text(0)
493 }
494 RenderItem::Shadow(index) => {
495 canonical
496 .shadows
497 .push(scene.shadows[index].translated(-shift.x, -shift.y));
498 RenderItem::Shadow(0)
499 }
500 };
501 Some((
502 Self {
503 scene: canonical,
504 item,
505 },
506 shift,
507 clip,
508 ))
509 }
510
511 fn key(&self) -> u64 {
513 let Self { scene, item } = self;
514 let mut hasher = HashWriter(FxHasher::default());
515 let _ = match item {
519 RenderItem::Text(_) => {
520 hash_text(&mut hasher.0, &scene.texts[0]);
521 Ok(())
522 }
523 RenderItem::Shadow(_) => {
524 hash_shadow(&mut hasher.0, &scene.shadows[0]);
525 Ok(())
526 }
527 RenderItem::Shape(_) => write!(hasher, "shape {:?}", scene.shapes[0]),
528 RenderItem::Image(_) => {
529 let draw = &scene.images[0];
530 write!(
531 hasher,
532 "image {} {:?} {:?} {} {:?} {:?} {:?} {:?} {:?}",
533 draw.image.id(),
534 draw.rect,
535 draw.snap_anchor,
536 draw.alpha,
537 draw.color_filter,
538 draw.sampling,
539 draw.blend_mode,
540 draw.src_rect,
541 draw.transform,
542 )
543 }
544 };
545 hasher.0.finish()
546 }
547
548 fn rasterize(
551 &self,
552 diagnostics: &RenderDiagnostics,
553 text: &TextRaster,
554 ) -> Option<(Rect, Vec<u8>)> {
555 let Self { scene, item } = self;
556 if let RenderItem::Text(_) = item {
557 return rasterize_text(&scene.texts[0], text);
558 }
559 let reach = scene.item_reach(*item)?;
560 let window = clip_rect_to_window(
561 reach,
562 None,
563 ClipBounds {
564 min_x: i32::MIN / 4,
565 min_y: i32::MIN / 4,
566 max_x: i32::MAX / 4,
567 max_y: i32::MAX / 4,
568 },
569 )?;
570 let width = (window.max_x - window.min_x) as u32;
571 let height = (window.max_y - window.min_y) as u32;
572 let mut pixels = vec![0u8; width as usize * height as usize * 4];
573 let mut target = Target {
574 pixels: pixels.as_chunks_mut::<4>().0,
575 left: window.min_x,
576 top: window.min_y,
577 width,
578 height,
579 };
580 draw_item(&mut target, scene, *item, diagnostics, text);
581 Some((
582 Rect {
583 x: window.min_x as f32,
584 y: window.min_y as f32,
585 width: width as f32,
586 height: height as f32,
587 },
588 pixels,
589 ))
590 }
591}
592
593fn rasterize_text(draw: &TextDraw, text: &TextRaster) -> Option<(Rect, Vec<u8>)> {
595 let (text, rect) = crate::draw::rasterize_text(draw, text)?;
596 let mut pixels = text.pixels;
597 for pixel in pixels.as_chunks_mut::<4>().0 {
602 let alpha = u16::from(pixel[3]);
603 for channel in &mut pixel[..3] {
604 let x = u16::from(*channel) * alpha + 127;
605 *channel = ((x + 1 + (x >> 8)) >> 8) as u8;
606 }
607 }
608 Some((rect, pixels))
609}
610
611fn hash_text(hasher: &mut FxHasher, draw: &TextDraw) {
617 let TextDraw {
618 rect,
619 snap_anchor,
620 text,
621 layout,
622 ink,
623 color,
624 text_style,
625 font_size,
626 scale,
627 clip,
628 } = draw;
629 hasher.write_u8(b't');
630 hash_rect(hasher, *rect);
631 hash_floats(hasher, &[layout.width(), layout.height()]);
632 hasher.write_u8(u8::from(
633 *ink == cranpose_render_common::text_raster::GlyphInk::Hidden,
634 ));
635 hash_point(hasher, *snap_anchor);
636 hasher.write_u64(text.render_hash());
637 hash_color(hasher, *color);
638 hasher.write_u64(text_style.render_hash());
639 hash_floats(hasher, &[*font_size, *scale]);
640 hash_clip(hasher, *clip);
641}
642
643fn hash_shadow(hasher: &mut FxHasher, draw: &ShadowDraw) {
645 let ShadowDraw {
646 shadow:
647 ShadowRRect {
648 bounds,
649 radii,
650 umbra_inset,
651 distance_correction,
652 hole,
653 },
654 snap_anchor,
655 color,
656 clip,
657 } = draw;
658 hasher.write_u8(b's');
659 hash_rect(hasher, *bounds);
660 hash_floats(hasher, radii);
661 hash_floats(hasher, &[*umbra_inset, *distance_correction]);
662 match hole {
663 Some(ShadowHole { inset, radius }) => {
664 hasher.write_u8(1);
665 hash_floats(hasher, &[*inset, *radius]);
666 }
667 None => hasher.write_u8(0),
668 }
669 hash_point(hasher, *snap_anchor);
670 hash_color(hasher, *color);
671 hash_clip(hasher, *clip);
672}
673
674fn hash_floats(hasher: &mut FxHasher, values: &[f32]) {
675 for value in values {
676 hasher.write_u32(value.to_bits());
677 }
678}
679
680fn hash_color(hasher: &mut FxHasher, color: Color) {
681 hash_floats(hasher, &[color.0, color.1, color.2, color.3]);
682}
683
684fn hash_clip(hasher: &mut FxHasher, clip: Option<Rect>) {
685 match clip {
686 Some(clip) => {
687 hasher.write_u8(1);
688 hash_rect(hasher, clip);
689 }
690 None => hasher.write_u8(0),
691 }
692}
693
694fn hash_rect(hasher: &mut FxHasher, rect: Rect) {
695 hash_floats(hasher, &[rect.x, rect.y, rect.width, rect.height]);
696}
697
698fn hash_point(hasher: &mut FxHasher, point: Option<Point>) {
699 match point {
700 Some(point) => {
701 hasher.write_u8(1);
702 hash_floats(hasher, &[point.x, point.y]);
703 }
704 None => hasher.write_u8(0),
705 }
706}
707
708struct HashWriter(FxHasher);
709
710impl std::fmt::Write for HashWriter {
711 fn write_str(&mut self, text: &str) -> std::fmt::Result {
712 self.0.write(text.as_bytes());
713 Ok(())
714 }
715}
716
717#[cfg(test)]
718#[path = "tests/sprites_tests.rs"]
719mod tests;