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