/**************************************************************************/
/* render_raytracing.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "core/math/math_funcs.h"
#include "servers/rendering/renderer_rd/environment/sky.h"
#include "servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.h"
#include "servers/rendering/renderer_rd/forward_clustered/scene_shader_raytracing.h"
#include "servers/rendering/renderer_rd/renderer_scene_render_rd.h"
#include "servers/rendering/renderer_rd/storage_rd/light_storage.h"
#include "servers/rendering/renderer_rd/storage_rd/material_storage.h"
#include "servers/rendering/renderer_rd/storage_rd/mesh_storage.h"
#include "servers/rendering/renderer_rd/storage_rd/particles_storage.h"
#include "servers/rendering/renderer_rd/storage_rd/texture_storage.h"
#include "servers/rendering/rendering_server_globals.h"
#include "servers/rendering/storage/environment_storage.h"
using namespace RendererSceneRenderImplementation;
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
BindlessBlock::TextureKind RenderRaytracing::_texture_kind_for_type(ShaderLanguage::DataType p_type) {
switch (p_type) {
case ShaderLanguage::TYPE_SAMPLER2DARRAY:
case ShaderLanguage::TYPE_ISAMPLER2DARRAY:
case ShaderLanguage::TYPE_USAMPLER2DARRAY:
return BindlessBlock::TEXTURE_2D_ARRAY;
case ShaderLanguage::TYPE_SAMPLER3D:
case ShaderLanguage::TYPE_ISAMPLER3D:
case ShaderLanguage::TYPE_USAMPLER3D:
return BindlessBlock::TEXTURE_3D;
case ShaderLanguage::TYPE_SAMPLERCUBE:
return BindlessBlock::TEXTURE_CUBE;
case ShaderLanguage::TYPE_SAMPLERCUBEARRAY:
return BindlessBlock::TEXTURE_CUBE_ARRAY;
default:
return BindlessBlock::TEXTURE_2D;
}
}
void RenderRaytracing::initialize(RenderForwardClustered *p_owner) {
owner = p_owner;
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
bindless_blocks[k] = memnew(BindlessBlock);
}
}
RenderRaytracing::~RenderRaytracing() {
for (KeyValue<RenderSceneBuffersRD *, RTViewportState *> &kv : viewport_states) {
_free_viewport_state_internal(kv.value);
}
viewport_states.clear();
cleanup_caches();
if (mat_ubo_pool_buffer.is_valid()) {
RD::get_singleton()->free_rid(mat_ubo_pool_buffer);
mat_ubo_pool_buffer = RID();
}
if (bindless_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(bindless_uniform_set)) {
RD::get_singleton()->free_rid(bindless_uniform_set);
}
bindless_uniform_set = RID();
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
if (bindless_blocks[k]) {
memdelete(bindless_blocks[k]);
bindless_blocks[k] = nullptr;
}
}
if (shader) {
memdelete(shader);
shader = nullptr;
}
}
// ---------------------------------------------------------------------------
// Per-viewport state lifecycle
// ---------------------------------------------------------------------------
RTViewportState *RenderRaytracing::_get_or_create_viewport_state(const RenderDataRD *p_render_data) {
if (!p_render_data || p_render_data->render_buffers.is_null()) {
return nullptr;
}
RenderSceneBuffersRD *key = p_render_data->render_buffers.ptr();
HashMap<RenderSceneBuffersRD *, RTViewportState *>::Iterator it = viewport_states.find(key);
if (it != viewport_states.end()) {
return it->value;
}
RTViewportState *state = memnew(RTViewportState);
viewport_states.insert(key, state);
return state;
}
RTViewportState *RenderRaytracing::_get_viewport_state(const RenderDataRD *p_render_data) const {
if (!p_render_data || p_render_data->render_buffers.is_null()) {
return nullptr;
}
RenderSceneBuffersRD *key = p_render_data->render_buffers.ptr();
HashMap<RenderSceneBuffersRD *, RTViewportState *>::ConstIterator it = viewport_states.find(key);
return (it != viewport_states.end()) ? it->value : nullptr;
}
void RenderRaytracing::_free_viewport_state_internal(RTViewportState *p_state) {
if (!p_state) {
return;
}
if (p_state->uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(p_state->uniform_set)) {
RD::get_singleton()->free_rid(p_state->uniform_set);
}
if (p_state->tlas.is_valid()) {
RD::get_singleton()->free_rid(p_state->tlas);
}
if (p_state->geometry_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->geometry_buffer);
}
if (p_state->material_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->material_buffer);
}
if (p_state->motion_index_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->motion_index_buffer);
}
if (p_state->motion_transform_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->motion_transform_buffer);
}
if (p_state->light_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->light_buffer);
}
if (p_state->params_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->params_buffer);
}
memdelete(p_state);
}
void RenderRaytracing::free_viewport_state(RenderSceneBuffersRD *p_render_buffers) {
HashMap<RenderSceneBuffersRD *, RTViewportState *>::Iterator it = viewport_states.find(p_render_buffers);
if (it == viewport_states.end()) {
return;
}
_free_viewport_state_internal(it->value);
viewport_states.remove(it);
}
// ---------------------------------------------------------------------------
// Material UBO sub-allocation pool
//
// Single device-address storage buffer of MAT_UBO_POOL_TOTAL_BYTES, divided
// into MAT_UBO_POOL_CAPACITY fixed-size slots. Allocate/release just bump a
// next-slot counter and a free-list. Per-material UBO writes are
// buffer_update at slot offset, no allocation. mat.uniform_address is
// pool_bda + slot * slot_size, so the closest-hit shader's
// CustomMaterialUniforms(addr) cast lands on the correct slot.
// ---------------------------------------------------------------------------
namespace {
// Pool tuning. Kept in this TU because nothing outside it needs to know.
// SLOT_SIZE bounds the per-material UBO before we fall back to a dedicated
// buffer; CAPACITY is the maximum number of pooled materials in flight.
constexpr uint32_t MAT_UBO_POOL_SLOT_SIZE = 512;
constexpr uint32_t MAT_UBO_POOL_CAPACITY = 100000;
constexpr uint64_t MAT_UBO_POOL_TOTAL_BYTES = uint64_t(MAT_UBO_POOL_SLOT_SIZE) * MAT_UBO_POOL_CAPACITY;
} // namespace
void RenderRaytracing::mat_ubo_pool_ensure_initialized() {
if (mat_ubo_pool_buffer.is_valid()) {
return;
}
Vector<uint8_t> init;
init.resize(MAT_UBO_POOL_TOTAL_BYTES);
memset(init.ptrw(), 0, MAT_UBO_POOL_TOTAL_BYTES);
mat_ubo_pool_buffer = RD::get_singleton()->storage_buffer_create(
MAT_UBO_POOL_TOTAL_BYTES, init, 0,
RD::BUFFER_CREATION_DEVICE_ADDRESS_BIT);
if (mat_ubo_pool_buffer.is_valid()) {
RD::get_singleton()->set_resource_name(mat_ubo_pool_buffer, "RT Material UBO Pool");
mat_ubo_pool_bda = RD::get_singleton()->buffer_get_device_address(mat_ubo_pool_buffer);
}
}
uint32_t RenderRaytracing::mat_ubo_pool_allocate() {
mat_ubo_pool_ensure_initialized();
if (!mat_ubo_pool_buffer.is_valid()) {
return UINT32_MAX;
}
// Pop from the free-list stack. mat_ubo_pool_free_count is the logical top;
// the underlying vector is never shrunk so this is a pure counter op.
if (mat_ubo_pool_free_count > 0) {
--mat_ubo_pool_free_count;
return mat_ubo_pool_free_slots[mat_ubo_pool_free_count];
}
if (mat_ubo_pool_next_slot >= MAT_UBO_POOL_CAPACITY) {
ERR_PRINT_ONCE("RT Material UBO Pool exhausted; falling back to dedicated buffer.");
return UINT32_MAX;
}
return mat_ubo_pool_next_slot++;
}
void RenderRaytracing::mat_ubo_pool_release(uint32_t p_slot) {
if (p_slot >= MAT_UBO_POOL_CAPACITY) {
return;
}
// Push onto the free-list stack. Grow the underlying buffer only when the
// counter would overflow the existing capacity; never shrink.
if (mat_ubo_pool_free_count < mat_ubo_pool_free_slots.size()) {
mat_ubo_pool_free_slots[mat_ubo_pool_free_count] = p_slot;
} else {
mat_ubo_pool_free_slots.push_back(p_slot);
}
++mat_ubo_pool_free_count;
}
void RenderRaytracing::mat_ubo_pool_update(uint32_t p_slot, const void *p_data, uint32_t p_size) {
if (!mat_ubo_pool_buffer.is_valid() || p_slot >= MAT_UBO_POOL_CAPACITY) {
return;
}
uint32_t size = MIN(p_size, MAT_UBO_POOL_SLOT_SIZE);
RD::get_singleton()->buffer_update(mat_ubo_pool_buffer, uint64_t(p_slot) * MAT_UBO_POOL_SLOT_SIZE, size, p_data);
}
uint64_t RenderRaytracing::mat_ubo_pool_get_address(uint32_t p_slot) const {
return mat_ubo_pool_bda + uint64_t(p_slot) * MAT_UBO_POOL_SLOT_SIZE;
}
// ---------------------------------------------------------------------------
// Cache management
// ---------------------------------------------------------------------------
// A cached BLAS can be reclaimed out from under us. blas_create registers the BLAS
// as a dependency of the vertex/index buffers it was built from
// (rendering_device.cpp:444), and _free_dependencies frees every dependent when the
// buffer goes, so a streaming mesh dropping its buffers takes the BLAS with it. The
// cache entry still holds the RID, and RID::is_valid() only tests for non-null -- it
// cannot see that the RD already reclaimed the slot -- so the next refresh frees it a
// second time. That is the "Attempted to free invalid ID ... was acceleration_structure"
// storm: hundreds per frame under dc-terrain streaming (hyper-games #11 §7.3).
static void rt_free_blas_if_owned(RID &r_blas) {
if (r_blas.is_valid() && RD::get_singleton()->acceleration_structure_is_valid(r_blas)) {
RD::get_singleton()->free_rid(r_blas);
}
r_blas = RID();
}
void RenderRaytracing::cleanup_caches() {
// Free all cached surface data. BLAS resources are NOT freed here because
// they are owned by the RD dependency chain.
for (uint32_t i = 0; i < surface_chunks.size(); i++) {
if (surface_chunks[i]) {
for (uint32_t j = 0; j < RT_CACHE_CHUNK_SIZE; j++) {
RTCacheEntry *entry = &surface_chunks[i][j];
if (entry->ptr) {
memdelete(entry->ptr);
entry->ptr = nullptr;
}
}
memdelete_arr(surface_chunks[i]);
}
}
surface_chunks.clear();
for (KeyValue<uint64_t, RTDeformedCacheEntry> &kv : deformed_surface_cache) {
RTDeformedCacheEntry &e = kv.value;
if (e.ptr) {
rt_free_blas_if_owned(e.ptr->blas);
memdelete(e.ptr);
e.ptr = nullptr;
}
}
deformed_surface_cache.clear();
// Free all cached material data
for (uint32_t i = 0; i < material_chunks.size(); i++) {
if (material_chunks[i]) {
for (uint32_t j = 0; j < RT_CACHE_CHUNK_SIZE; j++) {
RTMaterialCacheEntry *entry = &material_chunks[i][j];
if (entry->ptr) {
if (entry->ptr->uniform_buffer.is_valid()) {
RD::get_singleton()->free_rid(entry->ptr->uniform_buffer);
}
if (entry->ptr->uniform_pool_slot != UINT32_MAX) {
mat_ubo_pool_release(entry->ptr->uniform_pool_slot);
}
memdelete(entry->ptr);
entry->ptr = nullptr;
}
}
memdelete_arr(material_chunks[i]);
}
}
material_chunks.clear();
// Reset counters
material_free_slots.clear();
next_material_slot = 0;
vram_used = 0;
cache_hits = 0;
cache_misses = 0;
}
// ---------------------------------------------------------------------------
// RID helpers
// ---------------------------------------------------------------------------
uint32_t RenderRaytracing::get_rid_index(RID p_rid) {
return static_cast<uint32_t>(p_rid.get_id() & 0xFFFFFFFFULL);
}
uint32_t RenderRaytracing::get_rid_version(RID p_rid) {
return static_cast<uint32_t>(p_rid.get_id() >> 32);
}
RTCacheEntry *RenderRaytracing::get_surface_cache_entry(uint32_t p_index) {
uint32_t chunk_idx = p_index >> RT_CACHE_CHUNK_SHIFT;
uint32_t entry_idx = p_index & RT_CACHE_CHUNK_MASK;
// Grow vector if needed, initializing new slots to nullptr
while (chunk_idx >= surface_chunks.size()) {
surface_chunks.push_back(nullptr);
}
if (!surface_chunks[chunk_idx]) {
surface_chunks.set(chunk_idx, memnew_arr(RTCacheEntry, RT_CACHE_CHUNK_SIZE));
for (uint32_t i = 0; i < RT_CACHE_CHUNK_SIZE; i++) {
surface_chunks[chunk_idx][i] = RTCacheEntry();
}
}
return &surface_chunks[chunk_idx][entry_idx];
}
RTMaterialCacheEntry *RenderRaytracing::get_material_cache_entry(uint32_t p_index) {
uint32_t chunk_idx = p_index >> RT_CACHE_CHUNK_SHIFT;
uint32_t entry_idx = p_index & RT_CACHE_CHUNK_MASK;
// Grow vector if needed, initializing new slots to nullptr
while (chunk_idx >= material_chunks.size()) {
material_chunks.push_back(nullptr);
}
if (!material_chunks[chunk_idx]) {
material_chunks.set(chunk_idx, memnew_arr(RTMaterialCacheEntry, RT_CACHE_CHUNK_SIZE));
for (uint32_t i = 0; i < RT_CACHE_CHUNK_SIZE; i++) {
material_chunks[chunk_idx][i] = RTMaterialCacheEntry();
}
}
return &material_chunks[chunk_idx][entry_idx];
}
uint32_t RenderRaytracing::allocate_material_slot() {
if (!material_free_slots.is_empty()) {
uint32_t slot = material_free_slots[material_free_slots.size() - 1];
material_free_slots.resize(material_free_slots.size() - 1);
return slot;
}
return next_material_slot++;
}
// ---------------------------------------------------------------------------
// Per-frame preparation
// ---------------------------------------------------------------------------
void RenderRaytracing::prepare_frame() {
// Don't free BLAS or materials - they're cached.
// Scratch arrays are shared (single-threaded render thread); refilled per viewport.
blass.clear();
blas_transforms.clear();
instance_flags.clear();
sbt_offsets.clear();
geometry_data.clear();
material_data.clear();
motion_indices.clear();
motion_transforms.clear();
// Procedural BLAS/AABB lifetime is on the geometry instance.
{
const uint32_t DEFORMED_CACHE_TTL_FRAMES = 60;
uint32_t current_frame = RSG::rasterizer->get_frame_number();
LocalVector<uint64_t> to_remove;
for (KeyValue<uint64_t, RTDeformedCacheEntry> &kv : deformed_surface_cache) {
RTDeformedCacheEntry &e = kv.value;
if (e.last_used_frame != 0 && current_frame - e.last_used_frame > DEFORMED_CACHE_TTL_FRAMES) {
if (e.ptr) {
rt_free_blas_if_owned(e.ptr->blas);
memdelete(e.ptr);
e.ptr = nullptr;
}
to_remove.push_back(kv.key);
}
}
for (uint64_t k : to_remove) {
deformed_surface_cache.erase(k);
}
}
// Finish async HG compiles so live_ready_mask matches this frame (sync path fills at build_tlas end).
SceneShaderRaytracing::get_singleton()->drain_completed_compiles();
// Grow-only geometry/material/motion; TLAS reused; uploads in finalize_buffers().
// Reset per-frame metrics
cache_hits = 0;
cache_misses = 0;
// The whole set is rebuilt as a unit, so its liveness is evaluated once and
// handed to every table.
const bool bindless_set_valid = bindless_uniform_set.is_valid() &&
RD::get_singleton()->uniform_set_is_valid(bindless_uniform_set);
if (!bindless_set_valid) {
bindless_uniform_set = RID();
}
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
BindlessBlock *block = bindless_blocks[k];
if (!block->is_initialized()) {
block->initialize(RD::get_singleton(), (BindlessBlock::TextureKind)k);
}
block->begin_frame(bindless_set_valid);
}
}
// ---------------------------------------------------------------------------
// Surface processing
// ---------------------------------------------------------------------------
RTSurfaceData *RenderRaytracing::process_surface(
const void *p_surf,
void *p_mesh_surface,
uint32_t p_surface_invalidation_counter,
const Transform3D &p_transform,
LocalVector<RID> &r_dirty_blas_list) {
const RenderForwardClustered::GeometryInstanceSurfaceDataCache *surf =
static_cast<const RenderForwardClustered::GeometryInstanceSurfaceDataCache *>(p_surf);
RendererRD::MeshStorage *mesh_storage = RendererRD::MeshStorage::get_singleton();
// Cache key: mesh RID + surface index
RID mesh_rid = surf->owner->data->base;
uint32_t cache_key = (mesh_rid.get_local_index() << 8) | (surf->surface_index & 0xFF);
uint32_t mesh_version = get_rid_version(mesh_rid);
// Cache lookup
RTCacheEntry *entry = get_surface_cache_entry(cache_key);
uint32_t current_frame = RSG::rasterizer->get_frame_number();
bool needs_refresh = !entry->ptr ||
entry->cached_rid_version != mesh_version ||
entry->cached_counter != p_surface_invalidation_counter;
if (!needs_refresh && entry->ptr->blas.is_valid()) {
cache_hits++;
entry->last_used_frame = current_frame;
return entry->ptr;
}
// Cache miss - need to create new BLAS
cache_misses++;
// Allocate or reuse entry
if (!entry->ptr) {
entry->ptr = memnew(RTSurfaceData);
} else {
// Free the old BLAS before creating a new one -- if the RD still has it.
rt_free_blas_if_owned(entry->ptr->blas);
}
RTSurfaceData *surf_data = entry->ptr;
_populate_surface_blas(p_mesh_surface, RID(), false, false, false, cache_key, surf_data, r_dirty_blas_list);
surf->cached_final_transform_valid = false;
if (!surf_data->blas.is_valid()) {
return surf_data;
}
entry->cached_counter = p_surface_invalidation_counter;
entry->cached_rid_version = mesh_version;
entry->last_used_frame = current_frame;
return surf_data;
}
// ---------------------------------------------------------------------------
// Deformed surface processing
// ---------------------------------------------------------------------------
RTSurfaceData *RenderRaytracing::process_deformed_surface(
const void *p_surf,
void *p_mesh_surface,
const RTDeformedGeometrySource &p_source,
LocalVector<RID> &r_dirty_blas_list,
LocalVector<RID> &r_dirty_blas_update_list) {
const RenderForwardClustered::GeometryInstanceSurfaceDataCache *surf =
static_cast<const RenderForwardClustered::GeometryInstanceSurfaceDataCache *>(p_surf);
if (!p_source.current_vb.is_valid()) {
return nullptr;
}
uint32_t current_frame = RSG::rasterizer->get_frame_number();
uint64_t buffer_id = p_source.current_vb.get_id();
RTDeformedCacheEntry &entry = deformed_surface_cache[p_source.cache_key];
bool needs_refresh = !entry.ptr ||
entry.cached_key_version != p_source.cache_version ||
entry.cached_surface_counter != p_source.surface_counter ||
entry.cached_buffer_id != buffer_id ||
entry.cached_change_stamp != p_source.change_stamp;
auto stamp_deformed_geometry = [&](RTSurfaceData *p_surf_data) {
if (!p_surf_data || !p_surf_data->blas.is_valid()) {
return;
}
p_surf_data->geometry.flags |= RT_GEOM_FLAG_DEFORMED;
uint64_t prev_addr = 0;
if (p_source.prev_vb.is_valid() && p_source.prev_vb != p_source.current_vb) {
prev_addr = RD::get_singleton()->buffer_get_device_address(p_source.prev_vb);
}
p_surf_data->geometry.prev_vertex_buffer_address_lo = static_cast<uint32_t>(prev_addr & 0xFFFFFFFFULL);
p_surf_data->geometry.prev_vertex_buffer_address_hi = static_cast<uint32_t>(prev_addr >> 32);
};
if (!needs_refresh && entry.ptr->blas.is_valid()) {
cache_hits++;
entry.last_used_frame = current_frame;
stamp_deformed_geometry(entry.ptr);
return entry.ptr;
}
cache_misses++;
// Refit when only the current deformed vertex buffer contents changed.
bool can_refit = entry.ptr && entry.ptr->blas.is_valid() && entry.blas_built_once &&
entry.cached_key_version == p_source.cache_version &&
entry.cached_surface_counter == p_source.surface_counter &&
entry.cached_buffer_id == buffer_id;
if (can_refit) {
RTSurfaceData *surf_data = entry.ptr;
r_dirty_blas_update_list.push_back(surf_data->blas);
surf->cached_final_transform_valid = false;
stamp_deformed_geometry(surf_data);
entry.cached_change_stamp = p_source.change_stamp;
entry.last_used_frame = current_frame;
return surf_data;
}
if (!entry.ptr) {
entry.ptr = memnew(RTSurfaceData);
} else {
rt_free_blas_if_owned(entry.ptr->blas);
entry.blas_built_once = false;
}
RTSurfaceData *surf_data = entry.ptr;
_populate_surface_blas(p_mesh_surface, p_source.current_vb, true, true, true, static_cast<uint32_t>(p_source.cache_key), surf_data, r_dirty_blas_list);
surf->cached_final_transform_valid = false;
if (!surf_data->blas.is_valid()) {
return surf_data;
}
stamp_deformed_geometry(surf_data);
entry.blas_built_once = true;
entry.cached_change_stamp = p_source.change_stamp;
entry.cached_key_version = p_source.cache_version;
entry.cached_surface_counter = p_source.surface_counter;
entry.cached_buffer_id = buffer_id;
entry.last_used_frame = current_frame;
return surf_data;
}
// ---------------------------------------------------------------------------
// Surface BLAS helper (shared between static and deformed paths)
// ---------------------------------------------------------------------------
void RenderRaytracing::_populate_surface_blas(
void *p_mesh_surface,
RID p_vertex_buffer_override,
bool p_force_uncompressed,
bool p_prefer_fast_build,
bool p_allow_update,
uint32_t p_cache_key,
RTSurfaceData *r_surf_data,
LocalVector<RID> &r_dirty_blas_list) {
RendererRD::MeshStorage *mesh_storage = RendererRD::MeshStorage::get_singleton();
uint64_t surface_format = mesh_storage->mesh_surface_get_format(p_mesh_surface);
bool compressed = (surface_format & RSE::ARRAY_FLAG_COMPRESS_ATTRIBUTES) && !p_force_uncompressed;
bool is_2d = surface_format & RSE::ARRAY_FLAG_USE_2D_VERTICES;
r_surf_data->is_compressed = compressed;
if (compressed) {
AABB surface_aabb = mesh_storage->mesh_surface_get_aabb(p_mesh_surface);
r_surf_data->aabb_transform.basis = Basis::from_scale(surface_aabb.size);
r_surf_data->aabb_transform.origin = surface_aabb.position;
} else {
r_surf_data->aabb_transform = Transform3D();
}
RT_GeometryData &geom = r_surf_data->geometry;
memset(&geom, 0, sizeof(geom));
RID vertex_buffer = p_vertex_buffer_override.is_valid()
? p_vertex_buffer_override
: mesh_storage->mesh_surface_get_vertex_buffer(p_mesh_surface);
RID attribute_buffer = mesh_storage->mesh_surface_get_attribute_buffer(p_mesh_surface);
RID index_buffer = mesh_storage->mesh_surface_get_index_buffer(p_mesh_surface, 0);
if (vertex_buffer.is_valid()) {
geom.vertex_buffer_address = RD::get_singleton()->buffer_get_device_address(vertex_buffer);
}
if (attribute_buffer.is_valid()) {
geom.attribute_buffer_address = RD::get_singleton()->buffer_get_device_address(attribute_buffer);
}
uint32_t vertex_count = mesh_storage->mesh_surface_get_vertex_count(p_mesh_surface);
uint32_t index_count = mesh_storage->mesh_surface_get_index_count(p_mesh_surface, 0);
geom.vertex_count = vertex_count;
// Position stride
uint32_t position_stride;
if (is_2d) {
position_stride = sizeof(float) * 2;
} else if (compressed) {
position_stride = sizeof(uint16_t) * 4;
} else {
position_stride = sizeof(float) * 3;
}
geom.position_stride = position_stride;
// Normal/tangent layout
uint32_t normal_stride;
uint32_t tangent_stride = 0;
geom.normal_byte_offset = RT_OFFSET_NONE;
geom.tangent_byte_offset = RT_OFFSET_NONE;
uint32_t current_offset = position_stride * vertex_count;
bool has_normal = surface_format & RSE::ARRAY_FORMAT_NORMAL;
bool has_tangent = surface_format & RSE::ARRAY_FORMAT_TANGENT;
if (compressed) {
normal_stride = sizeof(uint16_t) * 2;
if (has_normal) {
geom.normal_byte_offset = current_offset;
current_offset += normal_stride * vertex_count;
}
} else {
if (has_normal && has_tangent) {
normal_stride = sizeof(uint16_t) * 4;
tangent_stride = sizeof(uint16_t) * 4;
geom.normal_byte_offset = current_offset;
geom.tangent_byte_offset = current_offset;
current_offset += normal_stride * vertex_count;
} else if (has_normal) {
normal_stride = sizeof(uint16_t) * 2;
geom.normal_byte_offset = current_offset;
current_offset += normal_stride * vertex_count;
} else {
normal_stride = 0;
}
}
geom.normal_stride = normal_stride;
geom.tangent_stride = tangent_stride;
geom.flags = compressed ? RT_GEOM_FLAG_COMPRESSED : 0;
if (compressed) {
AABB surface_aabb = mesh_storage->mesh_surface_get_aabb(p_mesh_surface);
geom.aabb_size_x = surface_aabb.size.x;
geom.aabb_size_y = surface_aabb.size.y;
geom.aabb_size_z = surface_aabb.size.z;
geom.aabb_pos_x = surface_aabb.position.x;
geom.aabb_pos_y = surface_aabb.position.y;
geom.aabb_pos_z = surface_aabb.position.z;
} else {
geom.aabb_size_x = 1.0f;
geom.aabb_size_y = 1.0f;
geom.aabb_size_z = 1.0f;
geom.aabb_pos_x = 0.0f;
geom.aabb_pos_y = 0.0f;
geom.aabb_pos_z = 0.0f;
}
// Attribute buffer layout
uint32_t attrib_offset = 0;
geom.uv_byte_offset = RT_OFFSET_NONE;
geom.color_byte_offset = RT_OFFSET_NONE;
if (surface_format & RSE::ARRAY_FORMAT_COLOR) {
geom.color_byte_offset = attrib_offset;
attrib_offset += sizeof(uint32_t);
}
if (surface_format & RSE::ARRAY_FORMAT_TEX_UV) {
geom.uv_byte_offset = attrib_offset;
attrib_offset += compressed ? sizeof(uint16_t) * 2 : sizeof(float) * 2;
}
if (surface_format & RSE::ARRAY_FORMAT_TEX_UV2) {
attrib_offset += compressed ? sizeof(uint16_t) * 2 : sizeof(float) * 2;
}
for (int ci = 0; ci < RSE::ARRAY_CUSTOM_COUNT; ci++) {
const uint32_t fmt_shift[RSE::ARRAY_CUSTOM_COUNT] = { RSE::ARRAY_FORMAT_CUSTOM0_SHIFT, RSE::ARRAY_FORMAT_CUSTOM1_SHIFT, RSE::ARRAY_FORMAT_CUSTOM2_SHIFT, RSE::ARRAY_FORMAT_CUSTOM3_SHIFT };
geom.custom_packed[ci] = RT_OFFSET_NONE;
if (surface_format & (1ULL << (RSE::ARRAY_CUSTOM0 + ci))) {
uint32_t fmt = (surface_format >> fmt_shift[ci]) & RSE::ARRAY_FORMAT_CUSTOM_MASK;
const uint32_t fmtsize[RSE::ARRAY_CUSTOM_MAX] = { 4, 4, 4, 8, 4, 8, 12, 16 };
// The offset was already being walked to size the stride; record it so
// hit shaders can actually read CUSTOM0-3 (shaders referencing them
// previously failed to compile on 'custom0_attrib' undeclared).
geom.custom_packed[ci] = (attrib_offset & 0x00FFFFFFu) | (fmt << 24);
attrib_offset += fmtsize[fmt];
}
}
geom.attribute_stride = attrib_offset;
// UV scale (fp16 packed, matches GLSL unpackHalf2x16)
Vector4 uv_scale = mesh_storage->mesh_surface_get_uv_scale(p_mesh_surface);
geom.uv_scale_packed = (uint32_t(Math::make_half_float(uv_scale.y)) << 16) | Math::make_half_float(uv_scale.x);
// Index format
if (index_buffer.is_valid() && index_count > 0) {
geom.index_buffer_address = RD::get_singleton()->buffer_get_device_address(index_buffer);
bool is_16bit = vertex_count <= 65536 && vertex_count > 0;
geom.index_format = is_16bit ? RT_INDEX_FORMAT_UINT16 : RT_INDEX_FORMAT_UINT32;
geom.primitive_count = index_count / 3;
} else {
geom.index_buffer_address = 0;
geom.index_format = RT_INDEX_FORMAT_NONE;
geom.primitive_count = vertex_count / 3;
}
// Create BLAS using the new geometry-based API.
{
RD::DataFormat pos_format;
if (is_2d) {
pos_format = RD::DATA_FORMAT_R32G32_SFLOAT;
} else if (compressed) {
pos_format = RD::DATA_FORMAT_R16G16B16A16_UNORM;
} else {
pos_format = RD::DATA_FORMAT_R32G32B32_SFLOAT;
}
RD::AccelerationStructureGeometry as_geom;
// Type defaults to TYPE_TRIANGLES; set explicitly for clarity.
as_geom.type = RD::AccelerationStructureGeometry::TYPE_TRIANGLES;
as_geom.geometry.triangles.vertex_buffer = vertex_buffer;
as_geom.geometry.triangles.vertex_stride = position_stride;
as_geom.geometry.triangles.vertex_count = vertex_count;
as_geom.geometry.triangles.vertex_format = pos_format;
if (index_buffer.is_valid() && index_count > 0) {
as_geom.geometry.triangles.index_buffer = index_buffer;
as_geom.geometry.triangles.index_count = index_count;
}
BitField<RD::AccelerationStructureFlagBits> as_flags = p_prefer_fast_build
? RD::ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT
: RD::ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT;
if (p_allow_update) {
as_flags.set_flag(RD::ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT);
}
r_surf_data->blas = RD::get_singleton()->blas_create({ &as_geom, 1 }, as_flags);
if (!r_surf_data->blas.is_valid()) {
return;
}
RD::get_singleton()->set_resource_name(r_surf_data->blas,
String(p_vertex_buffer_override.is_valid() ? "RT BLAS deformed [" : "RT BLAS [") + itos(p_cache_key) + "]");
r_dirty_blas_list.push_back(r_surf_data->blas);
}
}
// ---------------------------------------------------------------------------
// Uniform packing (file-local helpers)
// ---------------------------------------------------------------------------
static float _def_real(const ShaderLanguage::ShaderNode::Uniform &u, int idx) {
return (int)u.default_value.size() > idx ? u.default_value[idx].real : 0.0f;
}
static int32_t _def_sint(const ShaderLanguage::ShaderNode::Uniform &u, int idx) {
return (int)u.default_value.size() > idx ? u.default_value[idx].sint : 0;
}
static uint32_t _def_uint(const ShaderLanguage::ShaderNode::Uniform &u, int idx) {
return (int)u.default_value.size() > idx ? u.default_value[idx].uint : 0u;
}
static uint32_t _def_bool(const ShaderLanguage::ShaderNode::Uniform &u, int idx) {
return (int)u.default_value.size() > idx ? (uint32_t)u.default_value[idx].boolean : 0u;
}
static void pack_uniform(const ShaderLanguage::ShaderNode::Uniform &u, const Variant &val, uint8_t *dst) {
using SL = ShaderLanguage;
switch (u.type) {
case SL::TYPE_FLOAT: {
float v = val.get_type() == Variant::FLOAT ? (float)(double)val : _def_real(u, 0);
memcpy(dst, &v, 4);
} break;
case SL::TYPE_INT: {
int32_t v = val.get_type() == Variant::INT ? (int32_t)(int64_t)val : _def_sint(u, 0);
memcpy(dst, &v, 4);
} break;
case SL::TYPE_UINT: {
uint32_t v = val.get_type() == Variant::INT ? (uint32_t)(int64_t)val : _def_uint(u, 0);
memcpy(dst, &v, 4);
} break;
case SL::TYPE_BOOL: {
uint32_t v = val.get_type() == Variant::BOOL ? (uint32_t)(bool)val : _def_bool(u, 0);
memcpy(dst, &v, 4);
} break;
case SL::TYPE_VEC2: {
float fv[2];
if (val.get_type() == Variant::VECTOR2) {
Vector2 v = val;
fv[0] = (float)v.x;
fv[1] = (float)v.y;
} else {
fv[0] = _def_real(u, 0);
fv[1] = _def_real(u, 1);
}
memcpy(dst, fv, 8);
} break;
case SL::TYPE_VEC3: {
float fv[3] = {};
if (val.get_type() == Variant::VECTOR3) {
Vector3 v = val;
fv[0] = (float)v.x;
fv[1] = (float)v.y;
fv[2] = (float)v.z;
} else if (val.get_type() == Variant::COLOR) {
Color c = val;
if (u.hint == SL::ShaderNode::Uniform::HINT_SOURCE_COLOR) {
c = c.srgb_to_linear();
}
fv[0] = c.r;
fv[1] = c.g;
fv[2] = c.b;
} else {
fv[0] = _def_real(u, 0);
fv[1] = _def_real(u, 1);
fv[2] = _def_real(u, 2);
}
memcpy(dst, fv, 12);
} break;
case SL::TYPE_VEC4: {
float fv[4] = {};
if (val.get_type() == Variant::COLOR) {
Color c = val;
if (u.hint == SL::ShaderNode::Uniform::HINT_SOURCE_COLOR) {
c = c.srgb_to_linear();
}
fv[0] = c.r;
fv[1] = c.g;
fv[2] = c.b;
fv[3] = c.a;
} else if (val.get_type() == Variant::VECTOR4) {
Vector4 v = val;
fv[0] = (float)v.x;
fv[1] = (float)v.y;
fv[2] = (float)v.z;
fv[3] = (float)v.w;
} else {
for (int i = 0; i < 4; i++) {
fv[i] = _def_real(u, i);
}
}
memcpy(dst, fv, 16);
} break;
case SL::TYPE_IVEC2: {
int32_t iv[2];
if (val.get_type() == Variant::VECTOR2I) {
Vector2i v = val;
iv[0] = v.x;
iv[1] = v.y;
} else {
iv[0] = _def_sint(u, 0);
iv[1] = _def_sint(u, 1);
}
memcpy(dst, iv, 8);
} break;
case SL::TYPE_IVEC3: {
int32_t iv[3] = {};
if (val.get_type() == Variant::VECTOR3I) {
Vector3i v = val;
iv[0] = v.x;
iv[1] = v.y;
iv[2] = v.z;
} else {
for (int i = 0; i < 3; i++) {
iv[i] = _def_sint(u, i);
}
}
memcpy(dst, iv, 12);
} break;
case SL::TYPE_IVEC4: {
int32_t iv[4] = {};
if (val.get_type() == Variant::VECTOR4I) {
Vector4i v = val;
iv[0] = v.x;
iv[1] = v.y;
iv[2] = v.z;
iv[3] = v.w;
} else {
for (int i = 0; i < 4; i++) {
iv[i] = _def_sint(u, i);
}
}
memcpy(dst, iv, 16);
} break;
case SL::TYPE_UVEC2: {
uint32_t uv[2];
if (val.get_type() == Variant::VECTOR2I) {
Vector2i v = val;
uv[0] = (uint32_t)v.x;
uv[1] = (uint32_t)v.y;
} else {
uv[0] = _def_uint(u, 0);
uv[1] = _def_uint(u, 1);
}
memcpy(dst, uv, 8);
} break;
case SL::TYPE_UVEC3: {
uint32_t uv[3] = {};
if (val.get_type() == Variant::VECTOR3I) {
Vector3i v = val;
uv[0] = (uint32_t)v.x;
uv[1] = (uint32_t)v.y;
uv[2] = (uint32_t)v.z;
} else {
for (int i = 0; i < 3; i++) {
uv[i] = _def_uint(u, i);
}
}
memcpy(dst, uv, 12);
} break;
case SL::TYPE_UVEC4: {
uint32_t uv[4] = {};
if (val.get_type() == Variant::VECTOR4I) {
Vector4i v = val;
uv[0] = (uint32_t)v.x;
uv[1] = (uint32_t)v.y;
uv[2] = (uint32_t)v.z;
uv[3] = (uint32_t)v.w;
} else {
for (int i = 0; i < 4; i++) {
uv[i] = _def_uint(u, i);
}
}
memcpy(dst, uv, 16);
} break;
case SL::TYPE_BVEC2: {
uint32_t bv[2] = { _def_bool(u, 0), _def_bool(u, 1) };
memcpy(dst, bv, 8);
} break;
case SL::TYPE_BVEC3: {
uint32_t bv[3] = { _def_bool(u, 0), _def_bool(u, 1), _def_bool(u, 2) };
memcpy(dst, bv, 12);
} break;
case SL::TYPE_BVEC4: {
uint32_t bv[4] = { _def_bool(u, 0), _def_bool(u, 1), _def_bool(u, 2), _def_bool(u, 3) };
memcpy(dst, bv, 16);
} break;
case SL::TYPE_MAT2: {
// std140: mat2 = 2 column vec2s, each padded to vec4 (2x16 = 32 bytes).
float m[8] = {};
if (val.get_type() == Variant::TRANSFORM2D) {
Transform2D t = val;
m[0] = (float)t[0].x;
m[1] = (float)t[0].y;
m[4] = (float)t[1].x;
m[5] = (float)t[1].y;
} else {
for (int i = 0; i < 4; i++) {
m[(i / 2) * 4 + (i % 2)] = _def_real(u, i);
}
}
memcpy(dst, m, 32);
} break;
case SL::TYPE_MAT3: {
// std140: mat3 = 3 column vec3s, each padded to vec4 (3x16 = 48 bytes).
float m[12] = {};
if (val.get_type() == Variant::BASIS) {
Basis b = val;
for (int col = 0; col < 3; col++) {
Vector3 c = b.get_column(col);
m[col * 4 + 0] = (float)c.x;
m[col * 4 + 1] = (float)c.y;
m[col * 4 + 2] = (float)c.z;
}
} else {
for (int i = 0; i < 9; i++) {
m[(i / 3) * 4 + (i % 3)] = _def_real(u, i);
}
}
memcpy(dst, m, 48);
} break;
case SL::TYPE_MAT4: {
// std140: mat4 = 4 column vec4s (4x16 = 64 bytes).
float m[16] = {};
if (val.get_type() == Variant::PROJECTION) {
Projection p = val;
for (int col = 0; col < 4; col++) {
m[col * 4 + 0] = (float)p.columns[col].x;
m[col * 4 + 1] = (float)p.columns[col].y;
m[col * 4 + 2] = (float)p.columns[col].z;
m[col * 4 + 3] = (float)p.columns[col].w;
}
} else if (val.get_type() == Variant::TRANSFORM3D) {
Transform3D t = val;
Projection p(t);
for (int col = 0; col < 4; col++) {
m[col * 4 + 0] = (float)p.columns[col].x;
m[col * 4 + 1] = (float)p.columns[col].y;
m[col * 4 + 2] = (float)p.columns[col].z;
m[col * 4 + 3] = (float)p.columns[col].w;
}
} else {
for (int i = 0; i < 16; i++) {
m[i] = _def_real(u, i);
}
}
memcpy(dst, m, 64);
} break;
default:
break;
}
}
// ---------------------------------------------------------------------------
// Procedural geometry processing
// ---------------------------------------------------------------------------
void RenderRaytracing::update_procedural_blas(RTProceduralState *p_state, LocalVector<RID> &r_dirty_blas_list) {
// Pack AABB data into a byte buffer.
Vector<uint8_t> aabb_bytes;
uint32_t aabb_count = 1;
if (p_state->aabb_data.size() >= 6 && (p_state->aabb_data.size() % 6) == 0) {
aabb_count = p_state->aabb_data.size() / 6;
aabb_bytes.resize(p_state->aabb_data.size() * sizeof(float));
memcpy(aabb_bytes.ptrw(), p_state->aabb_data.ptr(), aabb_bytes.size());
} else {
const AABB &a = p_state->culling_aabb;
float single[6] = {
(float)a.position.x, (float)a.position.y, (float)a.position.z,
(float)(a.position.x + a.size.x), (float)(a.position.y + a.size.y), (float)(a.position.z + a.size.z)
};
aabb_bytes.resize(sizeof(single));
memcpy(aabb_bytes.ptrw(), single, sizeof(single));
}
uint32_t required_bytes = aabb_bytes.size();
bool needs_new_blas = false;
// Grow-only: only recreate the buffer when capacity is exceeded or count changed.
if (required_bytes > p_state->gpu_buffer_capacity || aabb_count != p_state->aabb_count) {
rt_free_blas_if_owned(p_state->blas);
if (p_state->gpu_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_state->gpu_buffer);
}
p_state->gpu_buffer = RD::get_singleton()->storage_buffer_create(required_bytes, aabb_bytes,
0, RD::BUFFER_CREATION_DEVICE_ADDRESS_BIT | RD::BUFFER_CREATION_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT);
p_state->gpu_buffer_capacity = required_bytes;
p_state->aabb_count = aabb_count;
needs_new_blas = true;
} else {
// Buffer is large enough -- just update contents.
RD::get_singleton()->buffer_update(p_state->gpu_buffer, 0, required_bytes, aabb_bytes.ptr());
needs_new_blas = !p_state->blas.is_valid();
}
if (needs_new_blas) {
ERR_FAIL_COND(!p_state->gpu_buffer.is_valid());
RD::AccelerationStructureGeometry geom;
geom.type = RD::AccelerationStructureGeometry::TYPE_AABBS;
geom.geometry.aabbs.buffer = p_state->gpu_buffer;
geom.geometry.aabbs.count = aabb_count;
geom.geometry.aabbs.stride = 24; // VkAabbPositionsKHR: two float3 (min, max).
p_state->blas = RD::get_singleton()->blas_create({ &geom, 1 }, RD::ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT);
}
// BDA for shader access.
if (p_state->expose_bounds && p_state->gpu_buffer.is_valid()) {
p_state->gpu_buffer_address = RD::get_singleton()->buffer_get_device_address(p_state->gpu_buffer);
} else {
p_state->gpu_buffer_address = 0;
}
if (p_state->blas.is_valid()) {
r_dirty_blas_list.push_back(p_state->blas);
}
}
// ---------------------------------------------------------------------------
// Material processing
// ---------------------------------------------------------------------------
RTMaterialData *RenderRaytracing::process_material(RID p_material_rid, uint16_t p_material_invalidation_counter) {
// Static default material for invalid/null materials
static RTMaterialData s_default_mat;
static bool s_default_mat_initialized = false;
if (!s_default_mat_initialized) {
s_default_mat.data.albedo_color[0] = 1.0f;
s_default_mat.data.albedo_color[1] = 1.0f;
s_default_mat.data.albedo_color[2] = 1.0f;
s_default_mat.data.albedo_color[3] = 1.0f;
s_default_mat.data.emission_color[0] = 0.0f;
s_default_mat.data.emission_color[1] = 0.0f;
s_default_mat.data.emission_color[2] = 0.0f;
s_default_mat.data.emission_strength = 0.0f;
s_default_mat.data.roughness = 1.0f;
s_default_mat.data.specular = 0.5f;
s_default_mat.data.ao_strength = 1.0f;
s_default_mat.data.uv1_scale[0] = 1.0f;
s_default_mat.data.uv1_scale[1] = 1.0f;
s_default_mat.data.uv1_offset[0] = 0.0f;
s_default_mat.data.uv1_offset[1] = 0.0f;
s_default_mat_initialized = true;
}
if (!p_material_rid.is_valid()) {
return &s_default_mat;
}
// Cache lookup
uint32_t mat_idx = get_rid_index(p_material_rid);
uint32_t mat_version = get_rid_version(p_material_rid);
RTMaterialCacheEntry *entry = get_material_cache_entry(mat_idx);
uint32_t current_frame = RSG::rasterizer->get_frame_number();
bool needs_refresh = !entry->ptr ||
entry->cached_rid_version != mat_version ||
entry->cached_counter != p_material_invalidation_counter;
if (!needs_refresh) {
entry->last_used_frame = current_frame;
if (entry->ptr->is_custom_shader) {
uint32_t shader_id = RendererRD::MaterialStorage::get_singleton()->material_get_shader_id(p_material_rid);
uint32_t old_sbt = entry->ptr->rt_sbt_offset;
uint32_t new_sbt = SceneShaderRaytracing::get_singleton()->register_custom_shader(shader_id, p_material_rid);
entry->ptr->rt_sbt_offset = new_sbt;
// HG slot change invalidates cached UBO layout / BDA.
if (old_sbt != new_sbt) {
needs_refresh = true;
}
}
if (!needs_refresh) {
return entry->ptr;
}
}
// Cache miss - need to rebuild material
if (!entry->ptr) {
entry->ptr = memnew(RTMaterialData);
}
RTMaterialData *mat_data = entry->ptr;
RT_MaterialData &mat = mat_data->data;
// Initialize defaults
mat.albedo_color[0] = 1.0f;
mat.albedo_color[1] = 1.0f;
mat.albedo_color[2] = 1.0f;
mat.albedo_color[3] = 1.0f;
mat.emission_color[0] = 0.0f;
mat.emission_color[1] = 0.0f;
mat.emission_color[2] = 0.0f;
mat.emission_strength = 0.0f;
mat.metallic = 0.0f;
mat.roughness = 1.0f;
mat.specular = 0.5f;
mat.ao_strength = 1.0f;
mat.flags = 0;
mat.albedo_texture_idx = 0;
mat.normal_texture_idx = 0;
mat.orm_texture_idx = 0;
mat.emission_texture_idx = 0;
mat.uv1_scale[0] = 1.0f;
mat.uv1_scale[1] = 1.0f;
mat.uv1_offset[0] = 0.0f;
mat.uv1_offset[1] = 0.0f;
mat.normal_map_depth = 1.0f;
mat.uniform_address = 0;
RendererRD::MaterialStorage *material_storage = RendererRD::MaterialStorage::get_singleton();
RendererRD::TextureStorage *texture_storage = RendererRD::TextureStorage::get_singleton();
// Helper lambda to get texture from material parameter
// p_srgb should be true for color textures (albedo, emission) that need sRGB->linear conversion
auto get_material_texture = [&](const StringName &p_param, bool p_srgb = false) -> RID {
Variant tex_var = material_storage->material_get_param(p_material_rid, p_param);
if (tex_var.get_type() == Variant::OBJECT || tex_var.get_type() == Variant::RID) {
RID tex_rid = tex_var;
if (tex_rid.is_valid()) {
return texture_storage->texture_get_rd_texture(tex_rid, p_srgb);
}
}
return RID();
};
// Textures
// Albedo is a color texture - needs sRGB->linear conversion
RID albedo_rd = get_material_texture("texture_albedo", true);
if (albedo_rd.is_valid()) {
mat.albedo_texture_idx = _bindless(BindlessBlock::TEXTURE_2D)->add_texture(albedo_rd);
}
RID normal_rd = get_material_texture("texture_normal");
if (normal_rd.is_valid()) {
mat.normal_texture_idx = _bindless(BindlessBlock::TEXTURE_2D)->add_texture(normal_rd);
mat.flags |= RT_MAT_FLAG_HAS_NORMAL_MAP;
Variant normal_scale_var = material_storage->material_get_param(p_material_rid, "normal_scale");
if (normal_scale_var.get_type() == Variant::FLOAT) {
mat.normal_map_depth = normal_scale_var;
}
}
RID orm_rd = get_material_texture("texture_orm");
if (orm_rd.is_valid()) {
mat.orm_texture_idx = _bindless(BindlessBlock::TEXTURE_2D)->add_texture(orm_rd);
} else {
RID roughness_rd = get_material_texture("texture_roughness");
if (roughness_rd.is_valid()) {
mat.orm_texture_idx = _bindless(BindlessBlock::TEXTURE_2D)->add_texture(roughness_rd);
}
}
// Emission is a color texture - needs sRGB->linear conversion
RID emission_rd = get_material_texture("texture_emission", true);
if (emission_rd.is_valid()) {
mat.emission_texture_idx = _bindless(BindlessBlock::TEXTURE_2D)->add_texture(emission_rd);
mat.flags |= RT_MAT_FLAG_HAS_EMISSION_TEX;
// Set sensible defaults for emission when texture is present
mat.emission_color[0] = 1.0f;
mat.emission_color[1] = 1.0f;
mat.emission_color[2] = 1.0f;
mat.emission_strength = 1.0f;
}
// Material properties
// Colors declared with source_color in Godot shaders are stored in sRGB;
// material_get_param returns the raw sRGB value, so we convert to linear here.
Variant albedo_var = material_storage->material_get_param(p_material_rid, "albedo");
if (albedo_var.get_type() == Variant::COLOR) {
Color c = ((Color)albedo_var).srgb_to_linear();
mat.albedo_color[0] = c.r;
mat.albedo_color[1] = c.g;
mat.albedo_color[2] = c.b;
mat.albedo_color[3] = c.a;
mat_data->rt_sbt_offset = 0;
mat_data->is_custom_shader = false;
} else {
mat_data->is_custom_shader = true;
uint32_t shader_id = material_storage->material_get_shader_id(p_material_rid);
mat_data->rt_sbt_offset = SceneShaderRaytracing::get_singleton()->register_custom_shader(shader_id, p_material_rid);
const SceneShaderRaytracing::CustomShaderEntry *cse =
SceneShaderRaytracing::get_singleton()->get_custom_shader_entry(mat_data->rt_sbt_offset);
if (cse && cse->uniform_total_size > 0) {
Vector<uint8_t> ubo_data;
ubo_data.resize(cse->uniform_total_size);
memset(ubo_data.ptrw(), 0, cse->uniform_total_size);
for (const KeyValue<StringName, ShaderLanguage::ShaderNode::Uniform> &kv : cse->uniforms) {
const ShaderLanguage::ShaderNode::Uniform &u = kv.value;
if (ShaderLanguage::is_sampler_type(u.type)) {
continue;
}
if (u.order < 0 || u.order >= (int)cse->uniform_offsets.size()) {
continue;
}
uint32_t offset = cse->uniform_offsets[u.order];
uint32_t size = ShaderLanguage::get_datatype_size(u.type);
if (offset + size > cse->uniform_total_size) {
continue;
}
uint8_t *dst = ubo_data.ptrw() + offset;
if (u.scope == ShaderLanguage::ShaderNode::Uniform::SCOPE_GLOBAL) {
int32_t idx = material_storage->global_shader_uniform_get_buffer_index(kv.key);
uint32_t uidx = (idx >= 0) ? (uint32_t)idx : 0;
memcpy(dst, &uidx, sizeof(uint32_t));
} else {
Variant val = material_storage->material_get_param(p_material_rid, kv.key);
pack_uniform(u, val, dst);
}
}
RendererRD::TextureStorage *ts = RendererRD::TextureStorage::get_singleton();
for (int ti = 0; ti < cse->texture_uniforms.size(); ti++) {
const SceneShaderRaytracing::TextureUniformInfo &tui = cse->texture_uniforms[ti];
uint32_t bindless_idx = 0;
// The index is looked up in the table matching the uniform's sampler
// type; the generated GLSL indexes that same table (see
// SceneShaderRaytracing tex_defines), so the two must agree.
const BindlessBlock::TextureKind kind = _texture_kind_for_type(tui.type);
BindlessBlock *block = _bindless(kind);
if (tui.is_global) {
RID tex_rid = material_storage->global_shader_uniform_get_texture(tui.name);
if (tex_rid.is_valid()) {
RID rd_tex = ts->texture_get_rd_texture(tex_rid, tui.use_color);
if (rd_tex.is_valid()) {
bindless_idx = block->add_texture(rd_tex);
}
}
} else {
Variant tex_var = material_storage->material_get_param(p_material_rid, tui.name);
if (tex_var.get_type() == Variant::OBJECT || tex_var.get_type() == Variant::RID) {
RID tex_rid = tex_var;
if (tex_rid.is_valid()) {
RID rd_tex = ts->texture_get_rd_texture(tex_rid, tui.use_color);
if (rd_tex.is_valid()) {
bindless_idx = block->add_texture(rd_tex);
}
}
}
}
// The hint defaults below are all 2D textures. For any other kind,
// index 0 already IS that table's default of the correct type.
if (bindless_idx == 0 && kind == BindlessBlock::TEXTURE_2D &&
tui.hint != ShaderLanguage::ShaderNode::Uniform::HINT_NONE) {
using Hint = ShaderLanguage::ShaderNode::Uniform::Hint;
RID default_tex;
switch (tui.hint) {
case Hint::HINT_DEFAULT_BLACK:
default_tex = ts->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_BLACK);
break;
case Hint::HINT_DEFAULT_TRANSPARENT:
default_tex = ts->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_TRANSPARENT);
break;
case Hint::HINT_NORMAL:
default_tex = ts->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_NORMAL);
break;
case Hint::HINT_ANISOTROPY:
default_tex = ts->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_ANISO);
break;
default:
default_tex = ts->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_WHITE);
break;
}
if (default_tex.is_valid()) {
bindless_idx = block->add_texture(default_tex);
}
}
if (tui.buffer_offset + 4 <= cse->uniform_total_size) {
memcpy(ubo_data.ptrw() + tui.buffer_offset, &bindless_idx, 4);
}
}
// Try the suballoc pool first. Common materials (UBO <= slot size)
// just buffer_update an existing slot - O(1), no driver allocation,
// no per-frame storage_buffer_create cost.
bool used_pool = false;
if (cse->uniform_total_size <= MAT_UBO_POOL_SLOT_SIZE) {
if (mat_data->uniform_pool_slot == UINT32_MAX) {
mat_data->uniform_pool_slot = mat_ubo_pool_allocate();
}
if (mat_data->uniform_pool_slot != UINT32_MAX) {
// Transitioning from a dedicated buffer back into the pool.
if (mat_data->uniform_buffer.is_valid()) {
RD::get_singleton()->free_rid(mat_data->uniform_buffer);
mat_data->uniform_buffer = RID();
}
mat_ubo_pool_update(mat_data->uniform_pool_slot, ubo_data.ptr(), cse->uniform_total_size);
mat.uniform_address = mat_ubo_pool_get_address(mat_data->uniform_pool_slot);
used_pool = true;
}
}
if (!used_pool) {
// Oversized or pool exhausted: dedicated per-material buffer.
// This is the slow path: a per-material storage_buffer_create on
// every rebuild. Warn once so it's visible in the log; the fix is
// either to shrink the material's uniform footprint below
// MAT_UBO_POOL_SLOT_SIZE or to grow the pool slot/capacity.
const char *reason = (cse->uniform_total_size > MAT_UBO_POOL_SLOT_SIZE)
? "uniform size exceeds slot"
: "pool exhausted";
WARN_PRINT_ONCE(vformat(
"RT Material UBO falling back to dedicated buffer (%s): "
"sbt_offset=%u, uniform_total_size=%u, slot_size=%u.",
String(reason), mat_data->rt_sbt_offset,
cse->uniform_total_size, MAT_UBO_POOL_SLOT_SIZE));
if (mat_data->uniform_pool_slot != UINT32_MAX) {
mat_ubo_pool_release(mat_data->uniform_pool_slot);
mat_data->uniform_pool_slot = UINT32_MAX;
}
if (mat_data->uniform_buffer.is_valid()) {
RD::get_singleton()->free_rid(mat_data->uniform_buffer);
}
mat_data->uniform_buffer = RD::get_singleton()->storage_buffer_create(cse->uniform_total_size, ubo_data, 0, RD::BUFFER_CREATION_DEVICE_ADDRESS_BIT);
RD::get_singleton()->set_resource_name(mat_data->uniform_buffer, String("RT Material UBO [sbt=") + itos(mat_data->rt_sbt_offset) + "]");
mat.uniform_address = RD::get_singleton()->buffer_get_device_address(mat_data->uniform_buffer);
}
}
}
Variant metallic_var = material_storage->material_get_param(p_material_rid, "metallic");
if (metallic_var.get_type() == Variant::FLOAT) {
mat.metallic = metallic_var;
}
Variant roughness_var = material_storage->material_get_param(p_material_rid, "roughness");
if (roughness_var.get_type() == Variant::FLOAT) {
mat.roughness = roughness_var;
}
Variant specular_var = material_storage->material_get_param(p_material_rid, "specular");
if (specular_var.get_type() == Variant::FLOAT) {
mat.specular = specular_var;
}
Variant emission_var = material_storage->material_get_param(p_material_rid, "emission");
if (emission_var.get_type() == Variant::COLOR) {
Color c = ((Color)emission_var).srgb_to_linear();
mat.emission_color[0] = c.r;
mat.emission_color[1] = c.g;
mat.emission_color[2] = c.b;
}
Variant emission_energy_var = material_storage->material_get_param(p_material_rid, "emission_energy");
if (emission_energy_var.get_type() == Variant::FLOAT) {
mat.emission_strength = emission_energy_var;
}
// UV1 scale and offset (vec3 in Godot, we only use xy).
Variant uv1_scale_var = material_storage->material_get_param(p_material_rid, "uv1_scale");
if (uv1_scale_var.get_type() == Variant::VECTOR3) {
Vector3 s = uv1_scale_var;
mat.uv1_scale[0] = s.x;
mat.uv1_scale[1] = s.y;
}
Variant uv1_offset_var = material_storage->material_get_param(p_material_rid, "uv1_offset");
if (uv1_offset_var.get_type() == Variant::VECTOR3) {
Vector3 o = uv1_offset_var;
mat.uv1_offset[0] = o.x;
mat.uv1_offset[1] = o.y;
}
// Point filtering: check if material requests nearest filtering (e.g. pixel art).
// BaseMaterial3D exposes this as "texture_filter" int param (0=nearest, 1=linear, etc.).
Variant filter_var = material_storage->material_get_param(p_material_rid, "texture_filter");
if (filter_var.get_type() == Variant::INT) {
int filter_mode = filter_var;
// 0 = TEXTURE_FILTER_NEAREST, 2 = TEXTURE_FILTER_NEAREST_WITH_MIPMAPS,
// 4 = TEXTURE_FILTER_NEAREST_WITH_MIPMAPS_ANISOTROPIC
if (filter_mode == 0 || filter_mode == 2 || filter_mode == 4) {
mat.flags |= RT_MAT_FLAG_POINT_FILTER;
}
}
// Update cache entry
entry->cached_counter = p_material_invalidation_counter;
entry->cached_rid_version = mat_version;
entry->last_used_frame = current_frame;
return mat_data;
}
// ---------------------------------------------------------------------------
// Acceleration structure building
// ---------------------------------------------------------------------------
void RenderRaytracing::build_acceleration_structures(RTViewportState *p_state, const LocalVector<RID> &p_dirty_blas_list, const LocalVector<RID> &p_dirty_blas_update_list) {
for (const RID &blas_rid : p_dirty_blas_list) {
if (blas_rid.is_valid()) {
RD::get_singleton()->blas_build(blas_rid);
}
}
for (const RID &blas_rid : p_dirty_blas_update_list) {
if (blas_rid.is_valid()) {
RD::get_singleton()->blas_update(blas_rid);
}
}
uint32_t needed = MAX(blass.size(), (uint32_t)1);
if (!p_state->tlas.is_valid() || needed > p_state->tlas_max_instances) {
if (p_state->tlas.is_valid()) {
RD::get_singleton()->free_rid(p_state->tlas);
}
p_state->tlas_max_instances = needed * 2;
p_state->tlas = RD::get_singleton()->tlas_create(p_state->tlas_max_instances, RD::ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT);
RD::get_singleton()->set_resource_name(p_state->tlas, "RT TLAS");
}
LocalVector<RD::AccelerationStructureInstance> instances;
instances.resize(blass.size());
for (uint32_t i = 0; i < blass.size(); i++) {
RD::AccelerationStructureInstance &inst = instances[i];
inst.id = i;
inst.transform = blas_transforms[i];
inst.blas = blass[i];
inst.flags = BitField<RD::AccelerationStructureInstanceFlagBits>(instance_flags[i]);
uint32_t sbt_off = (i < sbt_offsets.size()) ? sbt_offsets[i] : 0;
inst.hit_sbt_range = RD::HitShaderBindingTableRange((1ULL << 32) | uint64_t(sbt_off));
}
RD::get_singleton()->tlas_build(p_state->tlas, instances);
}
void RenderRaytracing::finalize_buffers(RTViewportState *p_state) {
// Grow-only uploads. Callers must not free these in prepare_frame().
auto update_or_grow = [](RID &p_buffer, uint32_t &p_capacity, const void *p_data, uint32_t p_size) {
if (p_size == 0) {
return;
}
if (p_size > p_capacity) {
if (p_buffer.is_valid()) {
RD::get_singleton()->free_rid(p_buffer);
}
p_capacity = p_size;
Vector<uint8_t> init;
init.resize(p_size);
memcpy(init.ptrw(), p_data, p_size);
p_buffer = RD::get_singleton()->storage_buffer_create(p_size, init);
} else {
RD::get_singleton()->buffer_update(p_buffer, 0, p_size, p_data);
}
};
update_or_grow(p_state->geometry_buffer, p_state->geometry_buffer_capacity,
geometry_data.ptr(), geometry_data.size() * sizeof(RT_GeometryData));
update_or_grow(p_state->material_buffer, p_state->material_buffer_capacity,
material_data.ptr(), material_data.size() * sizeof(RT_MaterialData));
update_or_grow(p_state->motion_index_buffer, p_state->motion_index_buffer_capacity,
motion_indices.ptr(), motion_indices.size() * sizeof(int32_t));
update_or_grow(p_state->motion_transform_buffer, p_state->motion_transform_buffer_capacity,
motion_transforms.ptr(), motion_transforms.size() * sizeof(RT_InstanceMotionData));
}
// ---------------------------------------------------------------------------
// TLAS creation (main entry point per frame)
// ---------------------------------------------------------------------------
_FORCE_INLINE_ static uint32_t _rt_indices_to_primitives(RSE::PrimitiveType p_primitive, uint32_t p_indices) {
static const uint32_t divisor[RSE::PRIMITIVE_MAX] = { 1, 2, 1, 3, 1 };
static const uint32_t subtractor[RSE::PRIMITIVE_MAX] = { 0, 0, 1, 0, 2 };
return (p_indices - subtractor[p_primitive]) / divisor[p_primitive];
}
RTViewportState *RenderRaytracing::build_tlas(const RenderDataRD *p_render_data, uint32_t p_rt_flags) {
if (!p_render_data || !p_render_data->rt_instances) {
return nullptr;
}
RTViewportState *state = _get_or_create_viewport_state(p_render_data);
if (!state) {
return nullptr;
}
prepare_frame();
// Builds bundle if needed; live_ready_mask drives TLAS inclusion below.
SceneShaderRaytracing *rt_shader_singleton = SceneShaderRaytracing::get_singleton();
rt_shader_singleton->ensure_pipeline_bundle(p_rt_flags);
RendererRD::MeshStorage *mesh_storage = RendererRD::MeshStorage::get_singleton();
RendererRD::MaterialStorage *material_storage = RendererRD::MaterialStorage::get_singleton();
LocalVector<RID> dirty_blas_list;
LocalVector<RID> dirty_blas_update_list;
#ifdef TOOLS_ENABLED
uint32_t tlas_instance_count = 0;
uint32_t tlas_primitive_count = 0;
const bool collect_render_info = (p_render_data->render_info != nullptr);
#endif
// Process all AABB-culled geometry instances (superset of frustum).
const PagedArray<RenderGeometryInstance *> &rt_instances = *p_render_data->rt_instances;
for (uint32_t i = 0; i < (uint32_t)rt_instances.size(); i++) {
const RenderForwardClustered::GeometryInstanceForwardClustered *inst =
static_cast<const RenderForwardClustered::GeometryInstanceForwardClustered *>(rt_instances[i]);
if (!inst || !inst->data) {
continue;
}
const Transform3D &instance_transform = inst->transform;
// Determine previous-frame transform for motion vectors.
const Transform3D &prev_instance_transform =
(inst->transform_status == RenderForwardClustered::GeometryInstanceForwardClustered::TransformStatus::TELEPORTED)
? inst->transform
: inst->prev_transform;
// Handle procedural RT instances (intersection shaders).
if (inst->rt_procedural) {
SceneShaderRaytracing *rt_shader = SceneShaderRaytracing::get_singleton();
RTProceduralState *ps = inst->rt_procedural;
// Intersection code comes from ShaderMaterial on material_override.
if (!inst->data || !inst->data->material_override.is_valid()) {
continue;
}
RID proc_material_rid = inst->data->material_override;
uint32_t shader_id = material_storage->material_get_shader_id(proc_material_rid);
if (shader_id == 0) {
continue;
}
uint32_t hg_index = rt_shader->register_procedural_shader(shader_id, proc_material_rid);
if (hg_index == 0) {
continue;
}
if (!rt_shader->is_hg_ready_in_bundle(hg_index, p_rt_flags)) {
continue;
}
if (ps->dirty) {
update_procedural_blas(ps, dirty_blas_list);
ps->dirty = false;
}
if (ps->blas.is_valid()) {
blass.push_back(ps->blas);
blas_transforms.push_back(instance_transform);
sbt_offsets.push_back(hg_index);
RT_GeometryData geom = {};
geom.flags = RT_GEOM_FLAG_PROCEDURAL;
geom.vertex_buffer_address = ps->gpu_buffer_address;
geom.aabb_size_x = (float)ps->culling_aabb.size.x;
geom.aabb_size_y = (float)ps->culling_aabb.size.y;
geom.aabb_size_z = (float)ps->culling_aabb.size.z;
geometry_data.push_back(geom);
if (inst->transform_status == RenderForwardClustered::GeometryInstanceForwardClustered::TransformStatus::MOVED) {
motion_indices.push_back((int32_t)motion_transforms.size());
RT_InstanceMotionData motion = {};
RendererRD::MaterialStorage::store_transform_transposed_3x4(prev_instance_transform, motion.prev_object_to_world);
motion_transforms.push_back(motion);
} else {
motion_indices.push_back(-1);
}
// Material for procedural geometry (already validated above).
uint16_t proc_mat_counter = material_storage->material_get_rt_invalidation_counter(proc_material_rid);
RTMaterialData *proc_mat_data = process_material(proc_material_rid, proc_mat_counter);
material_data.push_back(proc_mat_data->data);
// Procedural instances disable triangle culling and are opaque.
uint32_t inst_flags = RD::ACCELERATION_STRUCTURE_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT |
RD::ACCELERATION_STRUCTURE_INSTANCE_FORCE_OPAQUE_BIT;
instance_flags.push_back(inst_flags);
}
continue;
}
// Walk the surface cache linked list.
const RenderForwardClustered::GeometryInstanceSurfaceDataCache *surf = inst->surface_caches;
bool instance_static = inst->transform_status == RenderForwardClustered::GeometryInstanceForwardClustered::TransformStatus::NONE;
while (surf) {
// Skip surfaces routed to the raster alpha overlay
if (surf->rt_pass_flags & RenderForwardClustered::GeometryInstanceSurfaceDataCache::FLAG_PASS_ALPHA) {
surf = surf->next;
continue;
}
void *mesh_surface = surf->surface;
uint32_t surface_counter = mesh_storage->mesh_surface_get_rt_invalidation_counter(mesh_surface);
// MeshInstance skinning/blend shapes provide a deformed vertex buffer.
RTSurfaceData *surf_data = nullptr;
if (inst->mesh_instance.is_valid()) {
RID curr_vb = mesh_storage->mesh_instance_get_vertex_buffer(inst->mesh_instance, surf->surface_index);
if (curr_vb.is_valid()) {
RTDeformedGeometrySource src;
src.current_vb = curr_vb;
src.prev_vb = mesh_storage->mesh_instance_get_prev_vertex_buffer(inst->mesh_instance, surf->surface_index);
src.change_stamp = mesh_storage->mesh_instance_get_last_change(inst->mesh_instance, surf->surface_index);
uint64_t mi_id = inst->mesh_instance.get_id();
uint32_t mi_index = static_cast<uint32_t>(mi_id & 0xFFFFFFFFULL);
src.cache_version = static_cast<uint32_t>(mi_id >> 32);
src.cache_key = (static_cast<uint64_t>(mi_index) << 16) | (surf->surface_index & 0xFFFFu);
src.surface_counter = surface_counter;
surf_data = process_deformed_surface(surf, mesh_surface, src, dirty_blas_list, dirty_blas_update_list);
}
}
if (!surf_data) {
surf_data = process_surface(surf, mesh_surface, surface_counter, instance_transform, dirty_blas_list);
}
if (!surf_data || !surf_data->blas.is_valid()) {
surf = surf->next;
continue;
}
// Resolve material before TLAS so we can skip surfaces whose HG is not live yet (override > surface > mesh).
RID material_rid;
if (surf->owner->data->material_override.is_valid()) {
material_rid = surf->owner->data->material_override;
} else if (surf->surface_index < surf->owner->data->surface_materials.size() &&
surf->owner->data->surface_materials[surf->surface_index].is_valid()) {
material_rid = surf->owner->data->surface_materials[surf->surface_index];
} else {
RID mesh_rid = surf->owner->data->base;
if (mesh_rid.is_valid() && mesh_storage->owns_mesh(mesh_rid)) {
material_rid = mesh_storage->mesh_surface_get_material(mesh_rid, surf->surface_index);
}
}
uint16_t material_counter = material_storage->material_get_rt_invalidation_counter(material_rid);
RTMaterialData *mat_data = process_material(material_rid, material_counter);
// Custom hit groups compile on a worker thread, so a surface's HG is
// typically not ready for the first frames after a material appears.
// Dropping the surface from the TLAS meanwhile makes the geometry
// INVISIBLE until the compile lands (seconds, for a large shader with
// several pipeline variants). Fall back to the default hit group
// instead: the surface renders immediately with standard material
// shading and upgrades to its own shader once that is ready.
//
// A permanently-failed HG (HGState::Failed) never becomes ready, so
// this also keeps such geometry visible rather than silently absent.
uint32_t sbt_offset = mat_data->rt_sbt_offset;
if (sbt_offset > 0 && !rt_shader_singleton->is_hg_ready_in_bundle(sbt_offset, p_rt_flags)) {
sbt_offset = 0;
}
// Compute or reuse cached final transform (instance * aabb_transform for compressed meshes).
Transform3D final_transform;
if (instance_static && surf->cached_final_transform_valid) {
final_transform = surf->cached_final_transform;
} else {
final_transform = instance_transform;
if (surf_data->is_compressed) {
final_transform = instance_transform * surf_data->aabb_transform;
}
surf->cached_final_transform = final_transform;
surf->cached_final_transform_valid = true;
}
blas_transforms.push_back(final_transform);
blass.push_back(surf_data->blas);
geometry_data.push_back(surf_data->geometry);
if (inst->transform_status == RenderForwardClustered::GeometryInstanceForwardClustered::TransformStatus::MOVED) {
motion_indices.push_back((int32_t)motion_transforms.size());
RT_InstanceMotionData motion = {};
Transform3D prev_final = prev_instance_transform;
if (surf_data->is_compressed) {
prev_final = prev_instance_transform * surf_data->aabb_transform;
}
RendererRD::MaterialStorage::store_transform_transposed_3x4(prev_final, motion.prev_object_to_world);
motion_transforms.push_back(motion);
} else {
motion_indices.push_back(-1);
}
#ifdef TOOLS_ENABLED
if (collect_render_info) {
tlas_instance_count++;
uint32_t vertices = mesh_storage->mesh_surface_get_vertices_drawn_count(mesh_surface);
tlas_primitive_count += _rt_indices_to_primitives(surf->primitive, vertices);
}
#endif
sbt_offsets.push_back(sbt_offset);
material_data.push_back(mat_data->data);
// Determine per-instance TLAS flags from material properties.
uint32_t inst_flags = 0;
if (surf->shader) {
switch (surf->shader->rt_cull_mode()) {
case RSE::CULL_MODE_DISABLED:
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT;
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_TRIANGLE_FLIP_FACING_BIT;
break;
case RSE::CULL_MODE_FRONT:
break;
case RSE::CULL_MODE_BACK:
default:
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_TRIANGLE_FLIP_FACING_BIT;
break;
}
} else {
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_TRIANGLE_FLIP_FACING_BIT;
}
// Uses the possibly-fallen-back offset: a surface shading through the
// default hit group must take the standard-material branch below.
if (sbt_offset > 0) {
// Custom shader: only enable any-hit if the shader uses alpha clip.
const SceneShaderRaytracing::CustomShaderEntry *cse =
rt_shader_singleton->get_custom_shader_entry(sbt_offset);
if (!cse || !cse->uses_alpha_clip) {
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_FORCE_OPAQUE_BIT;
}
} else {
// Standard material: FORCE_OPAQUE if no alpha usage.
bool is_alpha = surf->shader && (surf->shader->uses_alpha_clip || surf->shader->uses_blend_alpha || surf->shader->uses_alpha);
if (!is_alpha) {
inst_flags |= RD::ACCELERATION_STRUCTURE_INSTANCE_FORCE_OPAQUE_BIT;
}
}
instance_flags.push_back(inst_flags);
surf = surf->next;
}
}
#ifdef TOOLS_ENABLED
if (collect_render_info) {
p_render_data->render_info->info[RSE::VIEWPORT_RENDER_INFO_TYPE_VISIBLE][RSE::VIEWPORT_RENDER_INFO_OBJECTS_IN_FRAME] += tlas_instance_count;
p_render_data->render_info->info[RSE::VIEWPORT_RENDER_INFO_TYPE_VISIBLE][RSE::VIEWPORT_RENDER_INFO_PRIMITIVES_IN_FRAME] += tlas_primitive_count;
}
#endif
SceneShaderRaytracing::get_singleton()->finalize_custom_shaders();
build_acceleration_structures(state, dirty_blas_list, dirty_blas_update_list);
finalize_buffers(state);
return state;
}
// ---------------------------------------------------------------------------
// Light gathering
// ---------------------------------------------------------------------------
uint32_t RenderRaytracing::gather_lights(const RenderDataRD *p_render_data, RT_LightData *r_light_data, uint32_t p_max_lights) {
uint32_t rt_light_count = 0;
if (!p_render_data || !p_render_data->lights) {
return rt_light_count;
}
RendererRD::LightStorage *ls = RendererRD::LightStorage::get_singleton();
const Transform3D &cam_xform = p_render_data->scene_data->cam_transform;
const Vector3 cam_pos = cam_xform.origin;
// Compute light energy matching rasterizer conventions (light_storage.cpp).
// Applies PI multiplier (or physical-unit intensity), exposure, and negative sign.
auto compute_light_energy = [&](RID p_base, RSE::LightType p_type) -> float {
float sign = ls->light_is_negative(p_base) ? -1.0f : 1.0f;
float e = sign * ls->light_get_param(p_base, RSE::LIGHT_PARAM_ENERGY);
if (owner->is_using_physical_light_units()) {
e *= ls->light_get_param(p_base, RSE::LIGHT_PARAM_INTENSITY);
if (p_type == RSE::LIGHT_OMNI) {
e *= 1.0f / (Math::PI * 4.0f);
} else if (p_type == RSE::LIGHT_SPOT) {
e *= 1.0f / Math::PI;
}
} else {
e *= Math::PI;
}
if (p_render_data->camera_attributes.is_valid()) {
e *= RSG::camera_attributes->camera_attributes_get_exposure_normalization_factor(p_render_data->camera_attributes);
}
return e;
};
// Scoring helper: approximate power/solid-angle contribution.
struct LightScore {
RID light_instance;
float score;
};
LocalVector<LightScore> positional_lights;
// Helper: score a positional light and add to candidates.
auto score_positional_light = [&](RID light_instance) {
RID base = ls->light_instance_get_base_light(light_instance);
Transform3D xform = ls->light_instance_get_base_transform(light_instance);
Vector3 light_pos = xform.origin;
float dist_sq = cam_pos.distance_squared_to(light_pos);
Color color = ls->light_get_color(base);
float energy = ls->light_get_param(base, RSE::LIGHT_PARAM_ENERGY);
float lum = color.r * 0.2126f + color.g * 0.7152f + color.b * 0.0722f;
float score = (energy * lum) / MAX(dist_sq, 0.01f);
LightScore ls_entry = {};
ls_entry.light_instance = light_instance;
ls_entry.score = score;
positional_lights.push_back(ls_entry);
};
// Directional lights from the frustum-culled list (they're global, always included).
const PagedArray<RID> &lights = *p_render_data->lights;
for (uint32_t li = 0; li < (uint32_t)lights.size(); li++) {
RID light_instance = lights[li];
RID base = ls->light_instance_get_base_light(light_instance);
RSE::LightType type = ls->light_get_type(base);
if (type != RSE::LIGHT_DIRECTIONAL) {
continue;
}
if (rt_light_count >= p_max_lights) {
break;
}
RT_LightData &ld = r_light_data[rt_light_count];
Transform3D xform = ls->light_instance_get_base_transform(light_instance);
Vector3 dir = -xform.basis.get_column(2).normalized();
ld.position[0] = dir.x;
ld.position[1] = dir.y;
ld.position[2] = dir.z;
ld.type = RT_LIGHT_TYPE_DIRECTIONAL;
Color linear_col = ls->light_get_color(base).srgb_to_linear();
float energy = compute_light_energy(base, RSE::LIGHT_DIRECTIONAL);
ld.emission[0] = linear_col.r * energy;
ld.emission[1] = linear_col.g * energy;
ld.emission[2] = linear_col.b * energy;
ld.radius = Math::deg_to_rad(ls->light_get_param(base, RSE::LIGHT_PARAM_SIZE) * 0.5f); // Half-angle in radians.
ld.attenuation = 0.0f; // No distance attenuation.
ld.inv_max_range = -1.0f; // Infinite range.
ld.max_range_squared = 0.0f;
ld.specular_amount = ls->light_get_param(base, RSE::LIGHT_PARAM_SPECULAR);
ld.indirect_energy = ls->light_get_param(base, RSE::LIGHT_PARAM_INDIRECT_ENERGY);
ld.inv_spot_attenuation = 0.0f;
ld.cos_spot_angle = 0.0f;
ld.spot_direction[0] = 0.0f;
ld.spot_direction[1] = 0.0f;
ld.spot_direction[2] = 0.0f;
rt_light_count++;
}
// Positional lights from the AABB-culled RT list (superset of frustum).
if (p_render_data->rt_lights) {
const PagedArray<RID> &rt_lights = *p_render_data->rt_lights;
for (uint32_t li = 0; li < (uint32_t)rt_lights.size(); li++) {
score_positional_light(rt_lights[li]);
}
}
// Sort all positional lights by score descending.
struct LightScoreComparator {
bool operator()(const LightScore &a, const LightScore &b) const {
return a.score > b.score;
}
};
positional_lights.sort_custom<LightScoreComparator>();
// Fill remaining slots with top positional lights.
for (uint32_t i = 0; i < positional_lights.size() && rt_light_count < p_max_lights; i++) {
RID light_instance = positional_lights[i].light_instance;
RID base = ls->light_instance_get_base_light(light_instance);
RSE::LightType type = ls->light_get_type(base);
RT_LightData &ld = r_light_data[rt_light_count];
Transform3D xform = ls->light_instance_get_base_transform(light_instance);
ld.position[0] = xform.origin.x;
ld.position[1] = xform.origin.y;
ld.position[2] = xform.origin.z;
ld.type = (type == RSE::LIGHT_SPOT) ? RT_LIGHT_TYPE_SPOT : RT_LIGHT_TYPE_OMNI;
Color linear_col = ls->light_get_color(base).srgb_to_linear();
float energy = compute_light_energy(base, type);
ld.emission[0] = linear_col.r * energy;
ld.emission[1] = linear_col.g * energy;
ld.emission[2] = linear_col.b * energy;
ld.radius = ls->light_get_param(base, RSE::LIGHT_PARAM_SIZE);
ld.attenuation = ls->light_get_param(base, RSE::LIGHT_PARAM_ATTENUATION);
float range = ls->light_get_param(base, RSE::LIGHT_PARAM_RANGE);
if (range > 0.0f) {
ld.inv_max_range = 1.0f / range;
ld.max_range_squared = range * range;
} else {
ld.inv_max_range = -1.0f;
ld.max_range_squared = 0.0f;
}
ld.specular_amount = ls->light_get_param(base, RSE::LIGHT_PARAM_SPECULAR) * 2.0f; // Matches rasterizer convention (light_storage.cpp), normalizes 0.5 default to 1.0.
ld.indirect_energy = ls->light_get_param(base, RSE::LIGHT_PARAM_INDIRECT_ENERGY);
if (type == RSE::LIGHT_SPOT) {
ld.inv_spot_attenuation = 1.0f / MAX(0.001f, ls->light_get_param(base, RSE::LIGHT_PARAM_SPOT_ATTENUATION));
float spot_angle_deg = ls->light_get_param(base, RSE::LIGHT_PARAM_SPOT_ANGLE);
ld.cos_spot_angle = Math::cos(Math::deg_to_rad(spot_angle_deg));
Vector3 spot_dir = -xform.basis.get_column(2).normalized();
ld.spot_direction[0] = spot_dir.x;
ld.spot_direction[1] = spot_dir.y;
ld.spot_direction[2] = spot_dir.z;
} else {
ld.inv_spot_attenuation = 0.0f;
ld.cos_spot_angle = 0.0f;
ld.spot_direction[0] = 0.0f;
ld.spot_direction[1] = 0.0f;
ld.spot_direction[2] = 0.0f;
}
rt_light_count++;
}
return rt_light_count;
}
// ---------------------------------------------------------------------------
// Uniform set update
// ---------------------------------------------------------------------------
// All texture kinds share descriptor set 1, so it is rebuilt as a unit whenever
// any table changed. Split out of update_uniform_set because that function can
// now return early when the scene set is unchanged, and the bindless set still
// has to be kept current on those frames.
void RenderRaytracing::_update_bindless_uniform_set(RID p_shader_rd) {
bool bindless_dirty = !bindless_uniform_set.is_valid();
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
if (!bindless_blocks[k] || !bindless_blocks[k]->is_initialized()) {
return;
}
bindless_dirty = bindless_dirty || bindless_blocks[k]->needs_rebuild();
}
if (!bindless_dirty) {
return;
}
Vector<RD::Uniform> bindless_uniforms;
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
bindless_blocks[k]->collect_uniform(bindless_uniforms);
}
if (bindless_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(bindless_uniform_set)) {
RD::get_singleton()->free_rid(bindless_uniform_set);
}
bindless_uniform_set = RD::get_singleton()->uniform_set_create(bindless_uniforms, p_shader_rd, 1);
if (bindless_uniform_set.is_valid()) {
RD::get_singleton()->set_resource_name(bindless_uniform_set, "Bindless Texture Set");
for (uint32_t k = 0; k < BindlessBlock::TEXTURE_KIND_MAX; k++) {
bindless_blocks[k]->mark_rebuilt();
}
} else {
ERR_PRINT_ONCE("RT: failed to create the bindless texture uniform set.");
}
}
RID RenderRaytracing::update_uniform_set(RTViewportState *p_state, const RenderDataRD *p_render_data, uint32_t p_rt_flags) {
ERR_FAIL_NULL_V(p_state, RID());
// NOTE: the set is NOT freed here. It used to be destroyed and recreated every
// frame for every viewport -- ~30 bindings including the 12 material samplers
// -- which is pure per-frame driver work even when nothing about the scene
// changed. It is now rebuilt only when one of the bound RIDs differs (see the
// comparison against uniform_set_rids at the end of this function). Buffers
// updated in place, like params and lights, keep their RID and so do not
// require a new descriptor set.
// BindlessBlock handles its own uniform set cleanup via clear()
Ref<RenderForwardClustered::RenderBufferDataForwardClustered> rb_data;
if (p_render_data && p_render_data->render_buffers.is_valid()) {
if (p_render_data->render_buffers->has_custom_data(RB_SCOPE_FORWARD_CLUSTERED)) {
rb_data = p_render_data->render_buffers->get_custom_data(RB_SCOPE_FORWARD_CLUSTERED);
}
}
if (rb_data.is_null()) {
return RID();
}
// SET 0 indices must match raytracing_common_inc.glsl / scene_raytracing_raygen.glsl / samplers includes.
Vector<RD::Uniform> uniforms;
{
RD::Uniform u;
u.binding = 0;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
rb_data->rt_ensure_textures();
u.append_id(rb_data->rt_get_texture());
uniforms.push_back(u);
}
{
RD::Uniform u;
u.binding = 1;
u.uniform_type = RD::UNIFORM_TYPE_ACCELERATION_STRUCTURE;
ERR_FAIL_COND_V(p_state->tlas == RID(), RID());
u.append_id(p_state->tlas);
uniforms.push_back(u);
}
{
RD::Uniform u;
u.binding = 2;
u.uniform_type = RD::UNIFORM_TYPE_UNIFORM_BUFFER;
u.append_id(owner->scene_state.uniform_buffers[0]);
uniforms.push_back(u);
}
{
RD::Uniform u;
u.binding = 3;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
if (p_state->geometry_buffer.is_valid()) {
u.append_id(p_state->geometry_buffer);
} else {
// Use a default buffer if no geometry
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Binding 4: Per-instance motion index buffer (int32 per TLAS instance, -1 = no motion).
{
RD::Uniform u;
u.binding = 4;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
if (p_state->motion_index_buffer.is_valid()) {
u.append_id(p_state->motion_index_buffer);
} else {
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Motion transforms past sampler block growth reservation (bindings 28-31).
{
RD::Uniform u;
u.binding = 32;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
if (p_state->motion_transform_buffer.is_valid()) {
u.append_id(p_state->motion_transform_buffer);
} else {
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Binding 5: Material buffer.
{
RD::Uniform u;
u.binding = 5;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
if (p_state->material_buffer.is_valid()) {
u.append_id(p_state->material_buffer);
} else {
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Binding 6: Raytracing params + unjittered VP matrices.
{
struct {
float params[16];
float prev_vp_unjittered[16];
float curr_vp_unjittered[16];
} rt_ubo = {};
static_assert(sizeof(rt_ubo) == 48 * sizeof(float));
if (p_render_data && p_render_data->environment.is_valid()) {
const float *env_params = RendererEnvironmentStorage::get_singleton()->environment_get_pathtracing_params_ptr(p_render_data->environment);
if (env_params) {
memcpy(rt_ubo.params, env_params, sizeof(float) * 16);
}
}
// rt_params layout (see RaytracingParamIndex enum):
// [0] = VIS_MODE, [1] = SAMPLE_COUNT, [2] = MAX_BOUNCES,
// [3] = DLSS_RR_ENABLED, [14] = LIGHT_COUNT, [15] = FRAME_INDEX
rt_ubo.params[SceneShaderRaytracing::RT_PARAM_FRAME_INDEX] = float(p_state->frame_counter++);
// Unjittered VP for motion vectors (matches raster convention).
{
Projection correction;
correction.set_depth_correction(true);
Projection prev_vp = (correction * p_render_data->scene_data->prev_cam_projection) * Projection(p_render_data->scene_data->prev_cam_transform.affine_inverse());
RendererRD::MaterialStorage::store_camera(prev_vp, rt_ubo.prev_vp_unjittered);
Projection curr_vp = (correction * p_render_data->scene_data->cam_projection) * Projection(p_render_data->scene_data->cam_transform.affine_inverse());
RendererRD::MaterialStorage::store_camera(curr_vp, rt_ubo.curr_vp_unjittered);
}
// --- Light gathering ---
uint32_t rt_light_count = 0;
RT_LightData rt_light_data[RT_LIGHTS_MAX] = {};
rt_light_count = gather_lights(p_render_data, rt_light_data, RT_LIGHTS_MAX);
rt_ubo.params[SceneShaderRaytracing::RT_PARAM_LIGHT_COUNT] = float(rt_light_count);
// Upload light buffer.
{
uint32_t buf_size = RT_LIGHTS_MAX * sizeof(RT_LightData);
if (!p_state->light_buffer.is_valid()) {
p_state->light_buffer = RD::get_singleton()->storage_buffer_create(buf_size);
RD::get_singleton()->set_resource_name(p_state->light_buffer, "RT Light Buffer");
}
RD::get_singleton()->buffer_update(p_state->light_buffer, 0, buf_size, rt_light_data);
}
if (!p_state->params_buffer.is_valid()) {
p_state->params_buffer = RD::get_singleton()->uniform_buffer_create(sizeof(rt_ubo));
RD::get_singleton()->set_resource_name(p_state->params_buffer, "RT Params Buffer");
}
RD::get_singleton()->buffer_update(p_state->params_buffer, 0, sizeof(rt_ubo), &rt_ubo);
RD::Uniform u;
u.binding = 6;
u.uniform_type = RD::UNIFORM_TYPE_UNIFORM_BUFFER;
u.append_id(p_state->params_buffer);
uniforms.push_back(u);
}
// Binding 7: Sky radiance octahedral map (for pathtracing sky sampling).
{
RendererRD::TextureStorage *texture_storage = RendererRD::TextureStorage::get_singleton();
RID radiance_texture;
// Try to get radiance texture from sky
if (p_render_data && p_render_data->environment.is_valid()) {
RID sky_rid = owner->environment_get_sky(p_render_data->environment);
if (sky_rid.is_valid()) {
radiance_texture = owner->sky.sky_get_radiance_texture_rd(sky_rid);
}
}
// Fall back to default black texture if no sky
if (!radiance_texture.is_valid()) {
radiance_texture = texture_storage->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_BLACK);
}
RD::Uniform u;
u.binding = 7;
u.uniform_type = RD::UNIFORM_TYPE_TEXTURE;
u.append_id(radiance_texture);
uniforms.push_back(u);
}
// Binding 8: Sampler for radiance texture (linear filtering with mipmaps and clamp).
{
RD::Uniform u;
u.binding = 8;
u.uniform_type = RD::UNIFORM_TYPE_SAMPLER;
u.append_id(RendererRD::MaterialStorage::get_singleton()->sampler_rd_get_default(
RSE::CANVAS_ITEM_TEXTURE_FILTER_LINEAR_WITH_MIPMAPS, RSE::CANVAS_ITEM_TEXTURE_REPEAT_DISABLED));
uniforms.push_back(u);
}
// Bindings 9-12: DLSS Ray Reconstruction output buffers (only in DLSS RR shader variant).
bool dlss_rr_enabled = rb_data->dlss_rr_has_buffers();
if (dlss_rr_enabled) {
// Binding 9: DLSS RR Diffuse Albedo
{
RD::Uniform u;
u.binding = 9;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb_data->dlss_rr_get_diffuse_albedo());
uniforms.push_back(u);
}
// Binding 10: DLSS RR Specular Albedo
{
RD::Uniform u;
u.binding = 10;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb_data->dlss_rr_get_specular_albedo());
uniforms.push_back(u);
}
// Binding 11: DLSS RR Normal + Roughness
{
RD::Uniform u;
u.binding = 11;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb_data->dlss_rr_get_normal_roughness());
uniforms.push_back(u);
}
// Binding 12: DLSS RR Specular Hit Distance
{
RD::Uniform u;
u.binding = 12;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb_data->dlss_rr_get_specular_hit_dist());
uniforms.push_back(u);
}
}
// Binding 13: Light buffer (SSBO).
{
RD::Uniform u;
u.binding = 13;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
if (p_state->light_buffer.is_valid()) {
u.append_id(p_state->light_buffer);
} else {
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Binding 14: Global shader uniforms SSBO.
{
RD::Uniform u;
u.binding = 14;
u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER;
RID buf = RendererRD::MaterialStorage::get_singleton()->global_shader_uniforms_get_storage_buffer();
if (buf.is_valid()) {
u.append_id(buf);
} else {
u.append_id(RendererRD::MeshStorage::get_singleton()->get_default_rd_storage_buffer());
}
uniforms.push_back(u);
}
// Binding 15: RT depth output (R32F storage image for writing depth from closest_hit/miss).
{
RD::Uniform u;
u.binding = 15;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb_data->rt_get_depth_texture());
uniforms.push_back(u);
}
// Bindings 16-27: Material samplers (12 filter/repeat combinations for custom shaders).
RendererRD::MaterialStorage::get_singleton()->samplers_rd_get_default().append_uniforms(uniforms, 16);
// Binding 28: Velocity output (RG16F). Past the 16-27 sampler range.
{
Ref<RenderSceneBuffersRD> rb = p_render_data->render_buffers;
rb->ensure_velocity();
RD::Uniform u;
u.binding = 28;
u.uniform_type = RD::UNIFORM_TYPE_IMAGE;
u.append_id(rb->get_velocity_buffer(false));
uniforms.push_back(u);
}
// Use the pipeline-side shader so UniformSetFormat matches at bind time.
RID shader_rd = shader ? shader->get_pipeline_shader_rd(p_rt_flags) : RID();
if (shader_rd.is_valid()) {
// Flatten the bound RIDs (plus the shader, since the set is created
// against it) and reuse the existing set when nothing has changed.
LocalVector<RID> rids;
rids.push_back(shader_rd);
for (const RD::Uniform &u : uniforms) {
for (uint32_t i = 0; i < u.get_id_count(); i++) {
rids.push_back(u.get_id(i));
}
}
bool unchanged = p_state->uniform_set.is_valid() &&
RD::get_singleton()->uniform_set_is_valid(p_state->uniform_set) &&
p_state->uniform_set_rids.size() == rids.size();
if (unchanged) {
for (uint32_t i = 0; i < rids.size(); i++) {
if (p_state->uniform_set_rids[i] != rids[i]) {
unchanged = false;
break;
}
}
}
if (unchanged) {
// Still bind the bindless set below, then hand back the cached set.
if (bindless_blocks[0] && bindless_blocks[0]->is_initialized()) {
_update_bindless_uniform_set(shader_rd);
}
return p_state->uniform_set;
}
if (p_state->uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(p_state->uniform_set)) {
RD::get_singleton()->free_rid(p_state->uniform_set);
}
p_state->uniform_set = RID();
p_state->uniform_set_rids = rids;
p_state->uniform_set = RD::get_singleton()->uniform_set_create(
uniforms,
shader_rd,
RenderForwardClustered::SCENE_UNIFORM_SET);
RD::get_singleton()->set_resource_name(p_state->uniform_set, "RT Uniform Set");
// === SET 1: Bindless textures ===
_update_bindless_uniform_set(shader_rd);
}
return p_state->uniform_set;
}
// ---------------------------------------------------------------------------
// Output copy
// ---------------------------------------------------------------------------
void RenderRaytracing::copy_output_texture(const RenderDataRD *p_render_data) {
Ref<RenderSceneBuffersRD> rb = p_render_data->render_buffers;
ERR_FAIL_COND(rb.is_null());
Ref<RenderForwardClustered::RenderBufferDataForwardClustered> rb_data = rb->get_custom_data(RB_SCOPE_FORWARD_CLUSTERED);
if (rb_data.is_null() || !rb_data->rt_has_texture()) {
return;
}
// Copy raytracing output to main color buffer
for (uint32_t v = 0; v < rb->get_view_count(); v++) {
RID src = rb_data->rt_get_texture();
RID dst = rb->get_internal_texture(v);
owner->copy_effects->copy_to_rect(src, dst, Rect2i(0, 0, rb->get_internal_size().x, rb->get_internal_size().y), false, false, false, false, false, true);
}
}