use std::collections::HashMap;
use vello_common::encode::{EncodedGradient, GradientCacheKey, MAX_GRADIENT_LUT_SIZE};
use vello_common::fearless_simd::{Level, Simd, dispatch};
use vello_common::peniko::color::cache_key::CacheKey;
pub const BYTES_PER_TEXEL: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GradientTextureLayout {
pub width: u32,
pub height: u32,
}
impl GradientTextureLayout {
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub fn square(dim: u32) -> Self {
Self {
width: dim,
height: dim,
}
}
pub fn bytes_per_row(self) -> u32 {
self.width << 2
}
pub fn byte_capacity(self) -> usize {
self.width as usize * self.height as usize * BYTES_PER_TEXEL as usize
}
pub fn worst_case_entry_capacity(self) -> u32 {
let texels = self.width.saturating_mul(self.height);
texels / MAX_GRADIENT_LUT_SIZE as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CachedRamp {
pub lut_start: u32,
pub width: u32,
}
#[derive(Debug, Clone, Copy)]
struct CacheEntry {
ramp: CachedRamp,
last_used: u64,
}
#[derive(Debug, Default)]
struct ScratchSpace {
epochs: Vec<u64>,
removed: Vec<CachedRamp>,
prefix_sum: Vec<u32>,
}
#[derive(Debug)]
pub struct GradientCache {
epoch: u64,
entries: HashMap<CacheKey<GradientCacheKey>, CacheEntry>,
luts: Vec<u8>,
has_changed: bool,
capacity: u32,
level: Level,
scratch: ScratchSpace,
}
impl GradientCache {
pub fn new(capacity: u32, level: Level) -> Self {
Self {
epoch: 0,
entries: HashMap::new(),
luts: Vec::new(),
has_changed: false,
capacity,
level,
scratch: ScratchSpace::default(),
}
}
pub fn for_texture(layout: GradientTextureLayout, level: Level) -> Self {
Self::new(layout.worst_case_entry_capacity(), level)
}
pub fn capacity(&self) -> u32 {
self.capacity
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn luts_size(&self) -> usize {
self.luts.len()
}
pub fn luts(&self) -> &[u8] {
&self.luts
}
pub fn is_empty(&self) -> bool {
self.luts.is_empty()
}
pub fn has_changed(&self) -> bool {
self.has_changed
}
pub fn mark_synced(&mut self) {
self.has_changed = false;
}
pub fn lookup(&self, gradient: &EncodedGradient) -> Option<CachedRamp> {
self.entries
.get(&gradient.cache_key)
.map(|entry| entry.ramp)
}
pub fn get_or_create_ramp(&mut self, gradient: &EncodedGradient) -> CachedRamp {
self.epoch += 1;
if let Some(entry) = self.entries.get_mut(&gradient.cache_key) {
entry.last_used = self.epoch;
return entry.ramp;
}
let lut_start = u32::try_from(self.luts.len()).unwrap_or(u32::MAX) / BYTES_PER_TEXEL;
let width = dispatch!(self.level, simd => bake_ramp(simd, gradient, &mut self.luts));
let ramp = CachedRamp {
lut_start,
width: u32::try_from(width).unwrap_or(u32::MAX),
};
self.has_changed = true;
self.entries.insert(
gradient.cache_key.clone(),
CacheEntry {
ramp,
last_used: self.epoch,
},
);
ramp
}
pub fn maintain(&mut self) {
let excess = self.entries.len().saturating_sub(self.capacity as usize);
self.evict(excess);
}
pub fn take_luts(&mut self) -> Vec<u8> {
std::mem::take(&mut self.luts)
}
pub fn restore_luts(&mut self, luts: Vec<u8>) {
self.luts = luts;
}
pub fn begin_upload(&mut self, layout: GradientTextureLayout) -> Option<LutUpload<'_>> {
if self.luts.is_empty() {
return None;
}
let logical_len = self.luts.len();
if layout.byte_capacity() < logical_len {
log::warn!(
"gradient LUT upload skipped: {logical_len} packed bytes do not fit a \
{}x{} texture ({} bytes)",
layout.width,
layout.height,
layout.byte_capacity(),
);
return None;
}
let mut bytes = self.take_luts();
bytes.resize(layout.byte_capacity(), 0);
Some(LutUpload {
cache: self,
bytes,
logical_len,
layout,
})
}
fn evict(&mut self, count: usize) {
if count == 0 || self.entries.is_empty() {
return;
}
let mut epochs = std::mem::take(&mut self.scratch.epochs);
epochs.clear();
epochs.extend(self.entries.values().map(|entry| entry.last_used));
let (_, &mut threshold, _) = epochs.select_nth_unstable(count - 1);
self.scratch.epochs = epochs;
let mut removed = std::mem::take(&mut self.scratch.removed);
removed.clear();
self.entries.retain(|_, entry| {
if entry.last_used <= threshold {
removed.push(entry.ramp);
false
} else {
true
}
});
removed.sort_unstable_by_key(|ramp| ramp.lut_start);
let mut prefix_sum = std::mem::take(&mut self.scratch.prefix_sum);
self.compact_luts(&removed, &mut prefix_sum);
self.scratch.removed = removed;
self.scratch.prefix_sum = prefix_sum;
self.has_changed = true;
}
fn compact_luts(&mut self, removed: &[CachedRamp], prefix_sum: &mut Vec<u32>) {
if removed.is_empty() {
return;
}
prefix_sum.clear();
prefix_sum.push(0);
let mut write_pos = 0;
let mut read_pos = 0;
for ramp in removed {
let remove_start = (ramp.lut_start * BYTES_PER_TEXEL) as usize;
let remove_end = remove_start + (ramp.width * BYTES_PER_TEXEL) as usize;
if read_pos < remove_start {
self.luts.copy_within(read_pos..remove_start, write_pos);
write_pos += remove_start - read_pos;
}
read_pos = remove_end;
prefix_sum.push(prefix_sum.last().copied().unwrap_or(0) + ramp.width);
}
let luts_len = self.luts.len();
if read_pos < luts_len {
self.luts.copy_within(read_pos..luts_len, write_pos);
write_pos += luts_len - read_pos;
}
self.luts.truncate(write_pos);
for entry in self.entries.values_mut() {
let pos = removed.partition_point(|ramp| ramp.lut_start < entry.ramp.lut_start);
entry.ramp.lut_start -= prefix_sum[pos];
}
}
}
#[derive(Debug)]
pub struct LutUpload<'a> {
cache: &'a mut GradientCache,
bytes: Vec<u8>,
logical_len: usize,
layout: GradientTextureLayout,
}
impl LutUpload<'_> {
pub fn layout(&self) -> GradientTextureLayout {
self.layout
}
pub fn bytes_per_row(&self) -> u32 {
self.layout.bytes_per_row()
}
pub fn logical_len(&self) -> usize {
self.logical_len
}
}
impl std::ops::Deref for LutUpload<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.bytes
}
}
impl Drop for LutUpload<'_> {
fn drop(&mut self) {
let mut bytes = std::mem::take(&mut self.bytes);
bytes.truncate(self.logical_len);
self.cache.restore_luts(bytes);
}
}
#[inline(always)]
fn bake_ramp<S: Simd>(simd: S, gradient: &EncodedGradient, output: &mut Vec<u8>) -> usize {
let lut = gradient.u8_lut(simd);
let bytes: &[u8] = bytemuck::cast_slice(lut.lut());
output.extend_from_slice(bytes);
lut.width()
}
#[cfg(test)]
mod tests {
use super::*;
fn packed(len: usize) -> GradientCache {
let mut cache = GradientCache::new(8, Level::baseline());
cache.restore_luts(vec![0xAB; len]);
cache
}
#[test]
fn an_upload_into_a_texture_smaller_than_the_packed_ramps_is_refused() {
let layout = GradientTextureLayout::square(4);
let mut cache = packed(layout.byte_capacity() + 4);
assert!(
cache.begin_upload(layout).is_none(),
"a too-small layout must refuse rather than truncate"
);
assert_eq!(
cache.luts_size(),
layout.byte_capacity() + 4,
"the refusal leaves every packed ramp byte in place"
);
}
#[test]
fn an_upload_exactly_filling_the_texture_is_served() {
let layout = GradientTextureLayout::square(4);
let mut cache = packed(layout.byte_capacity());
let upload = cache.begin_upload(layout).expect("an exact fit is served");
assert_eq!(upload.len(), layout.byte_capacity());
assert_eq!(upload.logical_len(), layout.byte_capacity());
}
}