///|
/// Cross-frame cache for `BeginRetainedLayer` repaint boundaries.
///
/// The mechanism is the pool the host already has. A boundary's group is painted
/// into its own offscreen pass target, `moui_metal_pass_target` keeps one target
/// per slot between frames and rebuilds it only when the size differs, and the
/// composite samples that slot by number. So when the same boundary claims the
/// same slot at the same device rect in two consecutive frames, the pixels the
/// first frame wrote are still sitting in that texture and the second frame can
/// composite it without rebuilding the group. Nothing is copied and no new host
/// object is introduced; the only host change is that a pass record with no
/// children gets no encoder, because clearing it is exactly what would destroy
/// the cached pixels.
///
/// Three guards make that reuse safe rather than lucky, and each closes a
/// different way to paint an old picture:
///
/// - `revision` must match. `content_revision` is the framework's statement that
/// the group's content changed, and this route trusts it the way the runtime's
/// own caches do — which is why a boundary whose content changed *without* a
/// revision bump keeps painting the cached pixels. That is the contract, and
/// `metal_retained_wbtest.mbt` pins it as a pixel claim rather than leaving it
/// implied, because a stale-look is the only observable difference between
/// reading a cache and re-rendering it.
/// - `slot`, `device_x`, `device_y`, `device_width`, `device_height` must all
/// match. Slots are claimed per frame in post-order, so an unchanged-looking
/// boundary can move down the list when a sibling appears or disappears; if it
/// does, the texture at its old slot belongs to whatever claimed it, and a
/// rect that moved is a different render of the same content.
/// - the entry must have been painted in the frame *immediately* before this one.
/// A gap means some other frame ran between the two, and a pass target is not
/// private to a boundary — anything that claimed that slot in the gap overwrote
/// it. Ageing entries out at the start of each frame is what turns the cache
/// into a working set, and the drops it causes are counted. A hit re-stamps the
/// entry without painting, which keeps the same rule true for a stable page (an
/// entry that aged out on its second hit would make the cache save one frame in
/// two) and is still sound, because the frame that hit also *claimed* the slot,
/// so nothing else could have painted it.
priv struct MetalRetainedEntry {
revision : Int32
slot : Int32
device_x : Int32
device_y : Int32
device_width : Int32
device_height : Int32
painted_at : Int32
}
///|
priv struct MetalRetainedCache {
mut context : UInt64
mut generation : Int32
mut entries : Map[String, MetalRetainedEntry]
// Entries dropped at the start of the frame being built, so a host can tell a
// page that holds its boundaries from one whose list keeps changing.
mut evicted : Int32
}
///|
fn new_metal_retained_cache() -> MetalRetainedCache {
MetalRetainedCache::{
context: 0UL,
generation: 0,
entries: Map([]),
evicted: 0,
}
}
///|
/// Forget every entry. Used when the surface is resized, which re-sizes the host
/// pass targets the entries point into, and by a frame that has no history to
/// consult — the CPU-only report and a single proof frame.
fn clear_metal_retained_cache(cache : MetalRetainedCache) -> Unit {
cache.entries = Map([])
cache.evicted = 0
}
///|
/// Open one frame's worth of cache. A different context handle means a different
/// Metal device, whose pool holds nothing this cache painted, so every entry goes.
/// Otherwise every entry that was not painted by the immediately preceding frame
/// is dropped: its target may have been claimed by another pass in the gap.
fn MetalRetainedCache::begin_frame(
self : MetalRetainedCache,
context : UInt64,
) -> Unit {
if self.context != context {
self.context = context
self.entries = Map([])
}
let kept : Map[String, MetalRetainedEntry] = Map([])
self.evicted = 0
for (key, entry) in self.entries {
if entry.painted_at == self.generation {
kept.set(key, entry)
} else {
self.evicted = self.evicted + 1
}
}
self.entries = kept
self.generation = self.generation + 1
}
///|
/// True when this boundary may composite the texture it already owns. The caller
/// passes the slot it is *about* to claim, so the answer is a statement about
/// this frame's layout, not about a past one.
fn metal_retained_hit(
cache : MetalRetainedCache,
key : String,
revision : Int32,
slot : Int32,
device_x : Int32,
device_y : Int32,
device_width : Int32,
device_height : Int32,
) -> Bool {
match cache.entries.get(key) {
Some(entry) =>
entry.revision == revision &&
entry.slot == slot &&
entry.device_x == device_x &&
entry.device_y == device_y &&
entry.device_width == device_width &&
entry.device_height == device_height &&
entry.painted_at == cache.generation - 1
None => false
}
}
///|
/// Record what this frame painted into the slot, so the next one can ask whether
/// it may skip the work. The return says whether this key previously held a
/// *different* revision, which is the `update` case: the group was repainted
/// because the framework said its content changed, not because the layout moved.
fn metal_retained_store(
cache : MetalRetainedCache,
key : String,
revision : Int32,
slot : Int32,
device_x : Int32,
device_y : Int32,
device_width : Int32,
device_height : Int32,
) -> Bool {
let updated = match cache.entries.get(key) {
Some(entry) => entry.revision != revision
None => false
}
cache.entries.set(key, MetalRetainedEntry::{
revision,
slot,
device_x,
device_y,
device_width,
device_height,
painted_at: cache.generation,
})
updated
}