use frust_gpu::{PoolStats, PooledTexture, TextureAllocator, TextureDesc, TexturePool, TierCaps};
use crate::filters::blur::GpuFilterData;
use super::config::GpuConfig;
use super::pipelines::INTERMEDIATE_FORMAT;
pub const MAX_INTERMEDIATE_DIMENSION: u32 = 8192;
pub const RESIZE_KEEP_ALIVE_FRAMES: u64 = 2;
pub const INTERMEDIATE_USAGE: wgpu::TextureUsages = wgpu::TextureUsages::RENDER_ATTACHMENT
.union(wgpu::TextureUsages::TEXTURE_BINDING)
.union(wgpu::TextureUsages::COPY_SRC);
pub const RETAINED_PAGE_LABEL: &str = "frust-engine retained page";
#[must_use]
pub fn retained_page_descriptor(width: u32, height: u32) -> TextureDesc {
TextureDesc {
width,
height,
format: INTERMEDIATE_FORMAT,
usage: INTERMEDIATE_USAGE,
label: Some(RETAINED_PAGE_LABEL.to_string()),
}
}
#[must_use]
pub fn max_texture_size(caps: &TierCaps) -> u32 {
caps.max_texture_dimension_2d
.min(MAX_INTERMEDIATE_DIMENSION)
}
#[must_use]
pub fn atlas_layer_config(
page: (u32, u32),
alphas_tex_width: u32,
encoded_paints_tex_width: u32,
) -> GpuConfig {
GpuConfig::new(page.0, page.1, alphas_tex_width, encoded_paints_tex_width)
}
#[must_use]
pub fn atlas_page_fits(caps: &TierCaps, page: (u32, u32)) -> bool {
page.0 > 0
&& page.1 > 0
&& page.0 <= caps.max_texture_dimension_2d
&& page.1 <= caps.max_texture_dimension_2d
}
pub const FILTER_DATA_TEXTURE_WIDTH: u32 = 16;
const _: () = assert!(
FILTER_DATA_TEXTURE_WIDTH * super::TEXEL_BYTES == wgpu::COPY_BYTES_PER_ROW_ALIGNMENT,
"a filter-data row is exactly one copy alignment unit, which is what makes a one-row upload \
legal"
);
#[must_use]
pub fn filter_data_texture_descriptor(height: u32) -> wgpu::TextureDescriptor<'static> {
super::resource_texture_descriptor(
"frust-engine filter data texture",
FILTER_DATA_TEXTURE_WIDTH,
height,
)
}
#[must_use]
pub fn filter_data_texture_height(blocks: usize) -> Option<u32> {
let texels = u32::try_from(blocks)
.ok()?
.checked_mul(GpuFilterData::SIZE_TEXELS)?;
let height = texels
.div_ceil(FILTER_DATA_TEXTURE_WIDTH)
.max(super::MIN_RESOURCE_TEXTURE_HEIGHT);
(height <= MAX_INTERMEDIATE_DIMENSION).then_some(height)
}
#[must_use]
pub fn filter_sampler(device: &wgpu::Device) -> wgpu::Sampler {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("frust-engine filter sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
})
}
#[derive(Debug)]
pub enum IntermediateTexture<T = wgpu::Texture, V = wgpu::TextureView> {
Texture(PooledTexture<T, V>),
TooLarge {
width: u32,
height: u32,
max: u32,
},
}
impl<T, V> IntermediateTexture<T, V> {
#[must_use]
pub fn texture(&self) -> Option<&PooledTexture<T, V>> {
match self {
Self::Texture(texture) => Some(texture),
Self::TooLarge { .. } => None,
}
}
#[must_use]
pub fn is_too_large(&self) -> bool {
matches!(self, Self::TooLarge { .. })
}
}
#[derive(Debug)]
pub struct IntermediateTargets<T = wgpu::Texture, V = wgpu::TextureView> {
pool: TexturePool<T, V>,
max_texture_size: u32,
frame: u64,
}
impl<T, V> IntermediateTargets<T, V> {
#[must_use]
pub fn new(caps: &TierCaps) -> Self {
Self {
pool: TexturePool::new(caps),
max_texture_size: max_texture_size(caps),
frame: 0,
}
}
#[must_use]
pub fn max_texture_size(&self) -> u32 {
self.max_texture_size
}
#[must_use]
pub fn frame(&self) -> u64 {
self.frame
}
#[must_use]
pub fn stats(&self) -> PoolStats {
self.pool.stats()
}
#[must_use]
pub fn descriptor(width: u32, height: u32, label: &str) -> TextureDesc {
TextureDesc {
width,
height,
format: INTERMEDIATE_FORMAT,
usage: INTERMEDIATE_USAGE,
label: Some(label.to_string()),
}
}
pub fn acquire<A>(
&mut self,
allocator: &A,
width: u32,
height: u32,
label: &str,
) -> IntermediateTexture<T, V>
where
A: TextureAllocator<Texture = T, View = V>,
{
if width > self.max_texture_size || height > self.max_texture_size {
return IntermediateTexture::TooLarge {
width,
height,
max: self.max_texture_size,
};
}
let desc = Self::descriptor(width, height, label);
IntermediateTexture::Texture(self.pool.acquire(allocator, &desc, self.frame))
}
pub fn release(&mut self, texture: PooledTexture<T, V>) {
self.pool.release(texture);
}
pub fn end_frame(&mut self) {
self.frame = self.frame.saturating_add(1);
self.pool.age(self.frame);
}
pub fn drop_parked(&mut self) {
let horizon = self
.frame
.saturating_add(self.pool.max_unused_frames())
.saturating_sub(RESIZE_KEEP_ALIVE_FRAMES.saturating_sub(1));
self.pool.age(horizon);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use frust_gpu::DownlevelProfile;
#[derive(Debug, Default)]
struct FakeAllocator {
allocations: Cell<u32>,
}
impl TextureAllocator for FakeAllocator {
type Texture = u32;
type View = u32;
fn allocate_texture(&self, _desc: &TextureDesc) -> (u32, u32) {
self.allocations.set(self.allocations.get() + 1);
(self.allocations.get(), self.allocations.get())
}
}
fn caps() -> TierCaps {
TierCaps::fake(DownlevelProfile::Full)
}
fn targets() -> IntermediateTargets<u32, u32> {
IntermediateTargets::new(&caps())
}
#[test]
fn the_ceiling_is_the_smaller_of_the_adapter_limit_and_the_engine_cap() {
let mut caps = caps();
caps.max_texture_dimension_2d = 16384;
assert_eq!(max_texture_size(&caps), MAX_INTERMEDIATE_DIMENSION);
caps.max_texture_dimension_2d = 4096;
assert_eq!(max_texture_size(&caps), 4096);
}
#[test]
fn an_intermediate_is_a_sampled_copyable_render_attachment() {
let desc = IntermediateTargets::<u32, u32>::descriptor(64, 32, "layer");
assert_eq!(desc.format, INTERMEDIATE_FORMAT);
assert!(desc.usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
assert!(desc.usage.contains(wgpu::TextureUsages::TEXTURE_BINDING));
assert_eq!(desc.sample_count(), 1);
}
#[test]
fn an_atlas_layer_pass_maps_its_ndc_against_the_page_not_the_frame() {
let config = atlas_layer_config((1024, 1024), 2048, 1);
assert_eq!(config.width, 1024);
assert_eq!(config.height, 1024);
assert_eq!(config.strip_offset_x, 0);
assert_eq!(config.strip_offset_y, 0);
assert_eq!(config.negate_ndc, 0);
assert_eq!(config.alphas_tex_width_bits, 11, "log2(2048)");
assert_eq!(
config.encoded_paints_tex_width_bits, 0,
"a 1-texel stand-in reconstructs as `1 << 0`"
);
}
#[test]
fn a_page_past_the_adapters_own_limit_does_not_fit() {
let mut caps = caps();
caps.max_texture_dimension_2d = 2048;
assert!(atlas_page_fits(&caps, (2048, 2048)));
assert!(!atlas_page_fits(&caps, (4096, 1024)));
assert!(!atlas_page_fits(&caps, (1024, 4096)));
assert!(!atlas_page_fits(&caps, (0, 1024)));
assert!(!atlas_page_fits(&caps, (1024, 0)));
}
#[test]
fn the_atlas_pages_ceiling_is_the_adapters_rather_than_the_transient_cap() {
let mut caps = caps();
caps.max_texture_dimension_2d = 16384;
const { assert!(MAX_INTERMEDIATE_DIMENSION < 16384) };
assert!(atlas_page_fits(&caps, (16384, 16384)));
assert_eq!(max_texture_size(&caps), MAX_INTERMEDIATE_DIMENSION);
}
#[test]
fn the_filter_data_texture_is_one_copy_aligned_row_per_five_and_a_third_filters() {
let desc = filter_data_texture_descriptor(1);
assert_eq!(desc.size.width, FILTER_DATA_TEXTURE_WIDTH);
assert_eq!(desc.size.height, 1);
assert_eq!(desc.format, crate::gpu::RESOURCE_TEXTURE_FORMAT);
assert!(desc.usage.contains(wgpu::TextureUsages::TEXTURE_BINDING));
assert!(desc.usage.contains(wgpu::TextureUsages::COPY_DST));
assert!(!desc.usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
assert_eq!(filter_data_texture_height(0), Some(1), "never zero-height");
assert_eq!(filter_data_texture_height(1), Some(1));
assert_eq!(filter_data_texture_height(5), Some(1));
assert_eq!(filter_data_texture_height(6), Some(2));
assert_eq!(filter_data_texture_height(11), Some(3));
}
#[test]
fn a_filter_count_past_the_transient_ceiling_is_a_value_rather_than_a_device_error() {
let per_row = (FILTER_DATA_TEXTURE_WIDTH / GpuFilterData::SIZE_TEXELS) as usize;
let at_ceiling = per_row * MAX_INTERMEDIATE_DIMENSION as usize;
assert!(filter_data_texture_height(at_ceiling).is_some());
assert_eq!(filter_data_texture_height(usize::MAX), None);
assert_eq!(
filter_data_texture_height(at_ceiling * 2),
None,
"a block count no texture could hold is refused rather than clamped onto one"
);
}
#[test]
fn an_oversized_request_is_refused_without_allocating() {
let allocator = FakeAllocator::default();
let mut targets = targets();
let max = targets.max_texture_size();
let refused = targets.acquire(&allocator, max + 1, 16, "huge layer");
assert!(refused.is_too_large());
assert!(refused.texture().is_none());
assert!(matches!(
refused,
IntermediateTexture::TooLarge { width, height, max: ceiling }
if width == max + 1 && height == 16 && ceiling == max
));
assert_eq!(allocator.allocations.get(), 0);
assert_eq!(targets.stats().created, 0);
assert!(
targets
.acquire(&allocator, 16, max + 1, "huge layer")
.is_too_large()
);
assert_eq!(allocator.allocations.get(), 0);
}
#[test]
fn a_released_intermediate_is_reused_rather_than_reallocated() {
let allocator = FakeAllocator::default();
let mut targets = targets();
let first = targets.acquire(&allocator, 300, 200, "layer");
let pooled = match first {
IntermediateTexture::Texture(texture) => texture,
IntermediateTexture::TooLarge { .. } => unreachable!("300x200 is inside the ceiling"),
};
assert_eq!(pooled.requested_size(), (300, 200));
assert_eq!(pooled.size(), (512, 256));
targets.release(pooled);
let second = targets.acquire(&allocator, 300, 200, "layer");
assert!(second.texture().is_some());
assert_eq!(allocator.allocations.get(), 1, "the second acquire reuses");
assert_eq!(targets.stats().reused, 1);
}
#[test]
fn a_parked_entry_ages_out_after_the_keep_alive_window() {
let allocator = FakeAllocator::default();
let mut targets = targets();
let pooled = targets
.acquire(&allocator, 256, 256, "layer")
.texture()
.is_some();
assert!(pooled);
let entry = match targets.acquire(&allocator, 256, 256, "layer") {
IntermediateTexture::Texture(texture) => texture,
IntermediateTexture::TooLarge { .. } => unreachable!("256x256 is inside the ceiling"),
};
targets.release(entry);
assert_eq!(targets.stats().free, 1);
for _ in 0..60 {
targets.end_frame();
}
assert_eq!(
targets.stats().free,
1,
"still inside the keep-alive window"
);
targets.end_frame();
assert_eq!(targets.stats().free, 0);
assert_eq!(targets.stats().evicted, 1);
assert_eq!(targets.frame(), 61);
}
#[test]
fn a_resize_drops_the_parked_entries_the_recent_frames_did_not_use() {
let allocator = FakeAllocator::default();
let mut targets = targets();
for size in [256_u32, 512, 768] {
match targets.acquire(&allocator, size, size, "layer") {
IntermediateTexture::Texture(texture) => targets.release(texture),
IntermediateTexture::TooLarge { .. } => unreachable!("inside the ceiling"),
}
}
assert_eq!(targets.stats().free, 3);
assert_eq!(targets.stats().keys, 3);
for _ in 0..RESIZE_KEEP_ALIVE_FRAMES {
targets.end_frame();
}
targets.drop_parked();
assert_eq!(targets.stats().free, 0);
assert_eq!(targets.stats().keys, 0);
assert_eq!(targets.stats().evicted, 3);
assert!(targets.frame() < targets.pool.max_unused_frames());
assert_eq!(targets.stats().created, 3);
assert!(
targets
.acquire(&allocator, 256, 256, "layer")
.texture()
.is_some()
);
assert_eq!(targets.stats().created, 4);
}
#[test]
fn a_resize_storm_reuses_the_page_a_fixed_size_layer_keeps_asking_for() {
let allocator = FakeAllocator::default();
let mut targets = targets();
for _ in 0..200 {
targets.drop_parked();
match targets.acquire(&allocator, 640, 480, "layer") {
IntermediateTexture::Texture(texture) => targets.release(texture),
IntermediateTexture::TooLarge { .. } => unreachable!("inside the ceiling"),
}
targets.end_frame();
}
assert_eq!(
targets.stats().created,
1,
"a resize per frame must not turn the pool back into a plain allocator"
);
assert_eq!(targets.stats().reused, 199);
assert_eq!(targets.stats().evicted, 0);
assert_eq!(allocator.allocations.get(), 1);
}
#[test]
fn a_resize_does_not_age_the_pool_by_a_second_every_time_a_window_edge_moves() {
let allocator = FakeAllocator::default();
let mut targets = targets();
match targets.acquire(&allocator, 256, 256, "layer") {
IntermediateTexture::Texture(texture) => targets.release(texture),
IntermediateTexture::TooLarge { .. } => unreachable!("inside the ceiling"),
}
for _ in 0..100 {
targets.drop_parked();
}
assert_eq!(targets.frame(), 0, "a resize is not a frame");
assert_eq!(targets.stats().free, 1);
assert_eq!(targets.stats().evicted, 0);
}
#[test]
fn a_checked_out_intermediate_survives_a_drop_of_the_parked_entries() {
let allocator = FakeAllocator::default();
let mut targets = targets();
let held = match targets.acquire(&allocator, 256, 256, "layer") {
IntermediateTexture::Texture(texture) => texture,
IntermediateTexture::TooLarge { .. } => unreachable!("inside the ceiling"),
};
targets.drop_parked();
assert_eq!(targets.stats().evicted, 0, "nothing was parked to evict");
assert_eq!(targets.stats().in_use, 1);
targets.release(held);
assert_eq!(targets.stats().free, 1);
}
}