#pragma once
#include <cstdint>
#include <cstring>
#include <vector>
// ── Physical tile location in the cache atlas ─────────────────────────────────
// Packed as uint16: x in low byte, y in high byte (matches R16G16_UINT GPU format)
struct PhysicalLocation {
uint8_t x = 0;
uint8_t y = 0;
};
struct PageLookup {
bool valid = false;
PhysicalLocation location;
};
// ── Virtual page table ────────────────────────────────────────────────────────
// Multi-mip page table mapping virtual tile coordinates to physical cache locations.
// Mip 0 is the highest detail level with the most tiles.
// Each mip level halves in each dimension: mip N has (base_size >> N)^2 entries.
class PageTable {
public:
PageTable() = default;
// Initialize with number of mip levels. Mip 0 = base_tiles × base_tiles.
void init(int mip_levels, int base_tiles = 128) {
_mip_levels = mip_levels;
_base_tiles = base_tiles;
_entries.resize(mip_levels);
_valid.resize(mip_levels);
for (int m = 0; m < mip_levels; m++) {
int size = tiles_at_mip(m);
_entries[m].resize(size * size);
_valid[m].resize(size * size, false);
memset(_entries[m].data(), 0, _entries[m].size() * sizeof(PhysicalLocation));
}
}
int mip_levels() const { return _mip_levels; }
int base_tiles() const { return _base_tiles; }
int tiles_at_mip(int mip) const {
int s = _base_tiles >> mip;
return s > 0 ? s : 1;
}
// ── Map / Evict / Lookup ────────────────────────────────────────────
void map(int mip, int x, int y, PhysicalLocation loc) {
int idx = _index(mip, x, y);
if (idx < 0)
return;
_entries[mip][idx] = loc;
_valid[mip][idx] = true;
}
void evict(int mip, int x, int y) {
int idx = _index(mip, x, y);
if (idx < 0)
return;
_valid[mip][idx] = false;
_entries[mip][idx] = {};
}
PageLookup lookup(int mip, int x, int y) const {
int idx = _index(mip, x, y);
if (idx < 0)
return {};
if (_valid[mip][idx]) {
return { true, _entries[mip][idx] };
}
return {};
}
// Map a tile and fill unmapped children with fallback to this parent.
// This mirrors UE5/LibVT: if a high-detail tile isn't loaded, the page table
// points to the parent's physical tile instead (bilinear filter hides seams).
void map_with_fallback(int mip, int x, int y, PhysicalLocation loc) {
map(mip, x, y, loc);
_fill_children(mip, x, y, loc);
}
// Evict all entries across all mip levels (full flush).
void clear() {
for (int m = 0; m < _mip_levels; m++) {
std::fill(_valid[m].begin(), _valid[m].end(), false);
memset(_entries[m].data(), 0, _entries[m].size() * sizeof(PhysicalLocation));
}
}
// Evict a region of tiles at a given mip and all finer mips below it.
void invalidate_region(int mip, int x, int y, int w, int h) {
// Invalidate at this mip level
for (int ty = y; ty < y + h; ty++) {
for (int tx = x; tx < x + w; tx++) {
evict(mip, tx, ty);
}
}
// Invalidate finer mips (children)
if (mip > 0) {
invalidate_region(mip - 1, x * 2, y * 2, w * 2, h * 2);
}
}
// ── GPU page table data (for upload to R16G16_UINT texture) ─────────
// Get raw page table data for a specific mip level.
// Each entry is 4 bytes: [x, y, valid_flag, 0]
// Returns total bytes written.
int get_gpu_data(int mip, uint8_t *out, int max_bytes) const {
int size = tiles_at_mip(mip);
int needed = size * size * 4;
if (needed > max_bytes)
return 0;
for (int i = 0; i < size * size; i++) {
if (_valid[mip][i]) {
out[i * 4 + 0] = _entries[mip][i].x;
out[i * 4 + 1] = _entries[mip][i].y;
out[i * 4 + 2] = 1; // valid
out[i * 4 + 3] = 0;
} else {
out[i * 4 + 0] = 0;
out[i * 4 + 1] = 0;
out[i * 4 + 2] = 0; // invalid
out[i * 4 + 3] = 0;
}
}
return needed;
}
private:
int _mip_levels = 0;
int _base_tiles = 0;
std::vector<std::vector<PhysicalLocation>> _entries; // per-mip
std::vector<std::vector<bool>> _valid; // per-mip validity
int _index(int mip, int x, int y) const {
if (mip < 0 || mip >= _mip_levels)
return -1;
int size = tiles_at_mip(mip);
if (x < 0 || x >= size || y < 0 || y >= size)
return -1;
return y * size + x;
}
// Recursively fill unmapped children with parent's physical location.
void _fill_children(int parent_mip, int px, int py, PhysicalLocation loc) {
if (parent_mip <= 0)
return;
int child_mip = parent_mip - 1;
// Each parent tile covers 2×2 children
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
int cx = px * 2 + dx;
int cy = py * 2 + dy;
int idx = _index(child_mip, cx, cy);
if (idx < 0)
continue;
if (!_valid[child_mip][idx]) {
// Only fill if child is unmapped
_entries[child_mip][idx] = loc;
_valid[child_mip][idx] = true;
// Continue recursively
_fill_children(child_mip, cx, cy, loc);
}
}
}
}
};