use std::collections::{HashMap, HashSet};
use peniko::{ImageQuality, ImageSampler};
use vello_common::TextureId;
use vello_common::encode::{EncodedExternalTexture, EncodedPaint};
use vello_common::geometry::RectU16;
use vello_common::kurbo::{Affine, Rect};
use vello_common::paint::{IndexedPaint, Paint};
use crate::gpu::atlas::natural_to_dest;
#[derive(Debug, Default)]
pub struct ExternalExtents {
extents: HashMap<u64, (u16, u16)>,
warned: HashSet<u64>,
}
impl ExternalExtents {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, id: u64, size: (u32, u32)) -> bool {
let (Ok(width), Ok(height)) = (u16::try_from(size.0), u16::try_from(size.1)) else {
self.extents.remove(&id);
return false;
};
if width == 0 || height == 0 {
self.extents.remove(&id);
return false;
}
self.extents.insert(id, (width, height));
self.warned.remove(&id);
true
}
pub fn unbind(&mut self, id: u64) {
self.extents.remove(&id);
}
#[must_use]
pub fn get(&self, id: u64) -> Option<(u16, u16)> {
self.extents.get(&id).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.extents.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.extents.is_empty()
}
fn take_warning(&mut self, id: u64) -> bool {
self.warned.insert(id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExternalSkip {
Unregistered {
id: u64,
},
DegenerateDest,
SingularTransform,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExternalEncoding {
pub paint: Paint,
pub paint_index: usize,
}
pub fn encode_scene_texture(
id: u64,
dest: Rect,
transform: Affine,
extents: &mut ExternalExtents,
encoded_paints: &mut Vec<EncodedPaint>,
) -> Result<ExternalEncoding, ExternalSkip> {
let Some((width, height)) = extents.get(id) else {
note_unregistered(id, extents.take_warning(id));
return Err(ExternalSkip::Unregistered { id });
};
let paint_transform = natural_to_dest(transform, (u32::from(width), u32::from(height)), dest)
.ok_or(ExternalSkip::DegenerateDest)?;
let inverse = paint_transform.inverse();
if !inverse.as_coeffs().iter().all(|coeff| coeff.is_finite()) {
return Err(ExternalSkip::SingularTransform);
}
let paint_index = encoded_paints.len();
encoded_paints.push(EncodedPaint::ExternalTexture(EncodedExternalTexture {
texture_id: TextureId(id),
source_region: RectU16 {
x0: 0,
y0: 0,
x1: width,
y1: height,
},
sampler: ImageSampler {
quality: ImageQuality::Medium,
..ImageSampler::default()
},
may_have_transparency: true,
transform: inverse,
tint: None,
}));
Ok(ExternalEncoding {
paint: Paint::Indexed(IndexedPaint::new(paint_index)),
paint_index,
})
}
fn note_unregistered(id: u64, first: bool) {
if first {
log::warn!(
"SceneTexture {id} draws nothing: no texture is registered under that id \
(further sightings of this id are logged at debug level)"
);
} else {
log::debug!("SceneTexture {id} draws nothing: no texture is registered under that id");
}
}
#[cfg(test)]
mod tests {
use super::*;
const DEST: Rect = Rect::new(0.0, 0.0, 8.0, 4.0);
fn bound(size: (u32, u32)) -> ExternalExtents {
let mut extents = ExternalExtents::new();
assert!(extents.bind(7, size));
extents
}
#[test]
fn an_unregistered_id_encodes_nothing() {
let mut extents = ExternalExtents::new();
let mut paints = Vec::new();
let skip = encode_scene_texture(7, DEST, Affine::IDENTITY, &mut extents, &mut paints);
assert_eq!(skip, Err(ExternalSkip::Unregistered { id: 7 }));
assert!(paints.is_empty(), "a skipped draw leaves no orphan entry");
}
#[test]
fn an_unregistered_id_is_reported_once() {
let mut extents = ExternalExtents::new();
assert!(extents.take_warning(7), "the first sighting reports");
assert!(!extents.take_warning(7), "a later sighting does not");
assert!(extents.take_warning(8), "a different id reports on its own");
}
#[test]
fn rebinding_an_id_lets_it_report_again() {
let mut extents = ExternalExtents::new();
assert!(!extents.take_warning(7) || !extents.take_warning(7));
assert!(extents.bind(7, (4, 4)));
assert!(
extents.take_warning(7),
"a rebound id is a new fact about that id"
);
}
#[test]
fn a_bound_texture_encodes_its_whole_extent_as_the_source_region() {
let mut extents = bound((4, 2));
let mut paints = Vec::new();
let encoding = encode_scene_texture(7, DEST, Affine::IDENTITY, &mut extents, &mut paints)
.expect("a bound texture encodes");
assert_eq!(encoding.paint_index, 0);
match paints.as_slice() {
[EncodedPaint::ExternalTexture(entry)] => {
assert_eq!(entry.texture_id, TextureId(7));
assert_eq!(
entry.source_region,
RectU16 {
x0: 0,
y0: 0,
x1: 4,
y1: 2
}
);
assert!(
entry.may_have_transparency,
"a caller's texels are never claimed opaque"
);
}
other => panic!("expected one external-texture entry, got {other:?}"),
}
}
#[test]
fn the_encoded_transform_maps_the_destination_back_onto_the_texels() {
let mut extents = bound((4, 2));
let mut paints = Vec::new();
encode_scene_texture(7, DEST, Affine::IDENTITY, &mut extents, &mut paints)
.expect("a bound texture encodes");
let EncodedPaint::ExternalTexture(entry) = &paints[0] else {
panic!("expected an external-texture entry");
};
let far = entry.transform * kurbo::Point::new(8.0, 4.0);
assert!((far.x - 4.0).abs() < 1e-9, "x mapped to {}", far.x);
assert!((far.y - 2.0).abs() < 1e-9, "y mapped to {}", far.y);
}
#[test]
fn a_degenerate_destination_encodes_nothing() {
let mut extents = bound((4, 2));
let mut paints = Vec::new();
let skip = encode_scene_texture(
7,
Rect::new(0.0, 0.0, 0.0, 0.0),
Affine::scale(0.0),
&mut extents,
&mut paints,
);
assert_eq!(skip, Err(ExternalSkip::SingularTransform));
assert!(paints.is_empty());
}
#[test]
fn an_extent_past_the_packed_halves_is_refused_rather_than_truncated() {
let mut extents = ExternalExtents::new();
assert!(!extents.bind(7, (70_000, 8)));
assert!(extents.get(7).is_none());
}
#[test]
fn a_zero_extent_is_refused() {
let mut extents = ExternalExtents::new();
assert!(!extents.bind(7, (0, 8)));
assert!(extents.is_empty());
}
#[test]
fn unbinding_forgets_the_extent() {
let mut extents = bound((4, 2));
assert_eq!(extents.len(), 1);
extents.unbind(7);
assert!(extents.is_empty());
assert!(extents.get(7).is_none());
}
}