#pragma once
#include "page_table.h"
#include <cstdint>
#include <vector>
// ── Tile identity ─────────────────────────────────────────────────────────────
struct TileId {
int texture_id = -1;
int tile_x = 0;
int tile_y = 0;
int mip = 0;
bool operator==(const TileId &o) const {
return texture_id == o.texture_id && tile_x == o.tile_x &&
tile_y == o.tile_y && mip == o.mip;
}
bool empty() const { return texture_id < 0; }
};
// ── Cache slot ────────────────────────────────────────────────────────────────
struct CacheSlot {
TileId tile;
uint64_t frame_last_used = 0;
bool locked = false; // Pinned slots (root mips) are never evicted
bool occupied = false;
};
// ── Allocation result ─────────────────────────────────────────────────────────
struct AllocResult {
bool success = false;
PhysicalLocation location;
TileId evicted_tile; // If a tile was evicted to make room
bool was_eviction = false;
};
// ── Physical tile cache with LRU eviction ─────────────────────────────────────
// Models UE5's TexturePagePool: a grid of physical tile slots in an atlas.
// Each slot maps to a position in the GPU atlas texture.
class TileCache {
public:
TileCache() = default;
// Initialize cache with grid dimensions (e.g., 32×32 = 1024 slots).
// Each slot represents one tile_size × tile_size region in the GPU atlas.
void init(int grid_w, int grid_h) {
_grid_w = grid_w;
_grid_h = grid_h;
_slots.resize(grid_w * grid_h);
}
int grid_w() const { return _grid_w; }
int grid_h() const { return _grid_h; }
int total_slots() const { return _grid_w * _grid_h; }
int occupied_count() const {
int count = 0;
for (const auto &s : _slots) {
if (s.occupied)
count++;
}
return count;
}
int locked_count() const {
int count = 0;
for (const auto &s : _slots) {
if (s.locked)
count++;
}
return count;
}
// ── Allocate a slot ─────────────────────────────────────────────────
// Allocate a cache slot for a tile. If the cache is full, evicts the
// least-recently-used unlocked slot.
AllocResult alloc(const TileId &tile, uint64_t frame) {
// First check if this tile is already cached
for (int i = 0; i < (int)_slots.size(); i++) {
if (_slots[i].occupied && _slots[i].tile == tile) {
_slots[i].frame_last_used = frame;
return { true, _index_to_location(i), {}, false };
}
}
// Find a free slot
for (int i = 0; i < (int)_slots.size(); i++) {
if (!_slots[i].occupied) {
_slots[i].tile = tile;
_slots[i].frame_last_used = frame;
_slots[i].locked = false;
_slots[i].occupied = true;
return { true, _index_to_location(i), {}, false };
}
}
// Full — evict LRU (oldest frame_last_used, unlocked)
int lru_idx = -1;
uint64_t lru_frame = UINT64_MAX;
for (int i = 0; i < (int)_slots.size(); i++) {
if (_slots[i].occupied && !_slots[i].locked &&
_slots[i].frame_last_used < lru_frame) {
lru_frame = _slots[i].frame_last_used;
lru_idx = i;
}
}
if (lru_idx < 0) {
// All slots locked — cannot allocate
return { false, {}, {}, false };
}
TileId evicted = _slots[lru_idx].tile;
_slots[lru_idx].tile = tile;
_slots[lru_idx].frame_last_used = frame;
_slots[lru_idx].locked = false;
_slots[lru_idx].occupied = true;
return { true, _index_to_location(lru_idx), evicted, true };
}
// ── Touch (update LRU timestamp) ────────────────────────────────────
void touch(PhysicalLocation loc, uint64_t frame) {
int idx = _location_to_index(loc);
if (idx >= 0 && _slots[idx].occupied) {
_slots[idx].frame_last_used = frame;
}
}
// ── Lock / Unlock (pinned root mips) ────────────────────────────────
void lock(PhysicalLocation loc) {
int idx = _location_to_index(loc);
if (idx >= 0 && _slots[idx].occupied) {
_slots[idx].locked = true;
}
}
void unlock(PhysicalLocation loc) {
int idx = _location_to_index(loc);
if (idx >= 0) {
_slots[idx].locked = false;
}
}
bool is_locked(PhysicalLocation loc) const {
int idx = _location_to_index(loc);
if (idx >= 0 && _slots[idx].occupied) {
return _slots[idx].locked;
}
return false;
}
// ── Lookup ──────────────────────────────────────────────────────────
// Find the cache slot for a given tile. Returns location if found.
AllocResult find(const TileId &tile) const {
for (int i = 0; i < (int)_slots.size(); i++) {
if (_slots[i].occupied && _slots[i].tile == tile) {
return { true, _index_to_location(i), {}, false };
}
}
return { false, {}, {}, false };
}
// ── Free a specific tile ────────────────────────────────────────────
void free(const TileId &tile) {
for (auto &s : _slots) {
if (s.occupied && s.tile == tile) {
s.occupied = false;
s.locked = false;
s.tile = {};
return;
}
}
}
void free_at(PhysicalLocation loc) {
int idx = _location_to_index(loc);
if (idx >= 0) {
_slots[idx].occupied = false;
_slots[idx].locked = false;
_slots[idx].tile = {};
}
}
// ── Clear all ───────────────────────────────────────────────────────
void clear() {
for (auto &s : _slots) {
s = {};
}
}
// ── Slot access (for debugging/stats) ───────────────────────────────
const CacheSlot &slot_at(PhysicalLocation loc) const {
static CacheSlot empty;
int idx = _location_to_index(loc);
if (idx >= 0)
return _slots[idx];
return empty;
}
private:
int _grid_w = 0;
int _grid_h = 0;
std::vector<CacheSlot> _slots;
PhysicalLocation _index_to_location(int idx) const {
return { (uint8_t)(idx % _grid_w), (uint8_t)(idx / _grid_w) };
}
int _location_to_index(PhysicalLocation loc) const {
int idx = loc.y * _grid_w + loc.x;
if (idx >= 0 && idx < (int)_slots.size())
return idx;
return -1;
}
};