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::config::GpuConfig;
use super::paint_texture::{GpuEncodedImage, GpuEncodedPaint};
pub const STRIP_BIND_GROUP_COUNT: usize = 4;
const fn load_only_texture(
binding: u32,
visibility: wgpu::ShaderStages,
sample_type: wgpu::TextureSampleType,
view_dimension: wgpu::TextureViewDimension,
) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility,
ty: wgpu::BindingType::Texture {
sample_type,
view_dimension,
multisampled: false,
},
count: None,
}
}
const UNFILTERABLE: wgpu::TextureSampleType = wgpu::TextureSampleType::Float { filterable: false };
const FILTERABLE: wgpu::TextureSampleType = wgpu::TextureSampleType::Float { filterable: true };
pub const ATLAS_SAMPLER_BINDING: u32 = 2;
#[must_use]
pub fn strip_bind_group_layout_entries(
image_sampler: bool,
) -> [Vec<wgpu::BindGroupLayoutEntry>; STRIP_BIND_GROUP_COUNT] {
use wgpu::ShaderStages as S;
use wgpu::TextureSampleType as T;
use wgpu::TextureViewDimension as D;
[
vec![
load_only_texture(0, S::FRAGMENT, T::Uint, D::D2),
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: S::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(GpuConfig::SIZE),
},
count: None,
},
load_only_texture(2, S::FRAGMENT, UNFILTERABLE, D::D2),
],
if image_sampler {
vec![
load_only_texture(0, S::FRAGMENT, FILTERABLE, D::D2Array),
load_only_texture(1, S::FRAGMENT, UNFILTERABLE, D::D2),
wgpu::BindGroupLayoutEntry {
binding: ATLAS_SAMPLER_BINDING,
visibility: S::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
]
} else {
vec![
load_only_texture(0, S::FRAGMENT, UNFILTERABLE, D::D2Array),
load_only_texture(1, S::FRAGMENT, UNFILTERABLE, D::D2),
]
},
vec![load_only_texture(0, S::VERTEX_FRAGMENT, T::Uint, D::D2)],
vec![load_only_texture(0, S::FRAGMENT, UNFILTERABLE, D::D2)],
]
}
#[must_use]
pub fn strip_pipeline_layout(device: &wgpu::Device, image_sampler: bool) -> wgpu::PipelineLayout {
let entries = strip_bind_group_layout_entries(image_sampler);
let groups = entries.each_ref().map(|entries| {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("frust-engine strip group"),
entries,
})
});
let refs = groups.each_ref().map(Some);
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("frust-engine strip layout"),
bind_group_layouts: &refs,
immediate_size: 0,
})
}
#[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));
}
#[test]
fn the_explicit_strip_layout_is_the_uber_programs_derived_layout() {
for image_sampler in [false, true] {
explicit_layout_matches_derived(image_sampler);
}
}
fn explicit_layout_matches_derived(image_sampler: bool) {
use crate::gpu::shader_src::strip_program_source;
use crate::gpu::strips::PaintClass;
use wgpu::naga;
let src = strip_program_source(PaintClass::Uber, image_sampler);
let module = naga::front::wgsl::parse_str(&src).expect("the strip source parses");
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::empty(),
)
.validate(&module)
.expect("the strip source validates");
let sampled = |handle| {
(0..module.entry_points.len()).any(|index| {
info.get_entry_point(index)
.sampling_set
.iter()
.any(|key| key.image == handle)
})
};
let entries = strip_bind_group_layout_entries(image_sampler);
let mut declared = 0;
for (handle, global) in module.global_variables.iter() {
let Some(binding) = &global.binding else {
continue;
};
declared += 1;
let entry = entries
.get(binding.group as usize)
.and_then(|group| group.iter().find(|e| e.binding == binding.binding))
.unwrap_or_else(|| panic!("no layout entry for {binding:?}"));
let mut stages = wgpu::ShaderStages::NONE;
for (index, entry_point) in module.entry_points.iter().enumerate() {
if !info.get_entry_point(index)[handle].is_empty() {
stages |= match entry_point.stage {
naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX,
naga::ShaderStage::Fragment => wgpu::ShaderStages::FRAGMENT,
_ => wgpu::ShaderStages::NONE,
};
}
}
assert_eq!(entry.visibility, stages, "{:?} visibility", global.name);
match (&module.types[global.ty].inner, entry.ty) {
(
naga::TypeInner::Image {
dim: naga::ImageDimension::D2,
arrayed,
class: naga::ImageClass::Sampled { kind, multi: false },
},
wgpu::BindingType::Texture {
sample_type,
view_dimension,
multisampled: false,
},
) => {
let expected_dim = if *arrayed {
wgpu::TextureViewDimension::D2Array
} else {
wgpu::TextureViewDimension::D2
};
assert_eq!(view_dimension, expected_dim, "{:?}", global.name);
let expected_sample = match kind {
naga::ScalarKind::Uint => wgpu::TextureSampleType::Uint,
naga::ScalarKind::Float if sampled(handle) => FILTERABLE,
naga::ScalarKind::Float => UNFILTERABLE,
other => panic!("unexpected sample kind {other:?}"),
};
assert_eq!(sample_type, expected_sample, "{:?}", global.name);
}
(
naga::TypeInner::Sampler { comparison: false },
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
) => {
assert_eq!(binding.group, 1);
assert_eq!(binding.binding, ATLAS_SAMPLER_BINDING);
}
(
naga::TypeInner::Struct { span, .. },
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size,
},
) => {
assert_eq!(global.space, naga::AddressSpace::Uniform);
assert_eq!(
min_binding_size.map(|size| size.get()),
Some(u64::from(*span))
);
}
(shader, layout) => panic!("{:?}: {shader:?} vs {layout:?}", global.name),
}
}
assert_eq!(
declared,
entries.iter().map(Vec::len).sum::<usize>(),
"the layout declares a binding the program does not"
);
assert_eq!(
entries[1].len(),
if image_sampler { 3 } else { 2 },
"only the sampler family declares the sampler"
);
}
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()
}
}