use std::collections::HashMap;
use frust_gpu::SceneTextureId;
use vello_common::encode::EncodedExternalTexture;
use super::atlas::{extend_mode, pack_image_offset, pack_image_params, pack_image_size, pack_tint};
use super::paint_texture::{GpuEncodedImage, GpuEncodedPaint};
#[derive(Debug)]
pub struct ExternalTextures<V = wgpu::TextureView> {
views: HashMap<u64, V>,
}
impl<V> Default for ExternalTextures<V> {
fn default() -> Self {
Self {
views: HashMap::new(),
}
}
}
impl<V> ExternalTextures<V> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, id: SceneTextureId, view: V) -> Option<V> {
self.views.insert(id.get(), view)
}
pub fn unbind(&mut self, id: SceneTextureId) -> Option<V> {
self.views.remove(&id.get())
}
#[must_use]
pub fn get(&self, id: SceneTextureId) -> Option<&V> {
self.views.get(&id.get())
}
#[must_use]
pub fn view(&self, key: u64) -> Option<&V> {
self.views.get(&key)
}
#[must_use]
pub fn len(&self) -> usize {
self.views.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.views.is_empty()
}
}
#[derive(Debug, Default)]
pub struct ExternalRuns {
keys: Vec<u64>,
}
impl ExternalRuns {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.keys.clear();
}
pub fn slot_of(&mut self, key: u64) -> Option<u32> {
if let Some(index) = self.keys.iter().position(|held| *held == key) {
return u32::try_from(index).ok();
}
let slot = u32::try_from(self.keys.len()).ok()?;
self.keys.push(key);
Some(slot)
}
#[must_use]
pub fn keys(&self) -> &[u64] {
&self.keys
}
#[must_use]
pub fn key_at(&self, slot: u32) -> Option<u64> {
self.keys.get(slot as usize).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.keys.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
}
#[must_use]
pub fn lower_encoded_external(entry: &EncodedExternalTexture) -> GpuEncodedPaint {
let region = entry.source_region;
let (tint, tint_mode) = pack_tint(entry.tint);
GpuEncodedPaint::Image(GpuEncodedImage {
image_params: pack_image_params(
entry.sampler.quality as u32,
extend_mode(entry.sampler.x_extend),
extend_mode(entry.sampler.y_extend),
0,
true,
),
image_size: pack_image_size(
region.x1.saturating_sub(region.x0),
region.y1.saturating_sub(region.y0),
),
image_offset: pack_image_offset(region.x0, region.y0),
transform: entry.transform.as_coeffs().map(|coeff| coeff as f32),
tint,
tint_mode,
image_padding: 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
use peniko::{ImageQuality, ImageSampler};
use vello_common::TextureId;
use vello_common::geometry::RectU16;
use vello_common::kurbo::Affine;
fn registry() -> ExternalTextures<&'static str> {
ExternalTextures::new()
}
fn entry(region: RectU16) -> EncodedExternalTexture {
EncodedExternalTexture {
texture_id: TextureId(1),
source_region: region,
sampler: ImageSampler {
quality: ImageQuality::Medium,
..ImageSampler::default()
},
may_have_transparency: true,
transform: Affine::IDENTITY,
tint: None,
}
}
#[test]
fn a_bound_view_is_reachable_by_id_and_by_the_scenes_own_number() {
let mut textures = registry();
let id = fresh_id();
assert!(textures.bind(id, "view").is_none());
assert_eq!(textures.get(id), Some(&"view"));
assert_eq!(textures.view(id.get()), Some(&"view"));
assert_eq!(textures.len(), 1);
}
#[test]
fn rebinding_returns_the_previous_view() {
let mut textures = registry();
let id = fresh_id();
textures.bind(id, "first");
assert_eq!(textures.bind(id, "second"), Some("first"));
assert_eq!(textures.get(id), Some(&"second"));
}
#[test]
fn unbinding_returns_the_view_and_empties_the_registry() {
let mut textures = registry();
let id = fresh_id();
textures.bind(id, "view");
assert_eq!(textures.unbind(id), Some("view"));
assert!(textures.is_empty());
assert_eq!(textures.view(id.get()), None);
}
#[test]
fn an_unbound_id_resolves_to_nothing() {
let textures = registry();
assert_eq!(textures.view(4_242), None);
}
#[test]
fn a_slot_is_assigned_once_per_texture_and_reused_after() {
let mut runs = ExternalRuns::new();
assert_eq!(runs.slot_of(7), Some(0));
assert_eq!(runs.slot_of(9), Some(1));
assert_eq!(runs.slot_of(7), Some(0), "the same texture keeps its slot");
assert_eq!(runs.keys(), &[7, 9]);
assert_eq!(runs.key_at(1), Some(9));
assert_eq!(runs.key_at(2), None);
}
#[test]
fn clearing_forgets_the_previous_frames_textures() {
let mut runs = ExternalRuns::new();
runs.slot_of(7);
runs.clear();
assert!(runs.is_empty());
assert_eq!(runs.slot_of(9), Some(0), "slots restart each frame");
}
#[test]
fn a_lowered_external_record_names_the_external_source_and_no_atlas_layer() {
let record = lower_encoded_external(&entry(RectU16 {
x0: 0,
y0: 0,
x1: 64,
y1: 32,
}));
let GpuEncodedPaint::Image(image) = record else {
panic!("an external texture lowers to the image record");
};
assert_eq!(
(image.image_params >> 14) & 0x1,
1,
"the source-kind bit selects the external binding"
);
assert_eq!((image.image_params >> 6) & 0xFF, 0, "no atlas layer");
assert_eq!(image.image_size, pack_image_size(64, 32));
assert_eq!(image.image_offset, pack_image_offset(0, 0));
assert_eq!(image.image_padding, 0, "nothing was packed beside it");
}
#[test]
fn a_sub_rectangle_lowers_as_its_own_offset_and_extent() {
let record = lower_encoded_external(&entry(RectU16 {
x0: 8,
y0: 4,
x1: 24,
y1: 20,
}));
let GpuEncodedPaint::Image(image) = record else {
panic!("an external texture lowers to the image record");
};
assert_eq!(image.image_offset, pack_image_offset(8, 4));
assert_eq!(image.image_size, pack_image_size(16, 16));
}
fn fresh_id() -> SceneTextureId {
frust_gpu::Texture::new(
(),
(),
frust_gpu::TextureDesc {
width: 1,
height: 1,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
label: None,
},
)
.as_scene_texture()
}
}