use frust_gpu::TierCaps;
use vello_common::geometry::RectU16;
use crate::error::EngineError;
use crate::filters::blur::FILTER_ATLAS_PADDING;
use crate::gpu::targets::max_texture_size;
pub const DEFAULT_MIN_PAGE_SIZE: u32 = 512;
pub const DEFAULT_MAX_PAGE_SIZE: u32 = 4096;
pub const MAX_PAGE_BANDS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PageParity {
Even,
Odd,
Spill,
}
impl PageParity {
#[must_use]
pub const fn from_depth(depth: usize) -> Self {
if depth.is_multiple_of(2) {
Self::Even
} else {
Self::Odd
}
}
#[must_use]
pub const fn opposite(self) -> Self {
match self {
Self::Even => Self::Odd,
Self::Odd => Self::Even,
Self::Spill => Self::Spill,
}
}
#[must_use]
pub const fn index(self) -> usize {
match self {
Self::Even => 0,
Self::Odd => 1,
Self::Spill => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageBand {
pub bounds: RectU16,
pub size: PageSize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageConfig {
pub min_page_size: u32,
pub max_page_size: u32,
}
impl Default for PageConfig {
fn default() -> Self {
Self {
min_page_size: DEFAULT_MIN_PAGE_SIZE,
max_page_size: DEFAULT_MAX_PAGE_SIZE,
}
}
}
#[must_use]
pub fn page_ceiling(config: &PageConfig, caps: &TierCaps) -> u32 {
config.max_page_size.min(max_texture_size(caps))
}
pub fn page_size(
bounds: RectU16,
config: &PageConfig,
caps: &TierCaps,
) -> Result<PageSize, EngineError> {
let ceiling = page_ceiling(config, caps);
let width = u32::from(bounds.width());
let height = u32::from(bounds.height());
if width > ceiling || height > ceiling {
return Err(EngineError::IntermediateTextureTooLarge);
}
let floor = config.min_page_size.min(ceiling);
let (width, height) =
frust_gpu::pool::quantize_extent(width.max(floor), height.max(floor), ceiling);
Ok(PageSize { width, height })
}
pub fn page_bands(
bounds: RectU16,
config: &PageConfig,
caps: &TierCaps,
) -> Result<Vec<PageBand>, EngineError> {
let ceiling = page_ceiling(config, caps);
let width = u32::from(bounds.width());
if u32::from(bounds.height()) > ceiling {
return Err(EngineError::IntermediateTextureTooLarge);
}
if width <= ceiling {
return Ok(vec![PageBand {
bounds,
size: page_size(bounds, config, caps)?,
}]);
}
let count = width.div_ceil(ceiling.max(1));
if count > u32::try_from(MAX_PAGE_BANDS).unwrap_or(u32::MAX) {
return Err(EngineError::IntermediateTextureTooLarge);
}
let band_width = width.div_ceil(count);
let size = page_size(
RectU16::new(
0,
0,
u16::try_from(band_width).unwrap_or(u16::MAX),
bounds.height(),
),
config,
caps,
)?;
let end = u32::from(bounds.x1);
let mut bands = Vec::with_capacity(count as usize);
let mut x0 = u32::from(bounds.x0);
while x0 < end {
let x1 = end.min(x0.saturating_add(band_width));
bands.push(PageBand {
bounds: RectU16::new(
u16::try_from(x0).unwrap_or(u16::MAX),
bounds.y0,
u16::try_from(x1).unwrap_or(u16::MAX),
bounds.y1,
),
size,
});
x0 = x1;
}
Ok(bands)
}
pub fn filter_page_size(
bounds: RectU16,
config: &PageConfig,
caps: &TierCaps,
) -> Result<PageSize, EngineError> {
let device_ceiling = max_texture_size(caps);
let budget_ceiling = config.max_page_size;
let padding = u32::from(FILTER_ATLAS_PADDING).saturating_mul(2);
let width = u32::from(bounds.width()).saturating_add(padding);
let height = u32::from(bounds.height()).saturating_add(padding);
if width > device_ceiling || height > device_ceiling {
return Err(EngineError::IntermediateTextureTooLarge);
}
if width > budget_ceiling || height > budget_ceiling {
return Err(EngineError::IntermediateTextureLimitReached);
}
let ceiling = budget_ceiling.min(device_ceiling);
let floor = config.min_page_size.min(ceiling);
let (width, height) =
frust_gpu::pool::quantize_extent(width.max(floor), height.max(floor), ceiling);
Ok(PageSize { width, height })
}
#[cfg(test)]
mod tests {
use super::*;
use frust_gpu::DownlevelProfile;
fn caps() -> TierCaps {
TierCaps::fake(DownlevelProfile::Full)
}
#[test]
fn parity_alternates_down_a_chain_and_starts_odd_at_the_outermost_layer() {
assert_eq!(PageParity::from_depth(1), PageParity::Odd);
assert_eq!(PageParity::from_depth(2), PageParity::Even);
assert_eq!(PageParity::from_depth(3), PageParity::Odd);
assert_eq!(PageParity::from_depth(4), PageParity::Even);
assert_eq!(PageParity::Even.opposite(), PageParity::Odd);
assert_eq!(PageParity::Odd.opposite(), PageParity::Even);
assert_eq!(PageParity::Even.index(), 0);
assert_eq!(PageParity::Odd.index(), 1);
}
#[test]
fn the_spill_page_is_no_depths_group_and_indexes_past_the_ping_pong_pair() {
for depth in 0..=16 {
assert_ne!(PageParity::from_depth(depth), PageParity::Spill);
}
assert_eq!(PageParity::Spill.index(), 2);
assert!(PageParity::Spill.index() < crate::schedule::MAX_LIVE_PAGES);
assert_eq!(
PageParity::Spill.opposite(),
PageParity::Spill,
"the spill page is not one of the ping-pong pair, so it has no other"
);
}
#[test]
fn a_small_layer_is_floored_at_the_minimum_page_size() {
let size = page_size(RectU16::new(0, 0, 12, 8), &PageConfig::default(), &caps())
.expect("a 12x8 layer is far inside the ceiling");
assert_eq!(
size,
PageSize {
width: DEFAULT_MIN_PAGE_SIZE,
height: DEFAULT_MIN_PAGE_SIZE
}
);
}
#[test]
fn a_larger_layer_is_quantized_up_to_the_pool_grid() {
let size = page_size(
RectU16::new(0, 0, 900, 700),
&PageConfig::default(),
&caps(),
)
.expect("a 900x700 layer is inside the ceiling");
assert_eq!(
size,
PageSize {
width: 1024,
height: 768
}
);
}
#[test]
fn a_layer_past_the_ceiling_is_refused_rather_than_shrunk() {
let config = PageConfig {
min_page_size: DEFAULT_MIN_PAGE_SIZE,
max_page_size: 1024,
};
let ceiling = page_ceiling(&config, &caps());
assert_eq!(ceiling, 1024);
assert!(matches!(
page_size(RectU16::new(0, 0, 1025, 16), &config, &caps()),
Err(EngineError::IntermediateTextureTooLarge)
));
assert!(matches!(
page_size(RectU16::new(0, 0, 16, 1025), &config, &caps()),
Err(EngineError::IntermediateTextureTooLarge)
));
assert!(page_size(RectU16::new(0, 0, 1024, 1024), &config, &caps()).is_ok());
}
#[test]
fn a_floor_above_the_ceiling_still_produces_a_page_inside_it() {
let config = PageConfig {
min_page_size: 4096,
max_page_size: 512,
};
let size = page_size(RectU16::new(0, 0, 8, 8), &config, &caps())
.expect("the floor is clamped to the ceiling, not applied over it");
assert_eq!(
size,
PageSize {
width: 512,
height: 512
}
);
}
#[test]
fn the_adapter_limit_can_only_lower_the_configured_ceiling() {
let mut caps = caps();
caps.max_texture_dimension_2d = 2048;
let config = PageConfig::default();
assert_eq!(page_ceiling(&config, &caps), 2048);
caps.max_texture_dimension_2d = 16384;
assert_eq!(page_ceiling(&config, &caps), DEFAULT_MAX_PAGE_SIZE);
}
fn assert_tiles(bands: &[PageBand], bounds: RectU16) {
let mut x = bounds.x0;
for band in bands {
assert_eq!(band.bounds.x0, x, "bands abut with no gap and no overlap");
assert!(band.bounds.x1 > band.bounds.x0, "no band is degenerate");
assert_eq!(band.bounds.y0, bounds.y0, "a band spans the full height");
assert_eq!(band.bounds.y1, bounds.y1, "a band spans the full height");
x = band.bounds.x1;
}
assert_eq!(x, bounds.x1, "the bands end exactly at the layer's edge");
}
#[test]
fn a_layer_that_fits_one_page_is_a_single_band_holding_all_of_it() {
let bounds = RectU16::new(0, 0, 900, 700);
let config = PageConfig::default();
let bands = page_bands(bounds, &config, &caps()).expect("900x700 fits one page");
assert_eq!(bands.len(), 1);
assert_eq!(bands[0].bounds, bounds);
assert_eq!(
bands[0].size,
page_size(bounds, &config, &caps()).expect("the same layer sizes as one page"),
"an unbanded layer's band is sized exactly as `page_size` sizes it"
);
assert_tiles(&bands, bounds);
}
#[test]
fn a_5k_layer_splits_into_two_equal_bands_sharing_one_page_extent() {
let bounds = RectU16::new(0, 0, 5120, 2880);
let bands =
page_bands(bounds, &PageConfig::default(), &caps()).expect("a 5K layer is banded");
assert_eq!(bands.len(), 2, "5120 needs two bands under a 4096 ceiling");
assert_tiles(&bands, bounds);
assert_eq!(bands[0].bounds, RectU16::new(0, 0, 2560, 2880));
assert_eq!(bands[1].bounds, RectU16::new(2560, 0, 5120, 2880));
assert_eq!(
bands[0].size,
PageSize {
width: 2560,
height: 3072
}
);
assert_eq!(
bands[0].size, bands[1].size,
"every band of a layer asks the pool for one extent"
);
}
#[test]
fn an_uneven_width_gives_a_narrower_last_band_at_the_same_page_extent() {
let config = PageConfig {
min_page_size: 256,
max_page_size: 1024,
};
let bounds = RectU16::new(0, 0, 2500, 600);
let bands = page_bands(bounds, &config, &caps()).expect("2500 needs three bands");
assert_eq!(bands.len(), 3);
assert_tiles(&bands, bounds);
assert_eq!(bands[0].bounds.width(), 834);
assert_eq!(bands[1].bounds.width(), 834);
assert_eq!(bands[2].bounds.width(), 832, "the last band takes the rest");
assert!(
bands
.iter()
.all(|band| band.size == bands[0].size && band.size.width <= config.max_page_size),
"the short band is still sized from the widest one, and none exceeds the ceiling"
);
}
#[test]
fn bands_are_offset_by_the_layers_own_origin_rather_than_starting_at_zero() {
let config = PageConfig {
min_page_size: 256,
max_page_size: 512,
};
let bounds = RectU16::new(100, 40, 1100, 300);
let bands = page_bands(bounds, &config, &caps()).expect("a 1000-wide layer needs two");
assert_eq!(bands.len(), 2);
assert_tiles(&bands, bounds);
assert_eq!(bands[0].bounds, RectU16::new(100, 40, 600, 300));
assert_eq!(bands[1].bounds, RectU16::new(600, 40, 1100, 300));
}
#[test]
fn a_layer_taller_than_the_ceiling_is_still_refused_because_bands_are_columns() {
let config = PageConfig {
min_page_size: 256,
max_page_size: 1024,
};
assert!(page_bands(RectU16::new(0, 0, 512, 1024), &config, &caps()).is_ok());
assert!(matches!(
page_bands(RectU16::new(0, 0, 512, 1025), &config, &caps()),
Err(EngineError::IntermediateTextureTooLarge)
));
assert!(matches!(
page_bands(RectU16::new(0, 0, 4096, 1025), &config, &caps()),
Err(EngineError::IntermediateTextureTooLarge)
));
}
#[test]
fn a_width_needing_more_than_the_band_bound_is_refused_rather_than_split_further() {
let config = PageConfig {
min_page_size: 256,
max_page_size: 1024,
};
let ceiling = page_ceiling(&config, &caps());
let at_bound = ceiling * MAX_PAGE_BANDS as u32;
let bands = page_bands(RectU16::new(0, 0, at_bound as u16, 64), &config, &caps())
.expect("exactly the bound is served");
assert_eq!(bands.len(), MAX_PAGE_BANDS);
assert!(matches!(
page_bands(
RectU16::new(0, 0, (at_bound + 1) as u16, 64),
&config,
&caps()
),
Err(EngineError::IntermediateTextureTooLarge)
));
}
#[test]
fn every_band_of_a_layer_is_a_page_the_pool_would_accept() {
let config = PageConfig::default();
let caps = caps();
let ceiling = page_ceiling(&config, &caps);
for width in [4097_u32, 5120, 6000, 8192, 12288, 32768] {
let bounds = RectU16::new(0, 0, width as u16, 2880);
let bands = page_bands(bounds, &config, &caps).expect("inside the band bound");
assert_tiles(&bands, bounds);
for band in &bands {
assert!(band.bounds.width() as u32 <= ceiling);
assert_eq!(
band.size,
page_size(
RectU16::new(0, 0, bands[0].bounds.width(), bounds.height()),
&config,
&caps
)
.expect("a band is inside the ceiling by construction")
);
}
}
}
#[test]
fn a_small_filter_layer_is_padded_then_still_floored_at_the_minimum_page_size() {
let size = filter_page_size(RectU16::new(0, 0, 12, 8), &PageConfig::default(), &caps())
.expect("a padded 24x20 request is far inside the ceiling");
assert_eq!(
size,
PageSize {
width: DEFAULT_MIN_PAGE_SIZE,
height: DEFAULT_MIN_PAGE_SIZE
}
);
}
#[test]
fn filter_padding_can_push_a_borderline_layer_into_the_next_quantum() {
let size = filter_page_size(
RectU16::new(0, 0, 1013, 700),
&PageConfig::default(),
&caps(),
)
.expect("1025x712 padded is inside the default ceiling");
assert_eq!(
size,
PageSize {
width: 1280,
height: 768
}
);
}
#[test]
fn a_filter_layer_whose_padding_carries_it_past_the_device_ceiling_is_too_large() {
let config = PageConfig {
min_page_size: DEFAULT_MIN_PAGE_SIZE,
max_page_size: 8192,
};
let caps = caps();
let device_ceiling = max_texture_size(&caps);
assert_eq!(device_ceiling, 8192);
let at_ceiling = device_ceiling - u32::from(FILTER_ATLAS_PADDING) * 2;
assert!(
filter_page_size(RectU16::new(0, 0, at_ceiling as u16, 16), &config, &caps).is_ok()
);
let over_ceiling = at_ceiling + 1;
assert!(matches!(
filter_page_size(RectU16::new(0, 0, over_ceiling as u16, 16), &config, &caps),
Err(EngineError::IntermediateTextureTooLarge)
));
assert!(matches!(
filter_page_size(RectU16::new(0, 0, 16, over_ceiling as u16), &config, &caps),
Err(EngineError::IntermediateTextureTooLarge)
));
}
#[test]
fn a_filter_layer_inside_the_device_ceiling_but_past_the_pool_budget_is_limit_reached() {
let config = PageConfig::default();
let caps = caps();
assert!(config.max_page_size < max_texture_size(&caps));
let over_budget = config.max_page_size - u32::from(FILTER_ATLAS_PADDING) * 2 + 1;
assert!(matches!(
filter_page_size(RectU16::new(0, 0, over_budget as u16, 16), &config, &caps),
Err(EngineError::IntermediateTextureLimitReached)
));
assert!(matches!(
filter_page_size(RectU16::new(0, 0, 16, over_budget as u16), &config, &caps),
Err(EngineError::IntermediateTextureLimitReached)
));
}
#[test]
fn filter_page_size_never_overflows_padding_a_bounds_near_the_u16_ceiling() {
let size = filter_page_size(
RectU16::new(0, 0, u16::MAX, u16::MAX),
&PageConfig::default(),
&caps(),
);
assert!(matches!(
size,
Err(EngineError::IntermediateTextureTooLarge)
));
}
}