OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge / src / tile_cache.h

224 lines · c ref:
#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;
	}
};