#include "virtual_texture.h"
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/rd_texture_format.hpp>
#include <godot_cpp/classes/rd_texture_view.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
VirtualTexture::VirtualTexture() {
}
VirtualTexture::~VirtualTexture() {
_free_gpu_resources();
}
// ── Bind methods ──────────────────────────────────────────────────────────────
void VirtualTexture::_bind_methods() {
// Texture registration
ClassDB::bind_method(D_METHOD("register_texture", "name", "width", "height"),
&VirtualTexture::register_texture, DEFVAL(0), DEFVAL(0));
ClassDB::bind_method(D_METHOD("unregister_texture", "vt_id"), &VirtualTexture::unregister_texture);
ClassDB::bind_method(D_METHOD("get_texture_count"), &VirtualTexture::get_texture_count);
// Invalidation
ClassDB::bind_method(D_METHOD("invalidate_region", "texture_id", "tile_x", "tile_y", "w", "h", "mip"), &VirtualTexture::invalidate_region);
ClassDB::bind_method(D_METHOD("invalidate_all", "texture_id"), &VirtualTexture::invalidate_all);
// Atlas access
ClassDB::bind_method(D_METHOD("get_atlas_texture"), &VirtualTexture::get_atlas_texture);
ClassDB::bind_method(D_METHOD("get_page_table_texture"), &VirtualTexture::get_page_table_texture);
// Material factory
ClassDB::bind_method(D_METHOD("create_svt_material", "fallback_albedo", "roughness", "metallic"), &VirtualTexture::create_svt_material);
// Feedback
ClassDB::bind_method(D_METHOD("submit_feedback", "feedback_data"), &VirtualTexture::submit_feedback);
// Tile upload
ClassDB::bind_method(D_METHOD("upload_tile", "texture_id", "tile_x", "tile_y", "mip", "rgba_data"), &VirtualTexture::upload_tile);
// CPU image reconstruction
ClassDB::bind_method(D_METHOD("get_reconstructed_image", "vt_id"), &VirtualTexture::get_reconstructed_image);
ClassDB::bind_method(D_METHOD("get_all_textures"), &VirtualTexture::get_all_textures);
// Stats
ClassDB::bind_method(D_METHOD("get_stats"), &VirtualTexture::get_stats);
// Properties
ClassDB::bind_method(D_METHOD("set_tile_size", "size"), &VirtualTexture::set_tile_size);
ClassDB::bind_method(D_METHOD("get_tile_size"), &VirtualTexture::get_tile_size);
ADD_PROPERTY(PropertyInfo(Variant::INT, "tile_size"), "set_tile_size", "get_tile_size");
ClassDB::bind_method(D_METHOD("set_tile_budget", "budget"), &VirtualTexture::set_tile_budget);
ClassDB::bind_method(D_METHOD("get_tile_budget"), &VirtualTexture::get_tile_budget);
ADD_PROPERTY(PropertyInfo(Variant::INT, "tile_budget"), "set_tile_budget", "get_tile_budget");
ClassDB::bind_method(D_METHOD("set_cache_grid_size", "size"), &VirtualTexture::set_cache_grid_size);
ClassDB::bind_method(D_METHOD("get_cache_grid_size"), &VirtualTexture::get_cache_grid_size);
ADD_PROPERTY(PropertyInfo(Variant::INT, "cache_grid_size"), "set_cache_grid_size", "get_cache_grid_size");
ClassDB::bind_method(D_METHOD("set_mip_levels", "levels"), &VirtualTexture::set_mip_levels);
ClassDB::bind_method(D_METHOD("get_mip_levels"), &VirtualTexture::get_mip_levels);
ADD_PROPERTY(PropertyInfo(Variant::INT, "mip_levels"), "set_mip_levels", "get_mip_levels");
// Signals
ADD_SIGNAL(MethodInfo("tile_requested",
PropertyInfo(Variant::INT, "texture_id"),
PropertyInfo(Variant::INT, "tile_x"),
PropertyInfo(Variant::INT, "tile_y"),
PropertyInfo(Variant::INT, "mip")));
ADD_SIGNAL(MethodInfo("tile_loaded",
PropertyInfo(Variant::INT, "texture_id"),
PropertyInfo(Variant::INT, "tile_x"),
PropertyInfo(Variant::INT, "tile_y"),
PropertyInfo(Variant::INT, "mip")));
}
// ── Properties ────────────────────────────────────────────────────────────────
void VirtualTexture::set_tile_size(int size) { _tile_size = size; }
int VirtualTexture::get_tile_size() const { return _tile_size; }
void VirtualTexture::set_tile_budget(int budget) { _tile_budget = budget; }
int VirtualTexture::get_tile_budget() const { return _tile_budget; }
void VirtualTexture::set_cache_grid_size(int size) { _cache_grid_size = size; }
int VirtualTexture::get_cache_grid_size() const { return _cache_grid_size; }
void VirtualTexture::set_mip_levels(int levels) { _mip_levels = levels; }
int VirtualTexture::get_mip_levels() const { return _mip_levels; }
// ── Texture registration ──────────────────────────────────────────────────────
int VirtualTexture::register_texture(const String &name, int width, int height) {
// Dedup: return existing ID if already registered with same name
for (auto &tex : _textures) {
if (tex.active && tex.name == name) {
UtilityFunctions::print("VirtualTexture: Reusing texture '", name, "' id=", tex.id);
return tex.id;
}
}
int id = _next_texture_id++;
RegisteredTexture tex;
tex.id = id;
tex.name = name;
tex.active = true;
tex.width = width;
tex.height = height;
// Pre-allocate CPU image if dimensions known
if (width > 0 && height > 0) {
tex.cpu_image = Image::create(width, height, false, Image::FORMAT_RGBA8);
tex.cpu_image->fill(Color(0, 0, 0, 0));
}
_textures.push_back(tex);
UtilityFunctions::print("VirtualTexture: Registered texture '", name, "' as id=", id,
" (", width, "x", height, ")");
return id;
}
void VirtualTexture::unregister_texture(int vt_id) {
for (auto &tex : _textures) {
if (tex.id == vt_id) {
tex.active = false;
// Invalidate all tiles for this texture
invalidate_all(vt_id);
return;
}
}
}
int VirtualTexture::get_texture_count() const {
int count = 0;
for (const auto &tex : _textures) {
if (tex.active)
count++;
}
return count;
}
// ── Invalidation ──────────────────────────────────────────────────────────────
void VirtualTexture::invalidate_region(int texture_id, int tile_x, int tile_y, int w, int h, int mip) {
// Evict from page table
_page_table.invalidate_region(mip, tile_x, tile_y, w, h);
// Add invalidation requests so tiles get re-fetched
for (int ty = tile_y; ty < tile_y + h; ty++) {
for (int tx = tile_x; tx < tile_x + w; tx++) {
_request_queue.add_invalidated(texture_id, tx, ty, mip);
}
}
}
void VirtualTexture::invalidate_all(int texture_id) {
_page_table.clear();
// Free all cache slots for this texture
// (simple approach — evict everything)
_tile_cache.clear();
}
// ── Atlas / page table access ─────────────────────────────────────────────────
Ref<Texture2DRD> VirtualTexture::get_atlas_texture() const {
return _atlas_tex;
}
Ref<Texture2DRD> VirtualTexture::get_page_table_texture() const {
return _page_table_tex;
}
// ── Material factory ──────────────────────────────────────────────────────
Ref<ShaderMaterial> VirtualTexture::create_svt_material(const Color &fallback, float rough, float metal) {
// Lazy-load the shader
if (!_shader_loaded) {
_svt_shader = ResourceLoader::get_singleton()->load("res://addons/blender_bridge/svt_lookup.gdshader");
_shader_loaded = true;
}
if (_svt_shader.is_null()) {
UtilityFunctions::push_warning("VirtualTexture: svt_lookup.gdshader not found at res://addons/blender_bridge/");
return Ref<ShaderMaterial>();
}
Ref<ShaderMaterial> mat;
mat.instantiate();
mat->set_shader(_svt_shader);
mat->set_shader_parameter("svt_atlas", _atlas_tex);
mat->set_shader_parameter("svt_page_table", _page_table_tex);
mat->set_shader_parameter("tile_size_uv", 1.0f / _cache_grid_size);
mat->set_shader_parameter("page_table_size", (float)_page_table.tiles_at_mip(0));
mat->set_shader_parameter("albedo_color", fallback);
mat->set_shader_parameter("roughness", rough);
mat->set_shader_parameter("metallic", metal);
return mat;
}
// ── Tile upload ───────────────────────────────────────────────────────────────
Error VirtualTexture::upload_tile(int texture_id, int tile_x, int tile_y, int mip, const PackedByteArray &rgba_data) {
int expected_size = _tile_size * _tile_size * 4; // RGBA8
if (rgba_data.size() != expected_size) {
UtilityFunctions::printerr("VirtualTexture: upload_tile got ",
rgba_data.size(), " bytes, expected ", expected_size);
return ERR_INVALID_PARAMETER;
}
if (!_gpu_initialized) {
return ERR_UNCONFIGURED;
}
// Allocate or find existing cache slot
TileId tile = { texture_id, tile_x, tile_y, mip };
AllocResult alloc = _tile_cache.alloc(tile, _frame_count);
if (!alloc.success) {
return ERR_OUT_OF_MEMORY;
}
// Upload pixel data to atlas at the allocated location
_upload_tile_to_atlas(alloc.location, rgba_data);
// Update page table
_page_table.map_with_fallback(mip, tile_x, tile_y, alloc.location);
// Lock root mip tiles (top 2 mip levels never evicted)
if (mip >= _mip_levels - 2) {
_tile_cache.lock(alloc.location);
}
// Blit mip-0 tiles into the CPU reconstruction image
if (mip == 0) {
for (auto &tex : _textures) {
if (tex.id == texture_id && tex.cpu_image.is_valid()) {
Ref<Image> tile_img = Image::create_from_data(
_tile_size, _tile_size, false, Image::FORMAT_RGBA8, rgba_data);
int dst_x = tile_x * _tile_size;
int dst_y = tile_y * _tile_size;
int copy_w = MIN(_tile_size, tex.width - dst_x);
int copy_h = MIN(_tile_size, tex.height - dst_y);
if (copy_w > 0 && copy_h > 0) {
tex.cpu_image->blit_rect(tile_img, Rect2i(0, 0, copy_w, copy_h),
Vector2i(dst_x, dst_y));
}
break;
}
}
}
emit_signal("tile_loaded", texture_id, tile_x, tile_y, mip);
_tiles_uploaded_this_frame++;
return OK;
}
// ── Feedback submission ───────────────────────────────────────────────────────
void VirtualTexture::submit_feedback(const PackedByteArray &feedback_data) {
if (feedback_data.size() < 4)
return;
int count = feedback_data.size() / 4;
const uint32_t *data = reinterpret_cast<const uint32_t *>(feedback_data.ptr());
_request_queue.add_from_feedback(data, count);
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
void VirtualTexture::_ready() {
_page_table.init(_mip_levels);
_tile_cache.init(_cache_grid_size, _cache_grid_size);
_init_gpu_resources();
}
void VirtualTexture::_process(double delta) {
_frame_count++;
_tiles_uploaded_this_frame = 0;
_process_tile_requests();
// Upload updated page table to GPU each frame
if (_gpu_initialized) {
_upload_page_table_to_gpu();
}
}
void VirtualTexture::_exit_tree() {
_free_gpu_resources();
}
// ── GPU resource management ───────────────────────────────────────────────────
void VirtualTexture::_init_gpu_resources() {
RenderingServer *rs = RenderingServer::get_singleton();
if (!rs)
return;
RenderingDevice *rd = rs->get_rendering_device();
if (!rd) {
UtilityFunctions::printerr("VirtualTexture: No RenderingDevice available");
return;
}
// ── Physical cache atlas ────────────────────────────────────────────
// Size: cache_grid * tile_size in each dimension
int atlas_size = _cache_grid_size * _tile_size;
Ref<RDTextureFormat> atlas_fmt;
atlas_fmt.instantiate();
atlas_fmt->set_width(atlas_size);
atlas_fmt->set_height(atlas_size);
atlas_fmt->set_format(RenderingDevice::DATA_FORMAT_R8G8B8A8_UNORM);
atlas_fmt->set_usage_bits(
RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_COPY_TO_BIT);
Ref<RDTextureView> atlas_view;
atlas_view.instantiate();
_atlas_rid = rd->texture_create(atlas_fmt, atlas_view);
if (_atlas_rid.is_valid()) {
_atlas_tex.instantiate();
_atlas_tex->set_texture_rd_rid(_atlas_rid);
}
// ── Page table texture ──────────────────────────────────────────────
// One texel per virtual tile at mip 0, RGBA8:
// R = cache_x, G = cache_y, B = valid flag, A = 0
int pt_size = _page_table.tiles_at_mip(0);
Ref<RDTextureFormat> pt_fmt;
pt_fmt.instantiate();
pt_fmt->set_width(pt_size);
pt_fmt->set_height(pt_size);
pt_fmt->set_format(RenderingDevice::DATA_FORMAT_R8G8B8A8_UNORM);
pt_fmt->set_usage_bits(
RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
Ref<RDTextureView> pt_view;
pt_view.instantiate();
_page_table_rid = rd->texture_create(pt_fmt, pt_view);
if (_page_table_rid.is_valid()) {
_page_table_tex.instantiate();
_page_table_tex->set_texture_rd_rid(_page_table_rid);
}
_gpu_initialized = _atlas_rid.is_valid() && _page_table_rid.is_valid();
if (_gpu_initialized) {
UtilityFunctions::print("VirtualTexture: GPU resources initialized (",
atlas_size, "x", atlas_size, " atlas, ",
pt_size, "x", pt_size, " page table)");
} else {
UtilityFunctions::printerr("VirtualTexture: Failed to create GPU textures");
}
}
void VirtualTexture::_free_gpu_resources() {
if (!_gpu_initialized)
return;
RenderingServer *rs = RenderingServer::get_singleton();
if (!rs)
return;
RenderingDevice *rd = rs->get_rendering_device();
if (!rd)
return;
if (_atlas_rid.is_valid()) {
rd->free_rid(_atlas_rid);
_atlas_rid = RID();
}
if (_page_table_rid.is_valid()) {
rd->free_rid(_page_table_rid);
_page_table_rid = RID();
}
_atlas_tex.unref();
_page_table_tex.unref();
_gpu_initialized = false;
}
void VirtualTexture::_upload_page_table_to_gpu() {
RenderingServer *rs = RenderingServer::get_singleton();
if (!rs)
return;
RenderingDevice *rd = rs->get_rendering_device();
if (!rd)
return;
int pt_size = _page_table.tiles_at_mip(0);
int data_size = pt_size * pt_size * 4; // RGBA8
PackedByteArray pba;
pba.resize(data_size);
uint8_t *dst = pba.ptrw();
_page_table.get_gpu_data(0, dst, data_size);
// Upload to GPU
rd->texture_update(_page_table_rid, 0, pba);
}
void VirtualTexture::_upload_tile_to_atlas(PhysicalLocation loc, const PackedByteArray &rgba_data) {
RenderingServer *rs = RenderingServer::get_singleton();
if (!rs)
return;
RenderingDevice *rd = rs->get_rendering_device();
if (!rd)
return;
// Create a temporary source texture for this tile
Ref<RDTextureFormat> tile_fmt;
tile_fmt.instantiate();
tile_fmt->set_width(_tile_size);
tile_fmt->set_height(_tile_size);
tile_fmt->set_format(RenderingDevice::DATA_FORMAT_R8G8B8A8_UNORM);
tile_fmt->set_usage_bits(
RenderingDevice::TEXTURE_USAGE_CAN_COPY_FROM_BIT |
RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
Ref<RDTextureView> tile_view;
tile_view.instantiate();
TypedArray<PackedByteArray> data_layers;
data_layers.push_back(rgba_data);
RID tile_rid = rd->texture_create(tile_fmt, tile_view, data_layers);
if (!tile_rid.is_valid()) {
UtilityFunctions::printerr("VirtualTexture: Failed to create temp tile texture");
return;
}
// Copy tile into atlas at the physical location
// Atlas position = loc * tile_size
int dst_x = loc.x * _tile_size;
int dst_y = loc.y * _tile_size;
rd->texture_copy(
tile_rid, _atlas_rid,
Vector3(0, 0, 0), // src offset
Vector3(dst_x, dst_y, 0), // dst offset
Vector3(_tile_size, _tile_size, 1), // size
0, 0, // src/dst mip
0, 0 // src/dst layer
);
// Free temporary texture
rd->free_rid(tile_rid);
}
// ── Per-frame pipeline ────────────────────────────────────────────────────────
void VirtualTexture::_process_tile_requests() {
// Get prioritized batch within budget
auto batch = _request_queue.get_next_batch(_tile_budget);
for (const auto &req : batch) {
// Allocate a cache slot
AllocResult alloc = _tile_cache.alloc(req.tile, _frame_count);
if (!alloc.success)
continue;
// If eviction happened, update page table for evicted tile
if (alloc.was_eviction) {
// The evicted tile's page table entry will be stale — it'll
// get a fallback or be re-requested via feedback next frame
}
// Map in page table
_page_table.map_with_fallback(
req.tile.mip, req.tile.tile_x, req.tile.tile_y,
alloc.location);
// Signal that we need this tile's data
emit_signal("tile_requested",
req.tile.texture_id, req.tile.tile_x,
req.tile.tile_y, req.tile.mip);
_tiles_uploaded_this_frame++;
}
}
// ── CPU image reconstruction ──────────────────────────────────────────────────
Ref<Image> VirtualTexture::get_reconstructed_image(int vt_id) const {
for (const auto &tex : _textures) {
if (tex.id == vt_id && tex.active) {
return tex.cpu_image;
}
}
return Ref<Image>();
}
TypedArray<Dictionary> VirtualTexture::get_all_textures() const {
TypedArray<Dictionary> result;
for (const auto &tex : _textures) {
if (!tex.active)
continue;
Dictionary d;
d["id"] = tex.id;
d["name"] = tex.name;
d["width"] = tex.width;
d["height"] = tex.height;
d["has_image"] = tex.cpu_image.is_valid();
result.push_back(d);
}
return result;
}
Ref<Shader> VirtualTexture::get_svt_shader() const {
return _svt_shader;
}
// ── Stats ─────────────────────────────────────────────────────────────────────
Dictionary VirtualTexture::get_stats() const {
Dictionary stats;
stats["frame"] = (int)_frame_count;
stats["tiles_uploaded_this_frame"] = _tiles_uploaded_this_frame;
stats["cache_occupied"] = _tile_cache.occupied_count();
stats["cache_total"] = _tile_cache.total_slots();
stats["cache_locked"] = _tile_cache.locked_count();
stats["pending_requests"] = _request_queue.pending_count();
stats["registered_textures"] = get_texture_count();
return stats;
}