use frust_gpu::{TextureAllocator, TierCaps};
use vello_common::geometry::RectU16;
use vello_common::record::RecordedLayer;
use vello_common::tile::Tile;
use crate::cache::images::{AtlasTier, choose_tier, device_memory_bytes};
use crate::compile::{RetainedBracket, RetainedKind, RetainedPlacement};
use crate::config;
use crate::error::EngineError;
use crate::gpu::targets::{MAX_INTERMEDIATE_DIMENSION, max_texture_size, retained_page_descriptor};
pub const RETAINED_BYTES_PER_TEXEL: u64 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetainedBudget {
pub bytes: u64,
}
impl RetainedBudget {
pub const MOBILE: Self = Self { bytes: 32 << 20 };
pub const MOBILE_LARGE: Self = Self { bytes: 64 << 20 };
pub const DESKTOP: Self = Self { bytes: 256 << 20 };
#[must_use]
pub fn for_tier(tier: AtlasTier) -> Self {
match tier {
AtlasTier::Mobile => Self::MOBILE,
AtlasTier::MobileLarge => Self::MOBILE_LARGE,
AtlasTier::Desktop => Self::DESKTOP,
}
}
#[must_use]
pub fn for_caps(caps: &TierCaps) -> Self {
match config::retained_budget_mb() {
Some(mib) => Self {
bytes: mib.saturating_mul(1 << 20),
},
None => Self::for_tier(choose_tier(caps, device_memory_bytes()).tier),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetainedRefusal {
NotRetainable,
Empty,
Overflow,
TooLarge,
OverBudget,
Duplicate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetainedDecision {
Inline(RetainedRefusal),
Record {
bounds: RectU16,
},
Draw {
dest: RectU16,
origin: (u16, u16),
},
Hidden,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RetainedPlan {
pub decisions: Vec<RetainedDecision>,
pub evictions: Vec<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RetainedEntry {
pub key: u64,
pub extent_px: (u32, u32),
pub scale: f32,
}
#[derive(Debug)]
struct RetainedPage<T, V> {
key: u64,
bounds: RectU16,
placement: RetainedPlacement,
extent_px: (u32, u32),
size: (u32, u32),
last_used: u64,
_texture: T,
view: V,
}
impl<T, V> RetainedPage<T, V> {
fn bytes(&self) -> u64 {
page_bytes(self.size)
}
fn entry(&self) -> RetainedEntry {
RetainedEntry {
key: self.key,
extent_px: self.extent_px,
scale: self.placement.scale(),
}
}
}
#[derive(Debug)]
pub struct RetainedPages<T = wgpu::Texture, V = wgpu::TextureView> {
pages: Vec<RetainedPage<T, V>>,
budget: RetainedBudget,
max_dimension: u32,
frame: u64,
generation: u64,
bound: Vec<Option<usize>>,
refused: bool,
}
impl<T, V> RetainedPages<T, V> {
#[must_use]
pub fn new(budget: RetainedBudget, max_dimension: u32) -> Self {
Self {
pages: Vec::new(),
budget,
max_dimension: max_dimension.min(MAX_INTERMEDIATE_DIMENSION),
frame: 0,
generation: 0,
bound: Vec::new(),
refused: false,
}
}
#[must_use]
pub fn for_caps(caps: &TierCaps) -> Self {
Self::new(RetainedBudget::for_caps(caps), max_texture_size(caps))
}
#[must_use]
pub fn budget(&self) -> RetainedBudget {
self.budget
}
pub fn set_budget(&mut self, budget: RetainedBudget) {
self.budget = budget;
let mut order: Vec<(u64, u64)> = self
.pages
.iter()
.map(|page| (page.last_used, page.key))
.collect();
order.sort_unstable();
for (_, key) in order {
if self.used_bytes() <= budget.bytes {
break;
}
self.remove(key);
}
}
#[must_use]
pub fn max_dimension(&self) -> u32 {
self.max_dimension
}
#[must_use]
pub fn len(&self) -> usize {
self.pages.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pages.is_empty()
}
#[must_use]
pub fn used_bytes(&self) -> u64 {
self.pages.iter().map(RetainedPage::bytes).sum()
}
#[must_use]
pub fn contains(&self, key: u64) -> bool {
self.position(key).is_some()
}
#[must_use]
pub fn frame(&self) -> u64 {
self.frame
}
#[must_use]
pub fn generation(&self) -> u64 {
self.generation
}
pub fn entries(&self) -> impl Iterator<Item = RetainedEntry> + '_ {
self.pages.iter().map(RetainedPage::entry)
}
pub fn take_refusal(&mut self) -> bool {
std::mem::take(&mut self.refused)
}
pub fn clear(&mut self) {
if !self.pages.is_empty() {
self.generation = self.generation.wrapping_add(1);
}
self.pages.clear();
self.bound.clear();
}
#[must_use]
pub fn page_for(&self, bracket: u32) -> Option<(&V, (u32, u32))> {
let index = (*self.bound.get(bracket as usize)?)?;
let page = self.pages.get(index)?;
Some((&page.view, page.size))
}
pub fn plan(
&mut self,
brackets: &[RetainedBracket],
layers: &mut [RecordedLayer],
) -> Result<RetainedPlan, EngineError> {
self.frame = self.frame.saturating_add(1);
if brackets.is_empty() {
return Ok(RetainedPlan::default());
}
let mut decisions: Vec<Option<RetainedDecision>> = vec![None; brackets.len()];
for (slot, bracket) in decisions.iter_mut().zip(brackets) {
if let RetainedKind::Draw { window, masked, .. } = bracket.kind {
match self.resolve_draw(bracket, window, masked) {
Some(decision) => *slot = Some(decision),
None => {
self.clear();
self.refused = true;
return Err(EngineError::RetainedKeyMissing { key: bracket.key });
}
}
}
}
grow_enclosing_layers(brackets, &decisions, layers);
let named: Vec<u64> = brackets.iter().map(|bracket| bracket.key).collect();
let drawn: Vec<u64> = brackets
.iter()
.filter(|bracket| bracket.is_draw())
.map(|bracket| bracket.key)
.collect();
let mut candidates: Vec<(u64, u64, u64)> = self
.pages
.iter()
.filter(|page| !named.contains(&page.key))
.map(|page| (page.last_used, page.key, page.bytes()))
.collect();
candidates.sort_unstable();
let mut evicted = 0_usize;
let mut projected = self.used_bytes();
let mut recorded: Vec<u64> = drawn;
for (index, bracket) in brackets.iter().enumerate() {
let RetainedKind::Record { rect, retainable } = bracket.kind else {
continue;
};
let decision = self.admit(
bracket,
rect,
retainable,
layers,
&recorded,
&mut projected,
&candidates,
&mut evicted,
);
if matches!(decision, RetainedDecision::Record { .. }) {
recorded.push(bracket.key);
}
if let Some(slot) = decisions.get_mut(index) {
*slot = Some(decision);
}
}
Ok(RetainedPlan {
decisions: decisions
.into_iter()
.map(|decision| {
decision.unwrap_or(RetainedDecision::Inline(RetainedRefusal::NotRetainable))
})
.collect(),
evictions: candidates
.iter()
.take(evicted)
.map(|&(_, key, _)| key)
.collect(),
})
}
pub fn commit<A>(
&mut self,
allocator: &A,
brackets: &[RetainedBracket],
superseded: &[u64],
plan: &RetainedPlan,
rendered: &[bool],
) where
A: TextureAllocator<Texture = T, View = V>,
{
let mut changed = false;
for key in &plan.evictions {
changed |= self.remove(*key);
}
for key in stale_keys(brackets, superseded, plan, rendered) {
changed |= self.remove(key);
}
let frame = self.frame;
for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
match *decision {
RetainedDecision::Record { bounds } => {
if !rendered.get(index).copied().unwrap_or(false) {
continue;
}
let size = (u32::from(bounds.width()), u32::from(bounds.height()));
match self.position(bracket.key) {
Some(at) if fits(self.pages[at].size, size) => {
let page = &mut self.pages[at];
let before = page.entry();
page.bounds = bounds;
page.placement = bracket.placement;
page.extent_px = bracket.extent_px;
page.last_used = frame;
changed |= page.entry() != before;
}
existing => {
if let Some(at) = existing {
self.pages.swap_remove(at);
}
let (texture, view) = allocator
.allocate_texture(&retained_page_descriptor(size.0, size.1));
self.pages.push(RetainedPage {
key: bracket.key,
bounds,
placement: bracket.placement,
extent_px: bracket.extent_px,
size,
last_used: frame,
_texture: texture,
view,
});
changed = true;
}
}
}
RetainedDecision::Draw { .. } | RetainedDecision::Hidden => {
if let Some(at) = self.position(bracket.key) {
self.pages[at].last_used = frame;
}
}
RetainedDecision::Inline(_) => {}
}
}
self.bound.clear();
for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
let uses_page = match decision {
RetainedDecision::Record { .. } => rendered.get(index).copied().unwrap_or(false),
RetainedDecision::Draw { .. } => true,
RetainedDecision::Hidden | RetainedDecision::Inline(_) => false,
};
self.bound
.push(uses_page.then(|| self.position(bracket.key)).flatten());
}
if changed {
self.generation = self.generation.wrapping_add(1);
}
}
fn resolve_draw(
&self,
bracket: &RetainedBracket,
window: RectU16,
masked: bool,
) -> Option<RetainedDecision> {
if masked {
return None;
}
let page = self.pages.get(self.position(bracket.key)?)?;
if page.extent_px != bracket.extent_px {
return None;
}
let (dx, dy) = page.placement.offset_to(&bracket.placement)?;
let moved = (
i64::from(page.bounds.x0) + i64::from(dx),
i64::from(page.bounds.y0) + i64::from(dy),
i64::from(page.bounds.x1) + i64::from(dx),
i64::from(page.bounds.y1) + i64::from(dy),
);
let x0 = moved.0.max(i64::from(window.x0));
let y0 = moved.1.max(i64::from(window.y0));
let x1 = moved.2.min(i64::from(window.x1));
let y1 = moved.3.min(i64::from(window.y1));
if x0 >= x1 || y0 >= y1 {
return Some(RetainedDecision::Hidden);
}
let grid = |value: i64| u16::try_from(value).ok();
Some(RetainedDecision::Draw {
dest: RectU16::new(grid(x0)?, grid(y0)?, grid(x1)?, grid(y1)?),
origin: (grid(x0 - moved.0)?, grid(y0 - moved.1)?),
})
}
#[expect(
clippy::too_many_arguments,
reason = "the frame-wide running state of `plan`'s admission loop, \
threaded rather than bundled so each piece stays a plain local there"
)]
fn admit(
&self,
bracket: &RetainedBracket,
rect: RectU16,
retainable: bool,
layers: &[RecordedLayer],
recorded: &[u64],
projected: &mut u64,
candidates: &[(u64, u64, u64)],
evicted: &mut usize,
) -> RetainedDecision {
let refuse = RetainedDecision::Inline;
if !retainable {
return refuse(RetainedRefusal::NotRetainable);
}
if recorded.contains(&bracket.key) {
return refuse(RetainedRefusal::Duplicate);
}
let Some(layer) = layers.get(bracket.layer as usize) else {
return refuse(RetainedRefusal::NotRetainable);
};
let bounds = layer.bbox;
if bounds.is_empty() {
return refuse(RetainedRefusal::Empty);
}
let slack = rect_grown_by_a_tile(rect);
if !(bounds.x0 >= slack.x0
&& bounds.y0 >= slack.y0
&& bounds.x1 <= slack.x1
&& bounds.y1 <= slack.y1)
{
return refuse(RetainedRefusal::Overflow);
}
let size = (u32::from(bounds.width()), u32::from(bounds.height()));
if size.0 > self.max_dimension || size.1 > self.max_dimension {
return refuse(RetainedRefusal::TooLarge);
}
let need = page_bytes(size);
if need > self.budget.bytes {
return refuse(RetainedRefusal::OverBudget);
}
let current = self.position(bracket.key).and_then(|at| self.pages.get(at));
let after = match current {
Some(page) if fits(page.size, size) => *projected,
Some(page) => projected.saturating_sub(page.bytes()).saturating_add(need),
None => projected.saturating_add(need),
};
let mut freed_to = after;
let mut cursor = *evicted;
while freed_to > self.budget.bytes {
let Some(&(_, _, bytes)) = candidates.get(cursor) else {
return refuse(RetainedRefusal::OverBudget);
};
freed_to = freed_to.saturating_sub(bytes);
cursor += 1;
}
*evicted = cursor;
*projected = freed_to;
RetainedDecision::Record { bounds }
}
fn position(&self, key: u64) -> Option<usize> {
self.pages.iter().position(|page| page.key == key)
}
fn remove(&mut self, key: u64) -> bool {
match self.position(key) {
Some(at) => {
self.pages.swap_remove(at);
self.generation = self.generation.wrapping_add(1);
true
}
None => false,
}
}
}
fn stale_keys(
brackets: &[RetainedBracket],
superseded: &[u64],
plan: &RetainedPlan,
rendered: &[bool],
) -> Vec<u64> {
let mut kept: Vec<u64> = Vec::new();
let mut stale: Vec<u64> = Vec::new();
for (index, (bracket, decision)) in brackets.iter().zip(&plan.decisions).enumerate() {
let keeps = match decision {
RetainedDecision::Draw { .. } | RetainedDecision::Hidden => true,
RetainedDecision::Record { .. } => rendered.get(index).copied().unwrap_or(false),
RetainedDecision::Inline(_) => false,
};
if keeps {
kept.push(bracket.key);
} else {
stale.push(bracket.key);
}
}
stale.extend_from_slice(superseded);
let mut keys: Vec<u64> = Vec::new();
for key in stale {
if !kept.contains(&key) && !keys.contains(&key) {
keys.push(key);
}
}
keys
}
fn grow_enclosing_layers(
brackets: &[RetainedBracket],
decisions: &[Option<RetainedDecision>],
layers: &mut [RecordedLayer],
) {
let mut parents: Option<Vec<Option<usize>>> = None;
for (bracket, decision) in brackets.iter().zip(decisions) {
let Some(RetainedDecision::Draw { dest, .. }) = decision else {
continue;
};
let parents = parents.get_or_insert_with(|| parent_layers(layers));
let grown = tile_bounds_of(*dest);
let mut at = parents.get(bracket.layer as usize).copied().flatten();
let mut steps = 0_usize;
while let Some(index) = at {
steps += 1;
if steps > layers.len() {
break;
}
if let Some(layer) = layers.get_mut(index) {
if layer.bbox.is_empty() {
layer.bbox = grown;
} else {
layer.bbox.union(grown);
}
}
at = parents.get(index).copied().flatten();
}
}
}
fn parent_layers(layers: &[RecordedLayer]) -> Vec<Option<usize>> {
let mut parents = vec![None; layers.len()];
for (index, layer) in layers.iter().enumerate() {
for node in &layer.nodes {
if let Some(child) = node.layer
&& let Some(slot) = parents.get_mut(child as usize)
{
*slot = Some(index);
}
}
}
parents
}
fn tile_bounds_of(rect: RectU16) -> RectU16 {
let down = |value: u16, step: u16| value - value % step;
let up = |value: u16, step: u16| match value % step {
0 => value,
rest => value.saturating_add(step - rest),
};
RectU16::new(
down(rect.x0, Tile::WIDTH),
down(rect.y0, Tile::HEIGHT),
up(rect.x1, Tile::WIDTH),
up(rect.y1, Tile::HEIGHT),
)
}
fn rect_grown_by_a_tile(rect: RectU16) -> RectU16 {
RectU16::new(
rect.x0.saturating_sub(Tile::WIDTH),
rect.y0.saturating_sub(Tile::HEIGHT),
rect.x1.saturating_add(Tile::WIDTH),
rect.y1.saturating_add(Tile::HEIGHT),
)
}
fn page_bytes(size: (u32, u32)) -> u64 {
u64::from(size.0)
.saturating_mul(u64::from(size.1))
.saturating_mul(RETAINED_BYTES_PER_TEXEL)
}
fn fits(have: (u32, u32), need: (u32, u32)) -> bool {
have.0 >= need.0 && have.1 >= need.1
}