use rustc_hash::FxHashMap;
use valo_dl::DisplayList;
use valo_geometry::Rect;
use crate::report::PoolReport;
pub const MIN_CACHED_DRAWS: u32 = 16;
const SCALE_SERVE_BAND: std::ops::RangeInclusive<f32> = 0.5..=1.02;
pub enum RasterVerdict {
Inline,
Quad(QuadSource),
Fill(FillTarget),
}
pub struct QuadSource {
pub view: wgpu::TextureView,
pub size: [u32; 2],
pub content_scale: f32,
pub content_bounds: Rect,
}
pub struct FillTarget {
pub view: wgpu::TextureView,
pub texture: wgpu::Texture,
pub size: [u32; 2],
pub content_scale: f32,
pub content_bounds: Rect,
}
impl FillTarget {
pub fn quad_source(&self) -> QuadSource {
QuadSource {
view: self.view.clone(),
size: self.size,
content_scale: self.content_scale,
content_bounds: self.content_bounds,
}
}
}
struct Entry {
texture: wgpu::Texture,
view: wgpu::TextureView,
content_scale: f32,
content_bounds: Rect,
bytes: u64,
last_used: u64,
}
pub struct ListRasterCache {
entries: FxHashMap<u64, Entry>,
hold: bool,
frame: u64,
}
impl ListRasterCache {
pub fn new() -> Self {
Self {
entries: FxHashMap::default(),
hold: false,
frame: 0,
}
}
pub fn set_hold(&mut self, held: bool) {
self.hold = held;
}
pub fn resolve(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
list: &DisplayList,
needed_scale: f32,
max_dimension: u32,
) -> RasterVerdict {
let Some(bounds) = cacheable_bounds(list) else {
return RasterVerdict::Inline;
};
if let Some(entry) = self.entries.get_mut(&list.id()) {
if self.hold || scale_serves(entry.content_scale, needed_scale) {
entry.last_used = self.frame;
return RasterVerdict::Quad(quad_source(entry));
}
}
let Some(size) = raster_size(&bounds, needed_scale, max_dimension) else {
return RasterVerdict::Inline; };
let entry = create_entry(device, format, size, needed_scale, bounds, self.frame);
let fill = fill_target(&entry, size);
self.entries.insert(list.id(), entry);
RasterVerdict::Fill(fill)
}
pub fn end_frame(&mut self) {
let current = self.frame;
self.frame += 1;
self.entries.retain(|_, entry| entry.last_used >= current);
}
pub fn report(&self) -> PoolReport {
PoolReport {
count: self.entries.len() as u32,
bytes: self.entries.values().map(|e| e.bytes).sum(),
}
}
}
fn scale_serves(entry_scale: f32, needed_scale: f32) -> bool {
SCALE_SERVE_BAND.contains(&(needed_scale / entry_scale.max(1e-6)))
}
fn cacheable_bounds(list: &DisplayList) -> Option<Rect> {
if list.draw_count() < MIN_CACHED_DRAWS {
return None;
}
if list.backdrop_group_count() > 0 {
return None;
}
let bounds = list.bounds()?;
(bounds.width > 0.0 && bounds.height > 0.0 && bounds.width.is_finite()).then_some(bounds)
}
fn raster_size(bounds: &Rect, scale: f32, max_dimension: u32) -> Option<[u32; 2]> {
let w = (bounds.width * scale).ceil() as u32;
let h = (bounds.height * scale).ceil() as u32;
(w > 0 && h > 0 && w <= max_dimension && h <= max_dimension).then_some([w, h])
}
fn quad_source(entry: &Entry) -> QuadSource {
QuadSource {
view: entry.view.clone(),
size: [entry.texture.width(), entry.texture.height()],
content_scale: entry.content_scale,
content_bounds: entry.content_bounds,
}
}
fn fill_target(entry: &Entry, size: [u32; 2]) -> FillTarget {
FillTarget {
view: entry.view.clone(),
texture: entry.texture.clone(),
size,
content_scale: entry.content_scale,
content_bounds: entry.content_bounds,
}
}
fn create_entry(
device: &wgpu::Device,
format: wgpu::TextureFormat,
size: [u32; 2],
content_scale: f32,
content_bounds: Rect,
frame: u64,
) -> Entry {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("valo raster cache"),
size: wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&Default::default());
let bytes = size[0] as u64 * size[1] as u64 * 4;
Entry {
texture,
view,
content_scale,
content_bounds,
bytes,
last_used: frame,
}
}