1use crate::drawing::BlendSpace;
4use crate::{Point, Rect, ValidationError};
5use std::{any::Any, fmt::Debug};
6
7mod analytic;
8
9pub use analytic::{
10 ANALYTIC_HEADER_WORDS, ANALYTIC_STATE_WORDS, AnalyticInstanceState, AnalyticInstanceUpload,
11 AnalyticTrajectoryKind,
12};
13
14pub type InstanceComposite = fn(&mut [f32; 4], [f32; 4], BlendSpace) -> Result<(), ValidationError>;
16
17pub trait SpatialInstances: Debug + Send + Sync {
20 fn support(&self, index: usize, antialias: f64) -> Rect;
22
23 fn sample_indices(
26 &self,
27 indices: &[usize],
28 point: Point,
29 antialias: f64,
30 destination: [f32; 4],
31 space: BlendSpace,
32 composite: InstanceComposite,
33 ) -> Result<[f32; 4], ValidationError>;
34}
35
36pub const MAX_INSTANCE_COUNT: usize = 1_048_576;
38
39const MIN_POLYGON_SIDES: f32 = 3.0;
41
42const MAX_POLYGON_SIDES: f32 = 32.0;
44
45const MAX_ASPECT: f32 = 64.0;
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[repr(u32)]
51pub enum InstanceKind {
52 Disc,
54 Glow,
56 Sprite,
58 Segment,
60 SpriteSheet,
62 TaperedSegment,
64 Polygon,
66 Ring,
68 Capsule,
70 Soft,
72 Quad,
74 Bokeh,
76 Ribbon,
78}
79
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
84#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
85#[repr(u32)]
86pub enum InstanceBlend {
87 #[default]
89 SourceOver,
90 Plus,
92 Screen,
94 Multiply,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
100pub struct InstanceRecord {
101 pub position: [f32; 2],
103 pub start: [f32; 2],
105 pub color: [f32; 4],
107 pub radius: f32,
109 pub rotation: f32,
111 pub width: f32,
113 pub kind: InstanceKind,
115}
116
117impl InstanceRecord {
118 fn words(self) -> Result<[f32; 12], ValidationError> {
119 let words = [
120 self.position[0],
121 self.position[1],
122 self.start[0],
123 self.start[1],
124 self.color[0],
125 self.color[1],
126 self.color[2],
127 self.color[3],
128 self.radius,
129 self.rotation,
130 self.width,
131 self.kind as u32 as f32,
132 ];
133 if words.iter().any(|value| !value.is_finite())
134 || self.radius < 0.0
135 || self.width < 0.0
136 || self.color[3] < 0.0
137 {
138 return Err(ValidationError::invalid(
139 "instances.upload",
140 "record",
141 "Primitive geometry and color must be finite with nonnegative size and alpha",
142 ));
143 }
144 Ok(words)
145 }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
152pub struct ExtendedInstanceRecord {
153 pub base: InstanceRecord,
156 pub extra: [f32; 4],
158}
159
160impl ExtendedInstanceRecord {
161 pub fn validate(self) -> Result<(), ValidationError> {
163 self.words().map(|_| ())
164 }
165
166 fn words(self) -> Result<[f32; 16], ValidationError> {
167 let mut words = [0.0; 16];
168 words[..12].copy_from_slice(&self.base.words()?);
169 let parameters = match self.base.kind {
170 InstanceKind::Polygon => {
171 (MIN_POLYGON_SIDES..=MAX_POLYGON_SIDES).contains(&self.extra[0])
172 && self.extra[0].fract() == 0.0
173 }
174 InstanceKind::Ring => self.extra[0] > 0.0 && self.extra[0] <= 1.0,
175 InstanceKind::Soft => (0.0..=1.0).contains(&self.extra[0]),
176 InstanceKind::Capsule | InstanceKind::Sprite => {
177 (1.0..=MAX_ASPECT).contains(&self.extra[2])
178 }
179 InstanceKind::Quad => self.extra[2] > 0.0 && self.extra[2] <= MAX_ASPECT,
180 InstanceKind::Bokeh => {
181 (self.extra[0] == 0.0
182 || ((MIN_POLYGON_SIDES..=MAX_POLYGON_SIDES).contains(&self.extra[0])
183 && self.extra[0].fract() == 0.0))
184 && (0.0..=1.0).contains(&self.extra[1])
185 && (0.0..=1.0).contains(&self.extra[2])
186 }
187 InstanceKind::SpriteSheet => self.base.width > 0.0 && self.base.width <= MAX_ASPECT,
188 _ => true,
189 };
190 if self.extra.iter().any(|value| !value.is_finite())
191 || !parameters
192 || self.base.color[3] > 1.0
193 || (self.base.kind == InstanceKind::SpriteSheet
194 && (self.extra.iter().any(|value| !(0.0..=1.0).contains(value))
195 || self.extra[0] >= self.extra[2]
196 || self.extra[1] >= self.extra[3]))
197 || (matches!(
198 self.base.kind,
199 InstanceKind::TaperedSegment | InstanceKind::Ribbon
200 ) && !(0.0..=1.0).contains(&self.extra[3]))
201 {
202 return Err(ValidationError::invalid(
203 "instances.upload",
204 "extension",
205 "Invalid finite extension, UV bounds or endpoint alpha",
206 ));
207 }
208 words[12..].copy_from_slice(&self.extra);
209 Ok(words)
210 }
211}
212
213#[derive(Debug)]
215pub struct InstanceUpload {
216 words: Vec<f32>,
217}
218
219impl InstanceUpload {
220 pub fn new(
224 records: impl ExactSizeIterator<Item = InstanceRecord>,
225 sprite_size: [u32; 2],
226 texels: impl IntoIterator<Item = [f32; 4]>,
227 ) -> Result<Self, ValidationError> {
228 Self::new_blended(records, sprite_size, texels, InstanceBlend::SourceOver)
229 }
230
231 pub fn new_blended(
233 records: impl ExactSizeIterator<Item = InstanceRecord>,
234 sprite_size: [u32; 2],
235 texels: impl IntoIterator<Item = [f32; 4]>,
236 blend: InstanceBlend,
237 ) -> Result<Self, ValidationError> {
238 Self::encode(
239 records.map(|record| Ok((record.kind, record.words()?))),
240 sprite_size,
241 texels,
242 blend,
243 )
244 }
245
246 pub fn new_extended(
248 records: impl ExactSizeIterator<Item = ExtendedInstanceRecord>,
249 sprite_size: [u32; 2],
250 texels: impl IntoIterator<Item = [f32; 4]>,
251 blend: InstanceBlend,
252 ) -> Result<Self, ValidationError> {
253 Self::encode(
254 records.map(|record| Ok((record.base.kind, record.words()?))),
255 sprite_size,
256 texels,
257 blend,
258 )
259 }
260
261 fn encode<const WORDS: usize>(
262 records: impl ExactSizeIterator<Item = Result<(InstanceKind, [f32; WORDS]), ValidationError>>,
263 sprite_size: [u32; 2],
264 texels: impl IntoIterator<Item = [f32; 4]>,
265 blend: InstanceBlend,
266 ) -> Result<Self, ValidationError> {
267 let invalid = || {
268 crate::ValidationError::invalid(
269 "instances.upload",
270 "layout",
271 "Primitive upload must match its bounded count, geometry and image dimensions",
272 )
273 };
274 let instances = records.len();
275 let [width, height] = sprite_size;
276 if instances > MAX_INSTANCE_COUNT
277 || width > 16_384
278 || height > 16_384
279 || (width == 0) != (height == 0)
280 {
281 return Err(invalid());
282 }
283 let pixels = (width as usize)
284 .checked_mul(height as usize)
285 .ok_or_else(invalid)?;
286 let expected = instances
287 .checked_mul(WORDS)
288 .and_then(|records| {
289 pixels
290 .checked_mul(4)
291 .and_then(|pixels| records.checked_add(pixels))
292 })
293 .and_then(|payload| payload.checked_add(4))
294 .ok_or_else(invalid)?;
295 let mut words = Vec::new();
296 words.try_reserve_exact(expected).map_err(|_| invalid())?;
297 let layout = WORDS as u32 + ((blend as u32) << 5);
299 words.extend([instances as f32, width as f32, height as f32, layout as f32]);
300 for (index, record) in records.enumerate() {
301 let (kind, record) = record?;
302 if index >= instances
303 || (matches!(kind, InstanceKind::Sprite | InstanceKind::SpriteSheet) && pixels == 0)
304 || (WORDS == 12 && kind as u32 > InstanceKind::Segment as u32)
305 {
306 return Err(invalid());
307 }
308 words.extend(record);
309 }
310 if words.len() != 4 + instances * WORDS {
311 return Err(invalid());
312 }
313 for (index, texel) in texels.into_iter().enumerate() {
314 if index >= pixels || texel.iter().any(|value| !value.is_finite()) || texel[3] < 0.0 {
315 return Err(invalid());
316 }
317 words.extend(texel);
318 }
319 if words.len() != expected {
320 return Err(invalid());
321 }
322 Ok(Self { words })
323 }
324
325 pub fn len(&self) -> usize {
327 self.words[0] as usize
328 }
329
330 pub fn is_empty(&self) -> bool {
332 self.len() == 0
333 }
334
335 pub fn bytes(&self) -> u64 {
337 self.words.len() as u64 * 4
338 }
339
340 pub fn into_words(self) -> Vec<f32> {
342 self.words
343 }
344}
345
346pub trait InstanceBatch: Any + Debug + Send + Sync {
348 fn len(&self) -> usize;
350
351 fn is_empty(&self) -> bool {
353 self.len() == 0
354 }
355
356 fn blend(&self) -> InstanceBlend {
358 InstanceBlend::SourceOver
359 }
360
361 fn bounds(&self) -> Rect;
363
364 fn bytes(&self) -> u64;
366
367 fn work(&self) -> u64;
369
370 fn spatial(&self) -> Option<&dyn SpatialInstances> {
372 None
373 }
374
375 fn validate(&self) -> Result<(), ValidationError>;
377
378 fn sample(
380 &self,
381 point: Point,
382 antialias: f64,
383 destination: [f32; 4],
384 space: BlendSpace,
385 composite: InstanceComposite,
386 ) -> Result<[f32; 4], ValidationError>;
387
388 fn sample_with_zero_identity(
394 &self,
395 point: Point,
396 antialias: f64,
397 destination: [f32; 4],
398 space: BlendSpace,
399 composite: InstanceComposite,
400 ) -> Result<[f32; 4], ValidationError> {
401 self.sample(point, antialias, destination, space, composite)
402 }
403
404 fn upload_bytes(&self) -> u64 {
406 0
407 }
408
409 fn upload(&self) -> Result<Option<InstanceUpload>, ValidationError> {
411 Ok(None)
412 }
413
414 fn analytic_upload(&self) -> Option<&AnalyticInstanceUpload> {
417 None
418 }
419
420 fn analytic_fallback(&self) -> Option<&ValidationError> {
422 None
423 }
424
425 fn as_any(&self) -> &dyn Any;
427
428 fn content_eq(&self, other: &dyn InstanceBatch) -> bool;
430}
431
432impl PartialEq for dyn InstanceBatch {
433 fn eq(&self, other: &Self) -> bool {
434 self.content_eq(other)
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn versioned_uploads_share_the_header_and_validate_analytic_extensions() {
444 let base = InstanceRecord {
445 position: [1.0, 2.0],
446 start: [0.0; 2],
447 color: [2.0, 0.1, 0.0, 0.5],
448 radius: 2.0,
449 rotation: 0.0,
450 width: 1.0,
451 kind: InstanceKind::Disc,
452 };
453 let old = InstanceUpload::new(std::iter::once(base), [0, 0], [])
454 .unwrap()
455 .into_words();
456 let extended = ExtendedInstanceRecord {
457 base,
458 extra: [0.0, 0.0, 1.0, 0.0],
459 };
460 let new = InstanceUpload::new_extended(
461 std::iter::once(extended),
462 [0, 0],
463 [],
464 InstanceBlend::Screen,
465 )
466 .unwrap()
467 .into_words();
468 assert_eq!(old[3], 12.0);
469 assert_eq!(new[3], 16.0 + ((InstanceBlend::Screen as u32) << 5) as f32);
470 assert_eq!(&old[4..], &new[4..16]);
471 assert_eq!(new.len(), 20);
472 for (kind, parameters) in [
473 (InstanceKind::Polygon, [2.0, 0.0, 1.0, 0.0]),
474 (InstanceKind::Ring, [0.0; 4]),
475 (InstanceKind::Quad, [0.0; 4]),
476 (InstanceKind::Soft, [2.0, 0.0, 1.0, 0.0]),
477 (InstanceKind::Bokeh, [4.5, 0.0, 1.0, 0.0]),
478 (InstanceKind::TaperedSegment, [0.0, 0.0, 0.0, 1.1]),
479 (InstanceKind::SpriteSheet, [0.5, 0.0, 0.4, 1.0]),
480 ] {
481 let record = ExtendedInstanceRecord {
482 base: InstanceRecord { kind, ..base },
483 extra: parameters,
484 };
485 assert!(record.validate().is_err(), "{kind:?}");
486 assert!(InstanceUpload::new(std::iter::once(record.base), [0, 0], []).is_err());
487 }
488 }
489
490 #[test]
491 fn primitive_records_are_compact_and_validate_hidden_geometry() {
492 let record = InstanceRecord {
493 position: [3.0, 4.0],
494 start: [-1.0, 2.0],
495 color: [-0.5, 2.0, 0.0, 0.5],
496 radius: 2.0,
497 rotation: 0.75,
498 width: 1.0,
499 kind: InstanceKind::Segment,
500 };
501 let words = record.words().unwrap();
502 assert_eq!(std::mem::size_of_val(&words), 48);
503 assert_eq!(&words[..4], &[3.0, 4.0, -1.0, 2.0]);
504 assert_eq!(words[11], 3.0);
505 for invalid in [
506 InstanceRecord {
507 position: [f32::NAN, 0.0],
508 ..record
509 },
510 InstanceRecord {
511 start: [f32::INFINITY, 0.0],
512 ..record
513 },
514 InstanceRecord {
515 color: [0.0, 0.0, 0.0, -1.0],
516 ..record
517 },
518 InstanceRecord {
519 radius: -1.0,
520 ..record
521 },
522 InstanceRecord {
523 rotation: f32::NAN,
524 ..record
525 },
526 InstanceRecord {
527 width: -1.0,
528 ..record
529 },
530 ] {
531 assert!(invalid.words().is_err());
532 }
533 }
534
535 #[test]
536 fn primitive_upload_checks_sprite_texels_and_exact_byte_charge() {
537 let record = InstanceRecord {
538 position: [1.0, 2.0],
539 start: [0.0; 2],
540 color: [0.5; 4],
541 radius: 2.0,
542 rotation: 0.0,
543 width: 1.0,
544 kind: InstanceKind::Sprite,
545 };
546 let upload = InstanceUpload::new([record].into_iter(), [2, 1], [[0.5; 4]; 2]).unwrap();
547 assert_eq!(upload.len(), 1);
548 assert_eq!(upload.bytes(), (4 + 12 + 8) * 4);
549 assert_eq!(&upload.into_words()[..4], &[1.0, 2.0, 1.0, 12.0]);
550 assert!(InstanceUpload::new([record].into_iter(), [0, 0], []).is_err());
551 assert!(InstanceUpload::new([record].into_iter(), [2, 0], []).is_err());
552 assert!(InstanceUpload::new([record].into_iter(), [16_385, 1], []).is_err());
553 assert!(InstanceUpload::new([record].into_iter(), [2, 1], [[0.0; 4]]).is_err());
554 assert!(InstanceUpload::new([record].into_iter(), [1, 1], [[0.0; 4]; 2]).is_err());
555 assert!(InstanceUpload::new([record].into_iter(), [1, 1], [[f32::NAN; 4]]).is_err());
556 let record = InstanceRecord {
557 kind: InstanceKind::Disc,
558 ..record
559 };
560 let upload = InstanceUpload::new(std::iter::repeat_n(record, 65_537), [0, 0], []).unwrap();
561 assert_eq!(upload.len(), 65_537);
562 assert_eq!(upload.bytes(), 16 + 65_537 * 48);
563 assert!(
564 InstanceUpload::new(
565 std::iter::repeat_n(record, MAX_INSTANCE_COUNT + 1),
566 [0, 0],
567 []
568 )
569 .is_err()
570 );
571 let empty = InstanceUpload::new(std::iter::empty(), [0, 0], []).unwrap();
572 assert!(empty.is_empty());
573 assert_eq!(empty.bytes(), 16);
574 }
575
576 #[test]
577 fn zero_identity_default_preserves_external_batch_sampling() {
578 #[derive(Debug)]
579 struct ExternalBatch;
580
581 impl InstanceBatch for ExternalBatch {
582 fn len(&self) -> usize {
583 1
584 }
585
586 fn bounds(&self) -> Rect {
587 Rect::default()
588 }
589
590 fn bytes(&self) -> u64 {
591 0
592 }
593
594 fn work(&self) -> u64 {
595 1
596 }
597
598 fn validate(&self) -> Result<(), ValidationError> {
599 Ok(())
600 }
601
602 fn sample(
603 &self,
604 _: Point,
605 _: f64,
606 mut destination: [f32; 4],
607 space: BlendSpace,
608 composite: InstanceComposite,
609 ) -> Result<[f32; 4], ValidationError> {
610 composite(&mut destination, [0.25, 0.125, 0.0, 0.5], space)?;
611 Ok(destination)
612 }
613
614 fn as_any(&self) -> &dyn Any {
615 self
616 }
617
618 fn content_eq(&self, other: &dyn InstanceBatch) -> bool {
619 other.as_any().is::<Self>()
620 }
621 }
622
623 fn capture(
624 destination: &mut [f32; 4],
625 source: [f32; 4],
626 _: BlendSpace,
627 ) -> Result<(), ValidationError> {
628 if source != [0.0; 4] {
629 *destination = source;
630 }
631 Ok(())
632 }
633 let batch: &dyn InstanceBatch = &ExternalBatch;
634 assert_eq!(
635 batch.sample_with_zero_identity(
636 Point::ORIGIN,
637 1.0,
638 [0.0; 4],
639 BlendSpace::LinearRgb,
640 capture
641 ),
642 batch.sample(Point::ORIGIN, 1.0, [0.0; 4], BlendSpace::LinearRgb, capture)
643 );
644 assert_eq!(
645 batch
646 .sample_with_zero_identity(Point::ORIGIN, 1.0, [0.0; 4], BlendSpace::Srgb, capture)
647 .unwrap(),
648 [0.25, 0.125, 0.0, 0.5]
649 );
650 }
651}