Vulkan Schnee 0.0.1
High-performance rendering engine
Loading...
Searching...
No Matches
RenderingDataManager.cpp
Go to the documentation of this file.
2
3#include <array>
4#include <cstring>
5#include <plog/Log.h>
15#include "Engine/Mesh/Vertex.h"
21
22namespace EngineCore
23{
24 // Minimum buffer size to avoid creating zero-sized buffers
25 constexpr size_t MIN_BUFFER_SIZE = 64;
26
28 : engine(engine)
30 {
31 // Initialize all buffers with minimal placeholder size
32 // They will be resized as needed when data is uploaded
33 // VulkanStagedBuffer creates device-local buffers with staging for efficient GPU access
34 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
35
36 // Emplace and create primitive data buffers (indexed by primitive_rendering_id)
37 primitiveCullingData.emplace();
38 primitiveCullingData->create(context, MIN_BUFFER_SIZE, storageUsage);
39 primitiveCullingData->setDebugName("PrimitiveCullingData");
40
41 primitiveMeshletData.emplace();
42 primitiveMeshletData->create(context, MIN_BUFFER_SIZE, storageUsage);
43 primitiveMeshletData->setDebugName("PrimitiveMeshletData");
44
45 perObjectDataBuffer.emplace();
46 perObjectDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
47 perObjectDataBuffer->setDebugName("PerObjectData");
48
49 primitiveRenderData.emplace();
50 primitiveRenderData->create(context, MIN_BUFFER_SIZE, storageUsage);
51 primitiveRenderData->setDebugName("PrimitiveRenderData");
52
53 localBoundsBuffer.emplace();
54 localBoundsBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
55 localBoundsBuffer->setDebugName("LocalBoundsBuffer");
56
57 // Emplace and create meshlet data buffers (indexed by meshlet_rendering_id)
58 // Use persistent mapping for frequently updated geometry buffers to avoid map/unmap overhead
59 constexpr bool persistentMapping = true;
60
61 meshletBuffer.emplace();
62 meshletBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
63 meshletBuffer->setDebugName("MeshletBuffer");
64
65 meshletBoundsData.emplace();
66 meshletBoundsData->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
67 meshletBoundsData->setDebugName("MeshletBoundsData");
68
69 // Emplace and create geometry data buffers (indexed by vertex/triangle offsets in meshlets)
70 // Vertex buffer needs both STORAGE (for mesh shaders) and VERTEX_BUFFER (for VS path)
71 const VkBufferUsageFlags vertexBufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
72 vertexBuffer.emplace();
73 vertexBuffer->create(context, MIN_BUFFER_SIZE, vertexBufferUsage, persistentMapping);
74 vertexBuffer->setDebugName("VertexBuffer");
75
76 triangleBuffer.emplace();
77 triangleBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
78 triangleBuffer->setDebugName("TriangleBuffer");
79
80 // Emplace and create instancing buffers (for geometry deduplication)
81 meshGeometryDataBuffer.emplace();
82 meshGeometryDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
83 meshGeometryDataBuffer->setDebugName("MeshGeometryDataBuffer");
84
85 instanceDataBuffer.emplace();
86 instanceDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
87 instanceDataBuffer->setDebugName("InstanceDataBuffer");
88
91 instanceCullingDataBuffer->setDebugName("InstanceCullingDataBuffer");
92
93 // Vertex shader path buffers (for single-meshlet geometry)
94 vsIndexBuffer_.emplace();
95 vsIndexBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, persistentMapping);
96 vsIndexBuffer_->setDebugName("VSIndexBuffer");
97
99 singleMeshletGeometryBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
100 singleMeshletGeometryBuffer_->setDebugName("SingleMeshletGeometryBuffer");
101
102 // LOD cluster buffers (for per-cluster LOD selection)
103 clusterLodDataBuffer_.emplace();
104 clusterLodDataBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
105 clusterLodDataBuffer_->setDebugName("ClusterLodDataBuffer");
106
107 clusterGroupDataBuffer_.emplace();
108 clusterGroupDataBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
109 clusterGroupDataBuffer_->setDebugName("ClusterGroupDataBuffer");
110
111 // Initialize material buffers for all pipeline types
113
114 // Initialize SH probe buffer with default probe data
116
117 // Mark dirty so first frame will populate buffers with scene data
118 markDirty();
119 }
120
122 : engine(nullptr)
124 {
125 // Initialize all buffers with minimal placeholder size
126 // They will be resized as needed when data is uploaded
127 // VulkanStagedBuffer creates device-local buffers with staging for efficient GPU access
128 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
129
130 // Emplace and create primitive data buffers (indexed by primitive_rendering_id)
131 primitiveCullingData.emplace();
132 primitiveCullingData->create(context, MIN_BUFFER_SIZE, storageUsage);
133 primitiveCullingData->setDebugName("PrimitiveCullingData");
134
135 primitiveMeshletData.emplace();
136 primitiveMeshletData->create(context, MIN_BUFFER_SIZE, storageUsage);
137 primitiveMeshletData->setDebugName("PrimitiveMeshletData");
138
139 perObjectDataBuffer.emplace();
140 perObjectDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
141 perObjectDataBuffer->setDebugName("PerObjectData");
142
143 primitiveRenderData.emplace();
144 primitiveRenderData->create(context, MIN_BUFFER_SIZE, storageUsage);
145 primitiveRenderData->setDebugName("PrimitiveRenderData");
146
147 localBoundsBuffer.emplace();
148 localBoundsBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
149 localBoundsBuffer->setDebugName("LocalBoundsBuffer");
150
151 // Emplace and create meshlet data buffers (indexed by meshlet_rendering_id)
152 // Use persistent mapping for frequently updated geometry buffers to avoid map/unmap overhead
153 constexpr bool persistentMapping = true;
154
155 meshletBuffer.emplace();
156 meshletBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
157 meshletBuffer->setDebugName("MeshletBuffer");
158
159 meshletBoundsData.emplace();
160 meshletBoundsData->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
161 meshletBoundsData->setDebugName("MeshletBoundsData");
162
163 // Emplace and create geometry data buffers (indexed by vertex/triangle offsets in meshlets)
164 // Vertex buffer needs both STORAGE (for mesh shaders) and VERTEX_BUFFER (for VS path)
165 const VkBufferUsageFlags vertexBufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
166 vertexBuffer.emplace();
167 vertexBuffer->create(context, MIN_BUFFER_SIZE, vertexBufferUsage, persistentMapping);
168 vertexBuffer->setDebugName("VertexBuffer");
169
170 triangleBuffer.emplace();
171 triangleBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
172 triangleBuffer->setDebugName("TriangleBuffer");
173
174 // Emplace and create instancing buffers (for geometry deduplication)
175 meshGeometryDataBuffer.emplace();
176 meshGeometryDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
177 meshGeometryDataBuffer->setDebugName("MeshGeometryDataBuffer");
178
179 instanceDataBuffer.emplace();
180 instanceDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
181 instanceDataBuffer->setDebugName("InstanceDataBuffer");
182
184 instanceCullingDataBuffer->create(context, MIN_BUFFER_SIZE, storageUsage);
185 instanceCullingDataBuffer->setDebugName("InstanceCullingDataBuffer");
186
187 // Vertex shader path buffers (for single-meshlet geometry)
188 vsIndexBuffer_.emplace();
189 vsIndexBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, persistentMapping);
190 vsIndexBuffer_->setDebugName("VSIndexBuffer");
191
193 singleMeshletGeometryBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
194 singleMeshletGeometryBuffer_->setDebugName("SingleMeshletGeometryBuffer");
195
196 // LOD cluster buffers (for per-cluster LOD selection)
197 clusterLodDataBuffer_.emplace();
198 clusterLodDataBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
199 clusterLodDataBuffer_->setDebugName("ClusterLodDataBuffer");
200
201 clusterGroupDataBuffer_.emplace();
202 clusterGroupDataBuffer_->create(context, MIN_BUFFER_SIZE, storageUsage, persistentMapping);
203 clusterGroupDataBuffer_->setDebugName("ClusterGroupDataBuffer");
204
205 // Initialize material buffers for all pipeline types
207
208 // Initialize SH probe buffer with default probe data
210
211 // Mark dirty so first frame will populate buffers with scene data
212 markDirty();
213 }
214
216 {
217 // Destroy all staged buffers
218 if (primitiveCullingData.has_value()) primitiveCullingData->destroy();
219 if (primitiveMeshletData.has_value()) primitiveMeshletData->destroy();
220 if (perObjectDataBuffer.has_value()) perObjectDataBuffer->destroy();
221 if (primitiveRenderData.has_value()) primitiveRenderData->destroy();
222 if (meshletBuffer.has_value()) meshletBuffer->destroy();
223 if (meshletBoundsData.has_value()) meshletBoundsData->destroy();
224 if (vertexBuffer.has_value()) vertexBuffer->destroy();
225 if (triangleBuffer.has_value()) triangleBuffer->destroy();
226
227 // Destroy instancing buffers
228 if (meshGeometryDataBuffer.has_value()) meshGeometryDataBuffer->destroy();
229 if (instanceDataBuffer.has_value()) instanceDataBuffer->destroy();
230 if (instanceCullingDataBuffer.has_value()) instanceCullingDataBuffer->destroy();
231
232 // Destroy vertex shader path buffers
233 if (vsIndexBuffer_.has_value()) vsIndexBuffer_->destroy();
235
236 // Destroy LOD buffers
237 if (clusterLodDataBuffer_.has_value()) clusterLodDataBuffer_->destroy();
238 if (clusterGroupDataBuffer_.has_value()) clusterGroupDataBuffer_->destroy();
239
240 // Destroy material buffers
241 for (auto& [pipeline, buffer] : materialBuffersByPipeline) {
242 if (buffer.has_value()) {
243 buffer->destroy();
244 }
245 }
246
247 // Destroy SH probe buffer
248 if (shProbeBuffer_.has_value()) {
249 shProbeBuffer_->destroy();
250 }
251 }
252
254 {
255 injectedSceneManager = sceneManager;
256 }
257
259 {
260 injectedAssetManager = assetManager;
261 }
262
264 {
265 injectedRenderer = renderer;
266 }
267
269 {
270 TRACY_ZONE_SCOPED_NAMED("Update asset transforms if dirty");
271
272 PLOGD << "updateIfDirty called: isDirty=" << isDirty << " instancingEnabled=" << instancingEnabled_;
273
274 if (isDirty) {
275 PLOGI << "updateIfDirty: isDirty=true, proceeding with update";
276 // Upload material data FIRST - this populates materialPathTo*Index_ maps
277 // which are then used by the update methods to look up correct material IDs by path
279
280 bool buffersRecreated;
281 if (instancingEnabled_) {
282 // Instanced path does inline validation - no snapshot needed
283 buffersRecreated = updatePrimitiveDataInstanced();
284 } else {
285 // Legacy path still uses snapshot for consistency
287 buffersRecreated = updatePrimitiveData();
288 }
289
290 isDirty = false;
291 return buffersRecreated;
292 }
293 return false;
294 }
295
297 {
298 TRACY_ZONE_SCOPED_NAMED("Update primitive data");
299 // Resolve dependencies from Engine or injected members
300 SceneManager* sceneManager = engine ? engine->getSceneManager().get() : injectedSceneManager;
301 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
302 Renderer* renderer = engine ? engine->getRenderer() : injectedRenderer;
303
304 // Skip update if required dependencies are not available
305 if (!sceneManager) {
306 PLOGD << "SceneManager not available, skipping primitive data update";
307 return false;
308 }
309
310 // Clear entity-to-primitive mappings (will be rebuilt during iteration)
311 entityToPrimitiveIds_.clear();
313
314 // Packed triangle - 3 x 8-bit indices in a single uint32 (4x smaller than old format)
315 using PackedTriangle = Ecs::PackedTriangle;
316
317 // Output buffers (indexed by primitive_rendering_id)
318 std::vector<ObjectCullingData> primitiveCullingBuffer;
319 std::vector<LocalBoundsData> localBoundsDataBuffer; // Static local-space bounds (uploaded once)
320 std::vector<PrimitiveMeshletData> primitiveMeshletBuffer;
321 std::vector<glm::mat4> perObjectDataLocal;
322 std::vector<MeshPrimitiveRenderData> primitiveRenderDataBuffer;
323
324 // Meshlet data (indexed by meshlet_rendering_id)
325 std::vector<UnifiedMeshlet> meshletDataBuffer;
326 std::vector<MeshletBounds> meshletBoundsBuffer;
327
328 // Geometry data (indexed by vertex/triangle offsets in meshlets)
329 std::vector<Vertex> optimizedVertexBuffer;
330 std::vector<PackedTriangle> optimizedTriangleBuffer;
331
332 primitive_rendering_id primitiveId = 0;
333 meshlet_rendering_id meshletId = 0;
334
335 // Note: materialPathTo*Index_ maps are populated by uploadMaterialBuffers() which is called first
336 // Material lookup is done by path, decoupled from primitive iteration order
337
338 // Use ECS view for efficient iteration - directly queries MeshComponents
339 entt::registry& registry = Ecs::RegistryManager::get();
340 auto meshView = registry.view<Ecs::MeshComponentRef>();
341
342 PLOGD << "updatePrimitiveData: Found " << meshView.size() << " mesh components via ECS";
343
344 // Pre-pass: count totals for vector pre-allocation to avoid reallocations
345 // This is critical for large meshes (2M+ triangles) to prevent main thread stalls
346 size_t totalPrimitives = 0;
347 size_t totalMeshlets = 0;
348 size_t totalVertices = 0;
349 size_t totalTriangles = 0;
350
351 for (auto entity : meshView) {
352 MeshComponent* meshComp = meshView.get<Ecs::MeshComponentRef>(entity).component;
353 if (!meshComp || !meshComp->isVisible()) continue;
354 MeshAsset* asset = meshComp->getMeshAsset();
355 if (!asset || !isMeshInSnapshot(asset)) continue;
356 auto* meshData = asset->getMeshPrimitiveData();
357 if (!meshData) continue;
358
359 for (const Ecs::MeshPrimitive& primitive : meshData->primitives) {
360 // Require both meshletData and unpackedData (generated on background thread)
361 if (!primitive.meshletData.has_value() || !primitive.unpackedData.has_value()) continue;
362 totalPrimitives++;
363 totalMeshlets += primitive.meshletData->meshlets.size();
364 totalVertices += primitive.unpackedData->vertices.size();
365 totalTriangles += primitive.unpackedData->triangles.size();
366 }
367 }
368
369 // Reserve capacity to avoid reallocations during population
370 primitiveCullingBuffer.reserve(totalPrimitives);
371 localBoundsDataBuffer.reserve(totalPrimitives);
372 primitiveMeshletBuffer.reserve(totalPrimitives);
373 perObjectDataLocal.reserve(totalPrimitives);
374 primitiveRenderDataBuffer.reserve(totalPrimitives);
375 meshletDataBuffer.reserve(totalMeshlets);
376 meshletBoundsBuffer.reserve(totalMeshlets);
377 optimizedVertexBuffer.reserve(totalVertices);
378 optimizedTriangleBuffer.reserve(totalTriangles);
379
380 PLOGI << "[TIMING] Pre-allocated buffers: " << totalPrimitives << " primitives, "
381 << totalMeshlets << " meshlets, " << totalVertices << " vertices, "
382 << totalTriangles << " triangles"
383 << " (vertex buffer: " << (totalVertices * sizeof(Vertex) / 1024 / 1024) << " MB, "
384 << "triangle buffer: " << (totalTriangles * sizeof(PackedTriangle) / 1024) << " KB)";
385
386 PLOGI << "[TIMING] Starting data population loop...";
387
388 for (auto entity : meshView) {
389 MeshComponent* meshComp = meshView.get<Ecs::MeshComponentRef>(entity).component;
390 if (!meshComp || !meshComp->isVisible()) {
391 PLOGD << " Skipping: MeshComponent not visible";
392 continue;
393 }
394
395 MeshAsset* asset = meshComp->getMeshAsset();
396 // Use snapshot check to ensure consistency with updateTransforms()
397 if (!asset || !isMeshInSnapshot(asset)) {
398 PLOGD << " Skipping: MeshAsset not in render snapshot";
399 continue;
400 }
401
402 auto* meshData = asset->getMeshPrimitiveData();
403 if (!meshData) {
404 // Safety check - should always have data if in snapshot
405 PLOGD << " Skipping: MeshAsset has no primitive data (unexpected)";
406 continue;
407 }
408
409 PLOGD << " Processing mesh with " << meshData->primitives.size() << " primitives";
410
411 glm::mat4 worldMatrix = meshComp->getWorldTransform();
412
413 for ( const Ecs::MeshPrimitive & primitive : meshData->primitives) {
414 if (!primitive.meshletData.has_value() || !primitive.unpackedData.has_value()) {
415 PLOGD << " Primitive has no meshlet/unpacked data, skipping";
416 continue;
417 }
418
419 // Store local bounds (static data - uploaded once, GPU transforms to world space)
420 LocalBoundsData localBounds{};
421 localBounds.localCenterAndRadius = glm::vec4(
422 primitive.boundingSphere.center,
423 primitive.boundingSphere.radius
424 );
425 localBoundsDataBuffer.push_back(localBounds);
426
427 // Transform local bounds to world space (for initial culling data)
428 // Note: After GPU transform optimization, this is computed on GPU from localBounds + perObjectData
429 glm::vec3 worldCenter = worldMatrix * glm::vec4(primitive.boundingSphere.center, 1.0f);
430
431 // Extract max scale factor for bounding sphere radius scaling
432 float scaleX = glm::length(glm::vec3(worldMatrix[0]));
433 float scaleY = glm::length(glm::vec3(worldMatrix[1]));
434 float scaleZ = glm::length(glm::vec3(worldMatrix[2]));
435 float maxScale = std::max({scaleX, scaleY, scaleZ});
436
437 // Primitive culling data (vec4: xyz = position, w = radius)
438 ObjectCullingData culling{};
439 culling.worldPositionAndRadius = glm::vec4(worldCenter, primitive.boundingSphere.radius * maxScale);
440 culling.objectIndex = primitiveId;
441 primitiveCullingBuffer.push_back(culling);
442
443 // Primitive's meshlet range
444 uint32_t meshletStart = meshletId;
445 uint32_t meshletCountForPrimitive = static_cast<uint32_t>(primitive.meshletData->meshlets.size());
446
447 // Look up pipeline index from material type
448 uint32_t pipelineIndex = 0;
450
451 if (!primitive.materialPath.empty() && assetManager) {
452 std::optional<MaterialAsset*> materialAsset =
453 assetManager->getMaterialAssetManager()->getAsset(primitive.materialPath);
454
455 if (materialAsset.has_value()) {
456 pipelineType = materialAsset.value()->getType();
457 }
458 }
459
460 // Get pipeline index directly from PipelineNames enum
461 if (renderer) {
462 if (!renderer->getPipelineIndex(pipelineType, pipelineIndex)) {
463 // Fall back to default pipeline if lookup failed
464 renderer->getPipelineIndex(DEFAULT_MATERIAL_PIPELINE, pipelineIndex);
465 PLOGW << "updatePrimitiveDataInstanced: Pipeline lookup failed for pipelineType="
466 << static_cast<int>(pipelineType) << " primitiveId=" << primitiveId
467 << " materialPath=" << (primitive.materialPath.empty() ? "(empty)" : primitive.materialPath.getAssetHandle());
468 }
469 }
470
471 // Log for STATIC_LIGHTMAP primitives to debug visibility
472 if (pipelineType == STATIC_LIGHTMAP) {
473 PLOGI << "updatePrimitiveDataInstanced: STATIC_LIGHTMAP primitive found!"
474 << " primitiveId=" << primitiveId
475 << " pipelineIndex=" << pipelineIndex
476 << " meshletCount=" << meshletCountForPrimitive
477 << " materialPath=" << primitive.materialPath.getAssetHandle();
478 }
479
480 // Store meshlet metadata for this primitive
481 PrimitiveMeshletData meshletMeta{};
482 meshletMeta.meshletStartIndex = meshletStart;
483 meshletMeta.meshletCount = meshletCountForPrimitive;
484 meshletMeta.pipelineID = pipelineIndex;
485 primitiveMeshletBuffer.push_back(meshletMeta);
486
487 // Store transform
488 perObjectDataLocal.push_back(worldMatrix);
489
490 // Look up materialId from material path maps (populated by uploadMaterialBuffers())
491 // This uses the material path directly instead of primitiveId to avoid order mismatches
492 uint32_t materialId = 0;
493 std::string matPathKey = primitive.materialPath.empty() ? "__default__" : primitive.materialPath.getAssetHandle();
494
495 // Select the correct map based on pipeline type
496 switch (pipelineType) {
497 case DIFFUSE_FLAT_COLOR: {
498 auto it = materialPathToFlatColorIndex_.find(matPathKey);
499 if (it != materialPathToFlatColorIndex_.end()) materialId = it->second;
500 break;
501 }
502 case DIFFUSE_SHADER: {
503 auto it = materialPathToDiffuseIndex_.find(matPathKey);
504 if (it != materialPathToDiffuseIndex_.end()) materialId = it->second;
505 break;
506 }
508 case L0_SHADER:
509 case L1_SHADER:
510 case L2_SHADER:
511 case DYNAMIC_TEXTURES:
512 case STATIC_LIGHTMAP: {
513 auto it = materialPathToPbrIndex_.find(matPathKey);
514 if (it != materialPathToPbrIndex_.end()) materialId = it->second;
515 break;
516 }
517 case NORMALS_SHADER:
518 default: {
519 auto it = materialPathToNormalsIndex_.find(matPathKey);
520 if (it != materialPathToNormalsIndex_.end()) materialId = it->second;
521 break;
522 }
523 }
524
525 // Store primitive render data (texture/material IDs)
526 MeshPrimitiveRenderData renderData{};
527 renderData.colorTextureId = 0; // Texture index stored in material buffer
528 renderData.materialId = materialId;
529 primitiveRenderDataBuffer.push_back(renderData);
530
531 // Use pre-unpacked data from background thread (avoids main thread stall)
532 if (!primitive.unpackedData.has_value()) {
533 PLOGW << "Primitive has no pre-unpacked data, skipping";
534 continue;
535 }
536
537 const auto& unpackedVertices = primitive.unpackedData->vertices;
538 const auto& unpackedTriangles = primitive.unpackedData->triangles;
539
540 // Bulk insert all vertices/triangles for this primitive (much faster than per-element push_back)
541 size_t vertexBaseOffset = optimizedVertexBuffer.size();
542 size_t triangleBaseOffset = optimizedTriangleBuffer.size();
543
544 // Insert vertices in bulk
545 optimizedVertexBuffer.insert(optimizedVertexBuffer.end(),
546 unpackedVertices.begin(), unpackedVertices.end());
547
548 // Insert packed triangles in bulk via memcpy
549 size_t triangleInsertStart = optimizedTriangleBuffer.size();
550 optimizedTriangleBuffer.resize(optimizedTriangleBuffer.size() + unpackedTriangles.size());
551 static_assert(sizeof(PackedTriangle) == sizeof(Ecs::PackedTriangle), "PackedTriangle layout mismatch");
552 std::memcpy(&optimizedTriangleBuffer[triangleInsertStart],
553 unpackedTriangles.data(),
554 unpackedTriangles.size() * sizeof(PackedTriangle));
555
556 // Track position within pre-unpacked data for per-meshlet offsets
557 size_t unpackedVertexOffset = 0;
558 size_t unpackedTriangleOffset = 0;
559
560 // Process each meshlet - just build metadata (no data copying)
561 for (size_t m = 0; m < meshletCountForPrimitive; m++) {
562 const auto& meshlet = primitive.meshletData->meshlets[m];
563
564 // Calculate offsets into the already-inserted data
565 uint32_t vertexDataOffset = static_cast<uint32_t>(vertexBaseOffset + unpackedVertexOffset);
566 uint32_t triangleDataOffset = static_cast<uint32_t>(triangleBaseOffset + unpackedTriangleOffset);
567
568 UnifiedMeshlet unified{};
569 unified.primitiveIndex = primitiveId;
570 unified.pipelineIndex = pipelineIndex;
571 unified.vertexCount = meshlet.vertex_count;
572 unified.triangleCount = meshlet.triangle_count;
573 unified.vertexDataOffset = vertexDataOffset;
574 unified.triangleDataOffset = triangleDataOffset;
575 meshletDataBuffer.push_back(unified);
576
577 unpackedVertexOffset += meshlet.vertex_count;
578 unpackedTriangleOffset += meshlet.triangle_count;
579
580 // Compute meshlet bounds in world space
581 MeshletBounds bounds{};
582 float radius = primitive.boundingSphere.radius * maxScale;
583 bounds.worldPositionAndRadius = glm::vec4(worldCenter, radius);
584 bounds.meshletIndex = meshletId;
585 bounds.pipelineIndex = pipelineIndex;
586 meshletBoundsBuffer.push_back(bounds);
587
588 meshletId++;
589 }
590
591 // Record entity-to-primitive mapping for sparse transform updates
592 // Entity is already available from the ECS view iteration
593 entityToPrimitiveIds_[entity].push_back(primitiveId);
594 primitiveIdToComponent_[primitiveId] = meshComp;
595
596 primitiveId++;
597 }
598 }
599
600 // Update counts for dispatch sizing
601 primitiveCount = primitiveId;
602 meshletCount = meshletId;
603
604 // Debug: Count meshlets per pipeline type
605 std::array<uint32_t, 16> meshletCountPerPipeline{};
606 for (const auto& meshlet : meshletDataBuffer) {
607 if (meshlet.pipelineIndex < meshletCountPerPipeline.size()) {
608 meshletCountPerPipeline[meshlet.pipelineIndex]++;
609 }
610 }
611 PLOGI << "[DEBUG] Meshlets per pipeline:";
612 for (uint32_t i = 0; i < 9; ++i) {
613 if (meshletCountPerPipeline[i] > 0) {
614 PLOGI << " Pipeline " << i << ": " << meshletCountPerPipeline[i] << " meshlets";
615 }
616 }
617
618 PLOGI << "[TIMING] Data population loop complete";
619
620 // Handle empty scene case
621 if (primitiveCount == 0) {
622 PLOGD << ("RenderingDataManager: No primitives to upload");
623 return false;
624 }
625
626 PLOGI << "[TIMING] RenderingDataManager: Uploading " << primitiveCount << " primitives, " << meshletCount << " meshlets";
627 PLOGI << " Total vertices: " << optimizedVertexBuffer.size() << ", total triangles: " << optimizedTriangleBuffer.size();
628
629 // Ensure buffers are large enough and stage data for upload
630 // Track if any buffer was recreated (requires descriptor set updates)
631 // Sync objects are stored for later recording in transfer phase
632 bool anyBufferRecreated = false;
633
634 // Ensure Renderer's output buffers are sized for this primitive count
635 if (renderer) {
636 anyBufferRecreated |= renderer->ensureOutputBufferSizes(primitiveCount);
637 }
638 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
639
640 // Clear any previous sync objects
641 pendingSyncObjects_.clear();
642
643 // Primitive culling data (still uploaded for compatibility, but GPU computes from local bounds)
644 size_t cullingSize = primitiveCullingBuffer.size() * sizeof(ObjectCullingData);
645 anyBufferRecreated |= primitiveCullingData->ensureSize(cullingSize, storageUsage);
646 pendingSyncObjects_.push_back(primitiveCullingData->upload(context, primitiveCullingBuffer.data(), cullingSize));
647
648 // Local bounds data (static - uploaded once per structure change, GPU transforms to world space)
649 size_t localBoundsSize = localBoundsDataBuffer.size() * sizeof(LocalBoundsData);
650 anyBufferRecreated |= localBoundsBuffer->ensureSize(localBoundsSize, storageUsage);
651 pendingSyncObjects_.push_back(localBoundsBuffer->upload(context, localBoundsDataBuffer.data(), localBoundsSize));
652
653 // Primitive meshlet metadata
654 size_t meshletMetaSize = primitiveMeshletBuffer.size() * sizeof(PrimitiveMeshletData);
655 anyBufferRecreated |= primitiveMeshletData->ensureSize(meshletMetaSize, storageUsage);
656 pendingSyncObjects_.push_back(primitiveMeshletData->upload(context, primitiveMeshletBuffer.data(), meshletMetaSize));
657
658 // Primitive transforms
659 size_t transformSize = perObjectDataLocal.size() * sizeof(glm::mat4);
660 anyBufferRecreated |= perObjectDataBuffer->ensureSize(transformSize, storageUsage);
661 pendingSyncObjects_.push_back(perObjectDataBuffer->upload(context, perObjectDataLocal.data(), transformSize));
662
663 // Unified meshlet data
664 size_t meshletSize = meshletDataBuffer.size() * sizeof(UnifiedMeshlet);
665 anyBufferRecreated |= meshletBuffer->ensureSize(meshletSize, storageUsage);
666 pendingSyncObjects_.push_back(meshletBuffer->upload(context, meshletDataBuffer.data(), meshletSize));
667
668 // Meshlet bounds for culling
669 size_t boundsSize = meshletBoundsBuffer.size() * sizeof(MeshletBounds);
670 anyBufferRecreated |= meshletBoundsData->ensureSize(boundsSize, storageUsage);
671 pendingSyncObjects_.push_back(meshletBoundsData->upload(context, meshletBoundsBuffer.data(), boundsSize));
672
673 // Primitive render data (texture/material IDs)
674 size_t renderDataSize = primitiveRenderDataBuffer.size() * sizeof(MeshPrimitiveRenderData);
675 anyBufferRecreated |= primitiveRenderData->ensureSize(renderDataSize, storageUsage);
676 pendingSyncObjects_.push_back(primitiveRenderData->upload(context, primitiveRenderDataBuffer.data(), renderDataSize));
677
678 // Optimized vertex data (needs VERTEX_BUFFER_BIT for VS path)
679 const VkBufferUsageFlags vertexBufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
680 PLOGI << "[TIMING] Starting vertex buffer upload (" << (optimizedVertexBuffer.size() * sizeof(Vertex) / 1024 / 1024) << " MB)...";
681 size_t vertexSize = optimizedVertexBuffer.size() * sizeof(Vertex);
682 anyBufferRecreated |= vertexBuffer->ensureSize(vertexSize, vertexBufferUsage);
683 PLOGI << "[TIMING] Vertex buffer ensureSize complete";
684 pendingSyncObjects_.push_back(vertexBuffer->upload(context, optimizedVertexBuffer.data(), vertexSize));
685 PLOGI << "[TIMING] Vertex buffer upload complete";
686
687 // Optimized triangle data
688 PLOGI << "[TIMING] Starting triangle buffer upload (" << (optimizedTriangleBuffer.size() * sizeof(PackedTriangle) / 1024) << " KB)...";
689 size_t triangleSize = optimizedTriangleBuffer.size() * sizeof(PackedTriangle);
690 anyBufferRecreated |= triangleBuffer->ensureSize(triangleSize, storageUsage);
691 PLOGI << "[TIMING] Triangle buffer ensureSize complete";
692 pendingSyncObjects_.push_back(triangleBuffer->upload(context, optimizedTriangleBuffer.data(), triangleSize));
693 PLOGI << "[TIMING] Triangle buffer upload complete";
694
695 PLOGI << " > Staged " << optimizedVertexBuffer.size() << " vertices, "
696 << optimizedTriangleBuffer.size() << " triangles for upload";
697
698 if (anyBufferRecreated) {
699 PLOGI << " > Buffer(s) were recreated - descriptor sets need updating";
700 }
701
702 // Increment version so RenderProcesses know to update their descriptors
703 dataVersion_++;
704 PLOGI << " > Data version incremented to " << dataVersion_;
705
706 return anyBufferRecreated;
707 }
708
710 {
711 PLOGI << "[VS_DIAG] updatePrimitiveDataInstanced() called";
712
713 // Resolve dependencies from Engine or injected members
714 SceneManager* sceneManager = engine ? engine->getSceneManager().get() : injectedSceneManager;
715 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
716 Renderer* renderer = engine ? engine->getRenderer() : injectedRenderer;
717
718 // Skip update if required dependencies are not available
719 if (!sceneManager) {
720 PLOGD << "SceneManager not available, skipping primitive data update";
721 return false;
722 }
723
724 // Clear entity-to-primitive mappings (will be rebuilt during iteration)
725 entityToPrimitiveIds_.clear();
727
728 // Packed triangle - 3 x 8-bit indices in a single uint32 (4x smaller than old format)
729 using PackedTriangle = Ecs::PackedTriangle;
730
731 // ============================================================================
732 // INCREMENTAL GEOMETRY UPDATE OPTIMIZATION
733 // - Geometry buffers (vertices, triangles, meshlets) are append-only
734 // - Only clear and rebuild if geometryNeedsFullRebuild_ is set (mesh unload)
735 // - Instance buffers are always rebuilt (cheap)
736 // - Skip copying geometry data for meshes already in cache
737 // ============================================================================
738
739 // Track if this is a full rebuild or incremental
740 const bool fullRebuild = geometryNeedsFullRebuild_;
741
742 if (fullRebuild) {
743 PLOGI << "[INCREMENTAL] Full geometry rebuild triggered";
744 // Full rebuild: clear everything
745 geometryCache_.clear();
749 workingVertexBuffer_.clear();
751 workingVsIndexBuffer_.clear();
755
756 // Reset incremental tracking
757 nextGeometryId_ = 0;
758 nextMeshletId_ = 0;
765 clusterCount_ = 0;
767
769 } else {
770 PLOGI << "[INCREMENTAL] Incremental geometry update - " << geometryCache_.size() << " geometries already cached";
771 // Incremental: keep geometry buffers and cache, just track new additions
772 }
773
774 // Always rebuild instance buffers (they depend on current scene state)
775 // Pre-reserve capacity based on previous sizes
776 size_t prevInstanceCount = workingInstanceData_.capacity();
777 workingInstanceData_.clear();
782 workingPerObjectData_.clear();
784
785 if (prevInstanceCount > 0) {
786 workingInstanceData_.reserve(prevInstanceCount);
787 workingInstanceCullingData_.reserve(prevInstanceCount);
788 workingPrimitiveCullingBuffer_.reserve(prevInstanceCount);
789 workingLocalBoundsDataBuffer_.reserve(prevInstanceCount);
790 workingPrimitiveMeshletBuffer_.reserve(prevInstanceCount);
791 workingPerObjectData_.reserve(prevInstanceCount);
792 workingPrimitiveRenderData_.reserve(prevInstanceCount);
793 }
794
795 // Continue from last assigned IDs (for incremental) or start fresh (for full rebuild)
796 uint32_t meshGeometryId = nextGeometryId_;
798 uint32_t instanceId = 0;
799 primitive_rendering_id primitiveId = 0;
800
801 // Note: materialPathTo*Index_ maps are populated by uploadMaterialBuffers() which is called first
802 // Material lookup is done by path, decoupled from primitive iteration order
803
804 // Track new geometry added this update (for partial upload)
805 size_t newGeometryCount = 0;
806
807 // Counters for logging
808 size_t totalInstances = 0;
809 size_t singleMeshletCount = 0;
810 size_t multiMeshletCount = 0;
811
812 // Use ECS view for efficient iteration - directly queries MeshComponents
813 entt::registry& registry = Ecs::RegistryManager::get();
814 auto meshView = registry.view<Ecs::MeshComponentRef>();
815
816 PLOGD << "updatePrimitiveDataInstanced: Found " << meshView.size() << " mesh components via ECS";
817
818 // ============================================================================
819 // SINGLE PASS: Process all mesh components once
820 // For each component, check if geometry is cached; if not, add it.
821 // Then create instance data referencing the (possibly just-added) geometry.
822 // No separate snapshot needed - we validate inline.
823 // ============================================================================
824
825 {
826 TRACY_ZONE_SCOPED_NAMED("Build geometry and instance data");
827 for (auto entity : meshView) {
828 MeshComponent* meshComp = meshView.get<Ecs::MeshComponentRef>(entity).component;
829 if (!meshComp || !meshComp->isVisible()) continue;
830
831 MeshAsset* asset = meshComp->getMeshAsset();
832 if (!asset) continue;
833
834 // Inline validation: check mesh has valid primitive data (replaces isMeshInSnapshot)
835 auto* meshData = asset->getMeshPrimitiveData();
836 if (!meshData) continue;
837
838 glm::mat4 worldMatrix = meshComp->getWorldTransform();
839
840 // Extract max scale factor (computed once per component)
841 float scaleX = glm::length(glm::vec3(worldMatrix[0]));
842 float scaleY = glm::length(glm::vec3(worldMatrix[1]));
843 float scaleZ = glm::length(glm::vec3(worldMatrix[2]));
844 float maxScale = std::max({scaleX, scaleY, scaleZ});
845
846 uint32_t primIdx = 0;
847 for (const Ecs::MeshPrimitive& primitive : meshData->primitives) {
848 if (!primitive.meshletData.has_value() || !primitive.unpackedData.has_value()) {
849 primIdx++;
850 continue;
851 }
852
853 totalInstances++;
854 GeometryCacheKey key{asset, primIdx};
855
856 // Check if geometry is already cached
857 auto cacheIt = geometryCache_.find(key);
858
859 if (cacheIt == geometryCache_.end()) {
860 // ============================================================
861 // NEW GEOMETRY: Add to cache and build GPU data
862 // ============================================================
863
864 // Look up pipeline index from material type (cached in mapping)
865 uint32_t pipelineIndex = 0;
867
868 if (!primitive.materialPath.empty() && assetManager) {
869 std::optional<MaterialAsset*> materialAsset =
870 assetManager->getMaterialAssetManager()->getAsset(primitive.materialPath);
871 if (materialAsset.has_value()) {
872 pipelineType = materialAsset.value()->getType();
873 }
874 }
875
876 if (renderer) {
877 if (!renderer->getPipelineIndex(pipelineType, pipelineIndex)) {
878 renderer->getPipelineIndex(DEFAULT_MATERIAL_PIPELINE, pipelineIndex);
879 }
880 }
881
882 // Record geometry data
883 uint32_t meshletStart = meshletId;
884
885 // Determine data source: use LOD hierarchy data if available, otherwise base primitive data
886 // When LOD hierarchy exists, it contains meshlets for ALL LOD levels (including base level as LOD 0)
887 // The cluster meshlet indices reference into this combined LOD meshlet buffer
888 bool hasLodHierarchy = primitive.lodHierarchy.has_value() &&
889 primitive.lodHierarchy->lodLevelCount > 1;
890
891 const std::vector<Vertex>* unpackedVerticesPtr;
892 const std::vector<Ecs::PackedTriangle>* unpackedTrianglesPtr;
893 const std::vector<meshopt_Meshlet>* meshletsPtr;
894
895 if (hasLodHierarchy) {
896 // Use LOD hierarchy data (includes all LOD levels)
897 unpackedVerticesPtr = &primitive.lodHierarchy->unpackedData.vertices;
898 unpackedTrianglesPtr = &primitive.lodHierarchy->unpackedData.triangles;
899 meshletsPtr = &primitive.lodHierarchy->meshletData.meshlets;
900 VS_LOG(LogLOD, Debug, "Using LOD hierarchy data: {} meshlets, {} vertices, {} triangles",
901 meshletsPtr->size(), unpackedVerticesPtr->size(), unpackedTrianglesPtr->size());
902 } else {
903 // Use base primitive data (no LOD)
904 unpackedVerticesPtr = &primitive.unpackedData->vertices;
905 unpackedTrianglesPtr = &primitive.unpackedData->triangles;
906 meshletsPtr = &primitive.meshletData->meshlets;
907 }
908
909 const auto& unpackedVertices = *unpackedVerticesPtr;
910 const auto& unpackedTriangles = *unpackedTrianglesPtr;
911 const auto& meshlets = *meshletsPtr;
912 uint32_t meshletCount = static_cast<uint32_t>(meshlets.size());
913
914 MeshGeometryData geoData{};
915 geoData.meshletStartIndex = meshletStart;
916 geoData.meshletCount = meshletCount;
917 geoData.pipelineIndex = pipelineIndex;
918 geoData.singleMeshletGeoIndex = 0xFFFFFFFF; // Default: multi-meshlet (no VS path)
919 geoData.clusterStartIndex = 0xFFFFFFFF; // Default: no LOD data
920 geoData.clusterCount = 0;
921 geoData.groupStartIndex = 0xFFFFFFFF;
922 geoData.groupCount = 0;
923 size_t geoDataIndex = workingGeometryDataBuffer_.size();
924 workingGeometryDataBuffer_.push_back(geoData);
925
926 size_t vertexBaseOffset = workingVertexBuffer_.size();
927 size_t triangleBaseOffset = workingTriangleBuffer_.size();
928
930 unpackedVertices.begin(), unpackedVertices.end());
931
932 size_t triangleInsertStart = workingTriangleBuffer_.size();
933 workingTriangleBuffer_.resize(workingTriangleBuffer_.size() + unpackedTriangles.size());
934 static_assert(sizeof(PackedTriangle) == sizeof(Ecs::PackedTriangle), "PackedTriangle layout mismatch");
935 std::memcpy(&workingTriangleBuffer_[triangleInsertStart],
936 unpackedTriangles.data(),
937 unpackedTriangles.size() * sizeof(PackedTriangle));
938
939 // Build meshlets for this geometry
940 size_t unpackedVertexOffset = 0;
941 size_t unpackedTriangleOffset = 0;
942
943 for (size_t m = 0; m < meshletCount; m++) {
944 const auto& meshlet = meshlets[m];
945
946 UnifiedMeshlet unified{};
947 unified.primitiveIndex = meshGeometryId;
948 unified.pipelineIndex = pipelineIndex;
949 unified.vertexCount = meshlet.vertex_count;
950 unified.triangleCount = meshlet.triangle_count;
951 unified.vertexDataOffset = static_cast<uint32_t>(vertexBaseOffset + unpackedVertexOffset);
952 unified.triangleDataOffset = static_cast<uint32_t>(triangleBaseOffset + unpackedTriangleOffset);
953 workingMeshletDataBuffer_.push_back(unified);
954
955 // Build meshlet bounds (local space)
956 MeshletBounds bounds{};
957 bounds.worldPositionAndRadius = glm::vec4(
958 primitive.boundingSphere.center,
959 primitive.boundingSphere.radius);
960 bounds.meshletIndex = meshletId;
961 bounds.pipelineIndex = pipelineIndex;
962 workingMeshletBoundsBuffer_.push_back(bounds);
963
964 unpackedVertexOffset += meshlet.vertex_count;
965 unpackedTriangleOffset += meshlet.triangle_count;
966 meshletId++;
967 }
968
969 // Build geometry mapping (with cached pipeline and bounds)
970 MeshGeometryMapping mapping{};
971 mapping.meshGeometryId = meshGeometryId;
972 mapping.meshletStartIndex = meshletStart;
973 mapping.meshletCount = meshletCount;
974 mapping.vertexBaseOffset = static_cast<uint32_t>(vertexBaseOffset);
975 mapping.triangleBaseOffset = static_cast<uint32_t>(triangleBaseOffset);
976 mapping.pipelineIndex = pipelineIndex; // Cached for instance pass
977 mapping.localBoundsCenter = primitive.boundingSphere.center;
978 mapping.localBoundsRadius = primitive.boundingSphere.radius;
979
980 // Classify and build vertex shader path data for single-meshlet geometry
981 // Note: LOD primitives use the LOD cluster path, not VS path, even if base has single meshlet
982 mapping.isSingleMeshlet = (meshletCount == 1) && !hasLodHierarchy;
983 if (mapping.isSingleMeshlet) {
984 singleMeshletCount++;
985 mapping.vsIndexOffset = static_cast<uint32_t>(workingVsIndexBuffer_.size());
986
987 // Unpack PackedTriangle into contiguous uint32_t indices
988 for (const auto& tri : unpackedTriangles) {
989 workingVsIndexBuffer_.push_back(tri.packed & 0xFFu);
990 workingVsIndexBuffer_.push_back((tri.packed >> 8) & 0xFFu);
991 workingVsIndexBuffer_.push_back((tri.packed >> 16) & 0xFFu);
992 }
993 mapping.vsIndexCount = static_cast<uint32_t>(unpackedTriangles.size() * 3);
994
995 uint32_t singleMeshletIdx = static_cast<uint32_t>(workingSingleMeshletGeoData_.size());
996 workingGeometryDataBuffer_[geoDataIndex].singleMeshletGeoIndex = singleMeshletIdx;
997
998 SingleMeshletGeometryData singleGeo{};
999 singleGeo.indexCount = mapping.vsIndexCount;
1000 singleGeo.firstIndex = mapping.vsIndexOffset;
1001 singleGeo.vertexOffset = static_cast<int32_t>(vertexBaseOffset);
1002 singleGeo.pipelineIndex = pipelineIndex;
1003 workingSingleMeshletGeoData_.push_back(singleGeo);
1004 } else {
1005 multiMeshletCount++;
1006 mapping.vsIndexOffset = 0;
1007 mapping.vsIndexCount = 0;
1008 }
1009
1010 // Handle LOD hierarchy data if present
1011 if (primitive.lodHierarchy.has_value() &&
1012 primitive.lodHierarchy->lodLevelCount > 1) {
1013 const auto& lodData = *primitive.lodHierarchy;
1014
1015 // Record LOD cluster start index
1016 uint32_t clusterStart = static_cast<uint32_t>(workingClusterLodData_.size());
1017 uint32_t clusterCount = static_cast<uint32_t>(lodData.clusters.size());
1018 uint32_t groupStart = static_cast<uint32_t>(workingClusterGroupData_.size());
1019 uint32_t groupCount = static_cast<uint32_t>(lodData.groups.size());
1020
1021 // Update MeshGeometryData with LOD info
1022 workingGeometryDataBuffer_[geoDataIndex].clusterStartIndex = clusterStart;
1023 workingGeometryDataBuffer_[geoDataIndex].clusterCount = clusterCount;
1024 workingGeometryDataBuffer_[geoDataIndex].groupStartIndex = groupStart;
1025 workingGeometryDataBuffer_[geoDataIndex].groupCount = groupCount;
1026
1027 // Append LOD cluster data (with updated meshlet indices)
1028 for (const auto& cluster : lodData.clusters) {
1029 ClusterLodData clusterCopy = cluster;
1030 // Offset meshlet index to global buffer position
1031 // Note: meshletStart is the base meshlet for this geometry
1032 clusterCopy.meshletIndex = meshletStart + cluster.meshletIndex;
1033 workingClusterLodData_.push_back(clusterCopy);
1034 }
1035
1036 // Append LOD group data (group IDs are relative to this geometry's groups)
1037 for (const auto& group : lodData.groups) {
1038 ClusterGroupData groupCopy = group;
1039 workingClusterGroupData_.push_back(groupCopy);
1040 }
1041
1042 clusterCount_ += clusterCount;
1043 clusterGroupCount_ += groupCount;
1044
1045 VS_LOG(LogLOD, Warning, "[DIAG] RDM Geometry {}: clusterStart={} clusterCount={} groupStart={} groupCount={} meshletStart={} totalMeshlets={}",
1046 meshGeometryId, clusterStart, clusterCount, groupStart, groupCount, meshletStart, meshletCount);
1047
1048 // Log first cluster's data for verification
1049 if (!lodData.clusters.empty()) {
1050 const auto& firstCluster = lodData.clusters[0];
1051 VS_LOG(LogLOD, Warning, "[DIAG] RDM First cluster: error={:.4f} refinedGroupId={} meshletIndex={} (global={})",
1052 firstCluster.error, firstCluster.refinedGroupId, firstCluster.meshletIndex,
1053 meshletStart + firstCluster.meshletIndex);
1054 }
1055
1056 VS_LOG(LogLOD, Debug, "Geometry {}: {} clusters, {} groups",
1057 meshGeometryId, clusterCount, groupCount);
1058 }
1059
1060 geometryCache_[key] = mapping;
1061 cacheIt = geometryCache_.find(key); // Get iterator for instance creation
1062 meshGeometryId++;
1063 }
1064
1065 // ============================================================
1066 // INSTANCE: Create instance data using cached geometry
1067 // ============================================================
1068
1069 const MeshGeometryMapping& geoMapping = cacheIt->second;
1070
1071 // Use cached bounds from geometry mapping (avoids re-reading primitive)
1072 glm::vec3 worldCenter = worldMatrix * glm::vec4(geoMapping.localBoundsCenter, 1.0f);
1073 float worldRadius = geoMapping.localBoundsRadius * maxScale;
1074
1075 // Look up materialId from material path maps (populated by uploadMaterialBuffers())
1076 // This uses the material path directly instead of primitiveId to avoid order mismatches
1077 uint32_t materialId = 0;
1078 std::string matPathKey = primitive.materialPath.empty() ? "__default__" : primitive.materialPath.getAssetHandle();
1079
1080 // Determine which map to use based on pipeline type (need to re-lookup for cached geometry)
1081 PipelineNames lookupPipelineType = DEFAULT_MATERIAL_PIPELINE;
1082 if (!primitive.materialPath.empty() && assetManager) {
1083 std::optional<MaterialAsset*> materialAsset =
1084 assetManager->getMaterialAssetManager()->getAsset(primitive.materialPath);
1085 if (materialAsset.has_value()) {
1086 lookupPipelineType = materialAsset.value()->getType();
1087 PLOGD << "[DEBUG] Material asset found: path='" << matPathKey
1088 << "' type=" << static_cast<int>(lookupPipelineType);
1089 } else {
1090 PLOGW << "[DEBUG] Material asset NOT FOUND for path: '" << matPathKey << "'";
1091 }
1092 } else {
1093 PLOGW << "[DEBUG] No material path or no assetManager, using default. matPathKey='" << matPathKey << "'";
1094 }
1095
1096 // Select the correct map based on pipeline type
1097 switch (lookupPipelineType) {
1098 case DIFFUSE_FLAT_COLOR: {
1099 auto it = materialPathToFlatColorIndex_.find(matPathKey);
1100 if (it != materialPathToFlatColorIndex_.end()) {
1101 materialId = it->second;
1102 PLOGW << "[DEBUG] FLAT_COLOR lookup SUCCESS: matPathKey='" << matPathKey
1103 << "' -> materialId=" << materialId;
1104 } else {
1105 PLOGW << "[DEBUG] FLAT_COLOR lookup FAILED: matPathKey='" << matPathKey
1106 << "' not found in map (map size=" << materialPathToFlatColorIndex_.size() << ")";
1107 }
1108 break;
1109 }
1110 case DIFFUSE_SHADER: {
1111 auto it = materialPathToDiffuseIndex_.find(matPathKey);
1112 if (it != materialPathToDiffuseIndex_.end()) materialId = it->second;
1113 break;
1114 }
1116 case L0_SHADER:
1117 case L1_SHADER:
1118 case L2_SHADER:
1119 case DYNAMIC_TEXTURES:
1120 case STATIC_LIGHTMAP: {
1121 auto it = materialPathToPbrIndex_.find(matPathKey);
1122 if (it != materialPathToPbrIndex_.end()) {
1123 materialId = it->second;
1124 PLOGW << "[DEBUG] PBR lookup SUCCESS: matPathKey='" << matPathKey
1125 << "' -> materialId=" << materialId
1126 << " pipelineType=" << static_cast<int>(lookupPipelineType);
1127 } else {
1128 PLOGW << "[DEBUG] PBR lookup FAILED: matPathKey='" << matPathKey
1129 << "' not found in map (map size=" << materialPathToPbrIndex_.size() << ")";
1130 }
1131 break;
1132 }
1133 case NORMALS_SHADER:
1134 default: {
1135 auto it = materialPathToNormalsIndex_.find(matPathKey);
1136 if (it != materialPathToNormalsIndex_.end()) {
1137 materialId = it->second;
1138 }
1139 PLOGW << "[DEBUG] NORMALS/DEFAULT lookup: matPathKey='" << matPathKey
1140 << "' pipelineType=" << static_cast<int>(lookupPipelineType)
1141 << " materialId=" << materialId;
1142 break;
1143 }
1144 }
1145
1146 // Build instance data
1147 InstanceData instData{};
1148 instData.worldMatrix = worldMatrix;
1149 instData.meshGeometryId = geoMapping.meshGeometryId;
1150 instData.materialId = materialId;
1151 instData.colorTextureId = 0; // Texture index stored in material buffer
1152 workingInstanceData_.push_back(instData);
1153
1154 // Build instance culling data
1155 InstanceCullingData instCulling{};
1156 instCulling.worldPositionAndRadius = glm::vec4(worldCenter, worldRadius);
1157 instCulling.instanceId = instanceId;
1158 instCulling.meshGeometryId = geoMapping.meshGeometryId;
1159 workingInstanceCullingData_.push_back(instCulling);
1160
1161 // Legacy buffers for compatibility
1162 LocalBoundsData localBounds{};
1163 localBounds.localCenterAndRadius = glm::vec4(
1164 geoMapping.localBoundsCenter,
1165 geoMapping.localBoundsRadius
1166 );
1167 workingLocalBoundsDataBuffer_.push_back(localBounds);
1168
1169 ObjectCullingData culling{};
1170 culling.worldPositionAndRadius = glm::vec4(worldCenter, worldRadius);
1171 culling.objectIndex = primitiveId;
1172 workingPrimitiveCullingBuffer_.push_back(culling);
1173
1174 // Use cached pipeline index from geometry mapping (avoids redundant lookup)
1175 PrimitiveMeshletData meshletMeta{};
1176 meshletMeta.meshletStartIndex = geoMapping.meshletStartIndex;
1177 meshletMeta.meshletCount = geoMapping.meshletCount;
1178 meshletMeta.pipelineID = geoMapping.pipelineIndex;
1179 workingPrimitiveMeshletBuffer_.push_back(meshletMeta);
1180
1181 workingPerObjectData_.push_back(worldMatrix);
1182
1183 MeshPrimitiveRenderData renderData{};
1184 renderData.colorTextureId = 0; // Texture index stored in material buffer
1185 renderData.materialId = materialId;
1186 workingPrimitiveRenderData_.push_back(renderData);
1187
1188 // Record entity-to-primitive mapping for sparse transform updates
1189 entityToPrimitiveIds_[entity].push_back(primitiveId);
1190 primitiveIdToComponent_[primitiveId] = meshComp;
1191
1192 instanceId++;
1193 primitiveId++;
1194 primIdx++;
1195 }
1196 }
1197 } // End Tracy zone "Build geometry and instance data"
1198
1199 // Update incremental tracking for next update
1200 newGeometryCount = meshGeometryId - nextGeometryId_;
1201 nextGeometryId_ = meshGeometryId;
1202 nextMeshletId_ = meshletId;
1203
1204 uniqueGeometryCount_ = static_cast<uint32_t>(geometryCache_.size());
1205 singleMeshletGeometryCount_ = static_cast<uint32_t>(workingSingleMeshletGeoData_.size());
1207
1208 instanceCount_ = instanceId;
1209 primitiveCount = primitiveId;
1210 meshletCount = meshletId;
1211
1212 // Log incremental update stats
1213 if (fullRebuild) {
1214 PLOGI << "[INCREMENTAL] Full rebuild: " << uniqueGeometryCount_ << " geometries, "
1215 << instanceCount_ << " instances, " << meshletCount << " meshlets";
1216 } else {
1217 PLOGI << "[INCREMENTAL] Incremental update: " << newGeometryCount << " NEW geometries added, "
1218 << uniqueGeometryCount_ << " total cached, " << instanceCount_ << " instances";
1219 }
1220
1221 PLOGI << "[VS_DIAG] Final: singleMeshletGeometryCount_=" << singleMeshletGeometryCount_
1222 << ", multiMeshletCount=" << multiMeshletGeometryCount_;
1223
1224 // Handle empty scene
1225 if (instanceCount_ == 0) {
1226 PLOGD << "RenderingDataManager: No instances to upload";
1227 return false;
1228 }
1229
1230 // ============================================================================
1231 // Phase 2: Upload Buffers
1232 // For incremental updates, geometry buffers already contain committed data
1233 // so we upload the full working buffer (which includes old + new data)
1234 // The 2x growth strategy in VulkanStagedBuffer prevents frequent recreations
1235 // ============================================================================
1236
1237 bool anyBufferRecreated = false;
1238 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
1239
1240 // Ensure Renderer's output buffers are sized
1241 if (renderer) {
1242 anyBufferRecreated |= renderer->ensureOutputBufferSizes(primitiveCount);
1243 }
1244
1245 pendingSyncObjects_.clear();
1246
1247 // Upload instancing buffers
1248 // Geometry data buffer uses partial uploads for incremental updates
1249 if (!workingGeometryDataBuffer_.empty()) {
1250 size_t totalSize = workingGeometryDataBuffer_.size() * sizeof(MeshGeometryData);
1251 anyBufferRecreated |= meshGeometryDataBuffer->ensureSize(totalSize, storageUsage);
1252
1253 if (fullRebuild || anyBufferRecreated) {
1256 size_t newCount = workingGeometryDataBuffer_.size() - committedGeometryCount_;
1257 size_t newSize = newCount * sizeof(MeshGeometryData);
1258 size_t offset = committedGeometryCount_ * sizeof(MeshGeometryData);
1259 PLOGI << "[INCREMENTAL] Partial geometry data upload: " << newCount << " new geometries";
1260 pendingSyncObjects_.push_back(meshGeometryDataBuffer->uploadPartial(context,
1261 workingGeometryDataBuffer_.data() + committedGeometryCount_, newSize, offset));
1262 }
1263 }
1264
1265 size_t instDataSize = workingInstanceData_.size() * sizeof(InstanceData);
1266 anyBufferRecreated |= instanceDataBuffer->ensureSize(instDataSize, storageUsage);
1267 pendingSyncObjects_.push_back(instanceDataBuffer->upload(context, workingInstanceData_.data(), instDataSize));
1268
1269 size_t instCullSize = workingInstanceCullingData_.size() * sizeof(InstanceCullingData);
1270 anyBufferRecreated |= instanceCullingDataBuffer->ensureSize(instCullSize, storageUsage);
1271 pendingSyncObjects_.push_back(instanceCullingDataBuffer->upload(context, workingInstanceCullingData_.data(), instCullSize));
1272
1273 // Upload legacy buffers for shader compatibility
1274 size_t cullingSize = workingPrimitiveCullingBuffer_.size() * sizeof(ObjectCullingData);
1275 anyBufferRecreated |= primitiveCullingData->ensureSize(cullingSize, storageUsage);
1276 pendingSyncObjects_.push_back(primitiveCullingData->upload(context, workingPrimitiveCullingBuffer_.data(), cullingSize));
1277
1278 size_t localBoundsSize = workingLocalBoundsDataBuffer_.size() * sizeof(LocalBoundsData);
1279 anyBufferRecreated |= localBoundsBuffer->ensureSize(localBoundsSize, storageUsage);
1280 pendingSyncObjects_.push_back(localBoundsBuffer->upload(context, workingLocalBoundsDataBuffer_.data(), localBoundsSize));
1281
1282 size_t meshletMetaSize = workingPrimitiveMeshletBuffer_.size() * sizeof(PrimitiveMeshletData);
1283 anyBufferRecreated |= primitiveMeshletData->ensureSize(meshletMetaSize, storageUsage);
1284 pendingSyncObjects_.push_back(primitiveMeshletData->upload(context, workingPrimitiveMeshletBuffer_.data(), meshletMetaSize));
1285
1286 size_t transformSize = workingPerObjectData_.size() * sizeof(glm::mat4);
1287 anyBufferRecreated |= perObjectDataBuffer->ensureSize(transformSize, storageUsage);
1288 pendingSyncObjects_.push_back(perObjectDataBuffer->upload(context, workingPerObjectData_.data(), transformSize));
1289
1290 size_t renderDataSize = workingPrimitiveRenderData_.size() * sizeof(MeshPrimitiveRenderData);
1291 anyBufferRecreated |= primitiveRenderData->ensureSize(renderDataSize, storageUsage);
1292 pendingSyncObjects_.push_back(primitiveRenderData->upload(context, workingPrimitiveRenderData_.data(), renderDataSize));
1293
1294 // Debug: Count meshlets per pipeline type (instanced path)
1295 {
1296 std::array<uint32_t, 16> meshletCountPerPipeline{};
1297 for (const auto& meshlet : workingMeshletDataBuffer_) {
1298 if (meshlet.pipelineIndex < meshletCountPerPipeline.size()) {
1299 meshletCountPerPipeline[meshlet.pipelineIndex]++;
1300 }
1301 }
1302 PLOGI << "[DEBUG-INSTANCED] Meshlets per pipeline (total " << workingMeshletDataBuffer_.size() << "):";
1303 for (uint32_t i = 0; i < 9; ++i) {
1304 if (meshletCountPerPipeline[i] > 0) {
1305 PLOGI << " Pipeline " << i << ": " << meshletCountPerPipeline[i] << " meshlets";
1306 }
1307 }
1308 }
1309
1310 // Upload shared geometry data
1311 // For incremental updates: only upload NEW data (from committed offset to end)
1312 // For full rebuild: upload everything
1313 if (!workingMeshletDataBuffer_.empty()) {
1314 size_t totalSize = workingMeshletDataBuffer_.size() * sizeof(UnifiedMeshlet);
1315 anyBufferRecreated |= meshletBuffer->ensureSize(totalSize, storageUsage);
1316
1317 if (fullRebuild || anyBufferRecreated) {
1318 pendingSyncObjects_.push_back(meshletBuffer->upload(context, workingMeshletDataBuffer_.data(), totalSize));
1320 size_t newCount = workingMeshletDataBuffer_.size() - committedMeshletCount_;
1321 size_t newSize = newCount * sizeof(UnifiedMeshlet);
1322 size_t offset = committedMeshletCount_ * sizeof(UnifiedMeshlet);
1323 PLOGI << "[INCREMENTAL] Partial meshlet upload: " << newCount << " new meshlets";
1324 pendingSyncObjects_.push_back(meshletBuffer->uploadPartial(context,
1325 workingMeshletDataBuffer_.data() + committedMeshletCount_, newSize, offset));
1326 }
1327 }
1328
1329 if (!workingMeshletBoundsBuffer_.empty()) {
1330 size_t totalSize = workingMeshletBoundsBuffer_.size() * sizeof(MeshletBounds);
1331 anyBufferRecreated |= meshletBoundsData->ensureSize(totalSize, storageUsage);
1332
1333 if (fullRebuild || anyBufferRecreated) {
1334 pendingSyncObjects_.push_back(meshletBoundsData->upload(context, workingMeshletBoundsBuffer_.data(), totalSize));
1336 size_t newCount = workingMeshletBoundsBuffer_.size() - committedMeshletCount_;
1337 size_t newSize = newCount * sizeof(MeshletBounds);
1338 size_t offset = committedMeshletCount_ * sizeof(MeshletBounds);
1339 pendingSyncObjects_.push_back(meshletBoundsData->uploadPartial(context,
1340 workingMeshletBoundsBuffer_.data() + committedMeshletCount_, newSize, offset));
1341 }
1342 }
1343
1344 if (!workingVertexBuffer_.empty()) {
1345 size_t totalSize = workingVertexBuffer_.size() * sizeof(Vertex);
1346 const VkBufferUsageFlags vertexBufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
1347 anyBufferRecreated |= vertexBuffer->ensureSize(totalSize, vertexBufferUsage);
1348
1349 if (fullRebuild || anyBufferRecreated) {
1350 PLOGI << "[INSTANCING] Full vertex buffer upload (" << (totalSize / 1024) << " KB)";
1351 pendingSyncObjects_.push_back(vertexBuffer->upload(context, workingVertexBuffer_.data(), totalSize));
1352 } else if (workingVertexBuffer_.size() > committedVertexCount_) {
1353 size_t newCount = workingVertexBuffer_.size() - committedVertexCount_;
1354 size_t newSize = newCount * sizeof(Vertex);
1355 size_t offset = committedVertexCount_ * sizeof(Vertex);
1356 PLOGI << "[INCREMENTAL] Partial vertex upload: " << newCount << " new vertices (" << (newSize / 1024) << " KB)";
1357 pendingSyncObjects_.push_back(vertexBuffer->uploadPartial(context,
1358 workingVertexBuffer_.data() + committedVertexCount_, newSize, offset));
1359 }
1360 }
1361
1362 if (!workingTriangleBuffer_.empty()) {
1363 size_t totalSize = workingTriangleBuffer_.size() * sizeof(PackedTriangle);
1364 anyBufferRecreated |= triangleBuffer->ensureSize(totalSize, storageUsage);
1365
1366 if (fullRebuild || anyBufferRecreated) {
1367 PLOGI << "[INSTANCING] Full triangle buffer upload (" << (totalSize / 1024) << " KB)";
1368 pendingSyncObjects_.push_back(triangleBuffer->upload(context, workingTriangleBuffer_.data(), totalSize));
1369 } else if (workingTriangleBuffer_.size() > committedTriangleCount_) {
1370 size_t newCount = workingTriangleBuffer_.size() - committedTriangleCount_;
1371 size_t newSize = newCount * sizeof(PackedTriangle);
1372 size_t offset = committedTriangleCount_ * sizeof(PackedTriangle);
1373 PLOGI << "[INCREMENTAL] Partial triangle upload: " << newCount << " new triangles (" << (newSize / 1024) << " KB)";
1374 pendingSyncObjects_.push_back(triangleBuffer->uploadPartial(context,
1375 workingTriangleBuffer_.data() + committedTriangleCount_, newSize, offset));
1376 }
1377 }
1378
1379 // Upload vertex shader path buffers
1380 if (!workingVsIndexBuffer_.empty()) {
1381 size_t totalSize = workingVsIndexBuffer_.size() * sizeof(uint32_t);
1382 const VkBufferUsageFlags vsIndexUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
1383 anyBufferRecreated |= vsIndexBuffer_->ensureSize(totalSize, vsIndexUsage);
1384
1385 if (fullRebuild || anyBufferRecreated) {
1386 PLOGI << "[VS_PATH] Full VS index buffer upload (" << workingVsIndexBuffer_.size() << " indices)";
1387 pendingSyncObjects_.push_back(vsIndexBuffer_->upload(context, workingVsIndexBuffer_.data(), totalSize));
1388 } else if (workingVsIndexBuffer_.size() > committedVsIndexCount_) {
1389 size_t newCount = workingVsIndexBuffer_.size() - committedVsIndexCount_;
1390 size_t newSize = newCount * sizeof(uint32_t);
1391 size_t offset = committedVsIndexCount_ * sizeof(uint32_t);
1392 PLOGI << "[INCREMENTAL] Partial VS index upload: " << newCount << " new indices";
1393 pendingSyncObjects_.push_back(vsIndexBuffer_->uploadPartial(context,
1394 workingVsIndexBuffer_.data() + committedVsIndexCount_, newSize, offset));
1395 }
1396 }
1397
1398 if (!workingSingleMeshletGeoData_.empty()) {
1399 size_t totalSize = workingSingleMeshletGeoData_.size() * sizeof(SingleMeshletGeometryData);
1400 anyBufferRecreated |= singleMeshletGeometryBuffer_->ensureSize(totalSize, storageUsage);
1401
1402 if (fullRebuild || anyBufferRecreated) {
1403 PLOGI << "[VS_PATH] Full single-meshlet geometry upload (" << workingSingleMeshletGeoData_.size() << " geometries)";
1407 size_t newSize = newCount * sizeof(SingleMeshletGeometryData);
1409 PLOGI << "[INCREMENTAL] Partial single-meshlet geometry upload: " << newCount << " new geometries";
1412 }
1413 }
1414
1415 // Upload LOD cluster buffers
1416 VS_LOG(LogLOD, Warning, "[DIAG] RDM Upload: clusterCount_={} clusterGroupCount_={} workingClusters={} workingGroups={}",
1418
1419 if (!workingClusterLodData_.empty()) {
1420 size_t totalSize = workingClusterLodData_.size() * sizeof(ClusterLodData);
1421 anyBufferRecreated |= clusterLodDataBuffer_->ensureSize(totalSize, storageUsage);
1422
1423 if (fullRebuild || anyBufferRecreated) {
1424 VS_LOG(LogLOD, Warning, "[DIAG] Uploading {} clusters to GPU ({} bytes)", workingClusterLodData_.size(), totalSize);
1425 VS_LOG(LogLOD, Info, "Full cluster LOD data upload ({} clusters)", workingClusterLodData_.size());
1426 pendingSyncObjects_.push_back(clusterLodDataBuffer_->upload(context, workingClusterLodData_.data(), totalSize));
1427 }
1428 } else {
1429 VS_LOG(LogLOD, Warning, "[DIAG] No LOD cluster data to upload (workingClusterLodData_ is empty)");
1430 }
1431
1432 if (!workingClusterGroupData_.empty()) {
1433 size_t totalSize = workingClusterGroupData_.size() * sizeof(ClusterGroupData);
1434 anyBufferRecreated |= clusterGroupDataBuffer_->ensureSize(totalSize, storageUsage);
1435
1436 if (fullRebuild || anyBufferRecreated) {
1437 VS_LOG(LogLOD, Warning, "[DIAG] Uploading {} groups to GPU ({} bytes)", workingClusterGroupData_.size(), totalSize);
1438 VS_LOG(LogLOD, Info, "Full cluster group data upload ({} groups)", workingClusterGroupData_.size());
1439 pendingSyncObjects_.push_back(clusterGroupDataBuffer_->upload(context, workingClusterGroupData_.data(), totalSize));
1440 }
1441 }
1442
1443 PLOGI << "[INSTANCING] Memory savings: geometry shared across " << instanceCount_
1444 << " instances instead of duplicated";
1445
1446 // Update committed counts (data is now on GPU after upload)
1453
1454 if (anyBufferRecreated) {
1455 PLOGI << "[INSTANCING] Buffer(s) were recreated - descriptor sets need updating";
1456 }
1457
1458 dataVersion_++;
1459 PLOGI << "[INSTANCING] Data version incremented to " << dataVersion_;
1460
1461 return anyBufferRecreated;
1462 }
1463
1465 {
1466 geometryCache_.clear();
1467 geometryDirty_ = true;
1468 geometryNeedsFullRebuild_ = true; // Force full rebuild on next update
1469 markDirty();
1470 PLOGI << "Geometry cache invalidated - full rebuild required";
1471 }
1472
1474 {
1475 // GPU Transform Optimization: Only upload world matrices each frame.
1476 // The GPU compute shader (PrimitiveCulling.comp) transforms local bounds to world space
1477 // using the localBoundsBuffer (static) + perObjectDataBuffer (updated here).
1478 //
1479 // OPTIMIZATION: Use cached transforms and only update dirty entities.
1480 // - First frame after structural change: full iteration to populate cache
1481 // - Subsequent frames: only iterate entities with TransformDirtyRenderer tag
1482
1483 // Skip if no primitives exist yet (structure not built)
1484 if (primitiveCount == 0) {
1485 return false;
1486 }
1487
1488 // Get the ECS registry for dirty tracking and mesh component iteration
1489 entt::registry& registry = Ecs::RegistryManager::get();
1490
1491 // Check if we need a full rebuild or can do incremental update
1492 if (!transformCacheValid_) {
1493 // FULL REBUILD: Cache is invalid, iterate all mesh components via ECS
1494 TRACY_ZONE_SCOPED_NAMED("Full transform rebuild");
1495
1496 cachedTransforms_.clear();
1498
1499 primitive_rendering_id primitiveId = 0;
1500 auto meshView = registry.view<Ecs::MeshComponentRef>();
1501
1502 for (auto entity : meshView) {
1503 MeshComponent* meshComp = meshView.get<Ecs::MeshComponentRef>(entity).component;
1504 if (!meshComp || !meshComp->isVisible()) continue;
1505
1506 MeshAsset* asset = meshComp->getMeshAsset();
1507 if (!asset) continue;
1508
1509 // Inline validation (same as updatePrimitiveDataInstanced)
1510 auto* meshData = asset->getMeshPrimitiveData();
1511 if (!meshData) continue;
1512
1513 glm::mat4 worldMatrix = meshComp->getWorldTransform();
1514
1515 for (const Ecs::MeshPrimitive& primitive : meshData->primitives) {
1516 // Must match the same conditions as updatePrimitiveDataInstanced
1517 if (!primitive.meshletData.has_value() || !primitive.unpackedData.has_value()) continue;
1518 cachedTransforms_.push_back(worldMatrix);
1519 primitiveId++;
1520 }
1521 }
1522
1523 // Verify we collected the expected number of primitives
1524 if (primitiveId != primitiveCount) {
1525 PLOGW << "Transform update collected " << primitiveId
1526 << " primitives but expected " << primitiveCount
1527 << ". Structure may have changed - triggering full rebuild.";
1528 markDirty();
1529 return false;
1530 }
1531
1532 transformCacheValid_ = true;
1533
1534 // Clear all TransformDirtyRenderer tags after full rebuild
1535 // Must collect first, then remove (can't modify while iterating)
1536 {
1537 std::vector<entt::entity> entitiesToClear;
1538 auto dirtyView = registry.view<Ecs::TransformDirtyRenderer>();
1539 for (auto entity : dirtyView) {
1540 entitiesToClear.push_back(entity);
1541 }
1542 for (auto entity : entitiesToClear) {
1543 registry.remove<Ecs::TransformDirtyRenderer>(entity);
1544 }
1545 }
1546
1547 PLOGD << "RenderingDataManager: Full transform rebuild for " << primitiveCount << " primitives";
1548 } else {
1549 // INCREMENTAL UPDATE: Only update dirty entities
1550 TRACY_ZONE_SCOPED_NAMED("Incremental transform update");
1551
1552 // Collect dirty entities (can't remove while iterating)
1553 std::vector<entt::entity> dirtyEntities;
1554 auto dirtyView = registry.view<Ecs::TransformDirtyRenderer>();
1555 for (auto entity : dirtyView) {
1556 dirtyEntities.push_back(entity);
1557 }
1558
1559 // If no dirty entities, skip the upload entirely!
1560 if (dirtyEntities.empty()) {
1561 PLOGD << "RenderingDataManager: No dirty transforms, skipping update";
1562 return false;
1563 }
1564
1565 uint32_t updatedCount = 0;
1566
1567 for (entt::entity entity : dirtyEntities) {
1568 // Look up primitive IDs for this entity
1569 auto it = entityToPrimitiveIds_.find(entity);
1570 if (it == entityToPrimitiveIds_.end()) {
1571 continue; // Entity has no associated primitives
1572 }
1573
1574 for (primitive_rendering_id primId : it->second) {
1575 // Get the MeshComponent for this primitive
1576 auto compIt = primitiveIdToComponent_.find(primId);
1577 if (compIt == primitiveIdToComponent_.end()) {
1578 continue;
1579 }
1580
1581 MeshComponent* meshComp = compIt->second;
1582 if (!meshComp || !meshComp->isVisible()) {
1583 continue;
1584 }
1585
1586 // Update the cached transform
1587 if (primId < cachedTransforms_.size()) {
1588 cachedTransforms_[primId] = meshComp->getWorldTransform();
1589 updatedCount++;
1590 }
1591 }
1592 }
1593
1594 // Clear TransformDirtyRenderer tags after processing
1595 for (auto entity : dirtyEntities) {
1596 registry.remove<Ecs::TransformDirtyRenderer>(entity);
1597 }
1598
1599 PLOGD << "RenderingDataManager: Updated " << updatedCount << " dirty transforms out of " << primitiveCount;
1600 }
1601
1602 // Clear any previous transform sync objects
1604
1605 // Upload the cached transforms
1606 size_t transformSize = cachedTransforms_.size() * sizeof(glm::mat4);
1608 perObjectDataBuffer->upload(context, cachedTransforms_.data(), transformSize));
1609
1610 return true;
1611 }
1612
1614 {
1615 markDirty();
1616 }
1617
1622
1624 {
1625 PLOGI << "RenderingDataManager::onMeshLoaded called - queuing for next frame";
1626 // Queue the mesh load for processing at a safe point
1627 // This prevents the mesh from becoming visible mid-frame
1628 pendingMeshLoads_.push_back(asset);
1629 }
1630
1632 {
1633 // Invalidate geometry cache since mesh data is being removed
1634 // This ensures we rebuild the cache without stale references
1636 }
1637
1638 void RenderingDataManager::onTextureLoaded( TextureAsset * textureAsset, const std::filesystem::path& texturePath )
1639 {
1640 PLOGI << "onTextureLoaded called with path: " << texturePath;
1641
1642 if (!textureAsset) {
1643 PLOGW << "onTextureLoaded called with null textureAsset";
1644 markDirty();
1645 return;
1646 }
1647
1648 // Get the image data from the ECS entity
1649 entt::entity entity = textureAsset->getEntity();
1650 if (entity == entt::null) {
1651 PLOGW << "onTextureLoaded: TextureAsset has null entity for path: " << texturePath;
1652 markDirty();
1653 return;
1654 }
1655
1656 auto& registry = Ecs::RegistryManager::get();
1657 if (!registry.all_of<Ecs::ImageData>(entity)) {
1658 PLOGW << "onTextureLoaded: No ImageData component for path: " << texturePath;
1659 markDirty();
1660 return;
1661 }
1662
1663 // Get AssetManager to register the Vulkan texture
1664 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
1665 if (!assetManager) {
1666 PLOGW << "onTextureLoaded: AssetManager not available";
1667 markDirty();
1668 return;
1669 }
1670
1671 // Check if texture is already registered (avoid duplicates)
1672 if (assetManager->getTextureDescriptorIndex(texturePath) != 0xFFFFFFFF) {
1673 PLOGD << "onTextureLoaded: Texture already registered: " << texturePath;
1674 markDirty();
1675 return;
1676 }
1677
1678 // Get the image data and create a Vulkan Texture
1679 const auto& imageData = registry.get<Ecs::ImageData>(entity);
1680
1681 PLOGD << "onTextureLoaded - entity=" << static_cast<uint32_t>(entity)
1682 << " path=" << texturePath
1683 << " dims=" << imageData.image.width << "x" << imageData.image.height;
1684
1685 // Get texture type from the registry (was set at import time for correct format)
1686 TextureHandleRegistry* handleRegistry = assetManager->getTextureHandleRegistry();
1687 TextureType textureType = handleRegistry->getTextureTypeForPath(texturePath);
1688
1689 // If texture type wasn't pre-registered, default to BaseColor (SRGB)
1690 // This maintains backward compatibility for textures loaded directly
1691 if (textureType == TextureType::Unknown) {
1692 textureType = TextureType::BaseColor;
1693 PLOGD << "onTextureLoaded: No pre-registered type for " << texturePath << ", defaulting to BaseColor";
1694 }
1695
1696 // Create a Texture object with type-aware format selection
1697 // This ensures normal maps use LINEAR format, not SRGB!
1698 Texture vulkanTexture(imageData.image, textureType, context);
1699
1700 // Register with AssetManager to get a descriptor index
1701 Texture* registeredTexture = assetManager->registerTexture(texturePath, std::move(vulkanTexture));
1702 if (registeredTexture) {
1703 PLOGI << "onTextureLoaded: Created Vulkan texture for " << texturePath
1704 << " with descriptor index " << registeredTexture->getDescriptorIndex()
1705 << " type=" << static_cast<int>(textureType);
1706
1707 // Update TextureHandleRegistry with the loaded texture and descriptor index
1708 handleRegistry->onTextureLoaded(texturePath, registeredTexture, registeredTexture->getDescriptorIndex());
1709 } else {
1710 PLOGW << "onTextureLoaded: Failed to register texture: " << texturePath;
1711 }
1712
1713 markDirty();
1714 }
1715
1717 {
1718 markDirty();
1719 }
1720
1722 {
1723 // Mark dirty so material will be collected in next update
1724 markDirty();
1725 }
1726
1728 {
1729 // Mark dirty so material mappings will be rebuilt
1730 markDirty();
1731 }
1732
1734 {
1735 // Resolve dependencies from Engine or injected members
1736 SceneManager* sceneManager = engine ? engine->getSceneManager().get() : injectedSceneManager;
1737 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
1738
1739 // Skip if required dependencies are not available
1740 if (!sceneManager) {
1741 PLOGD << "SceneManager not available, skipping material collection";
1742 return;
1743 }
1744
1745 // Clear existing material mappings
1746 primitiveToPipeline.clear();
1748
1749 primitive_rendering_id primitiveId = 0;
1750
1751 // Iterate through all visible mesh components
1752 std::vector<Actor*> allActors = sceneManager->getAllActors();
1753
1754 for (Actor* actor : allActors) {
1755 if (!actor->hasComponent<MeshComponent>()) continue;
1756
1757 for (auto* meshComp : actor->getComponents<MeshComponent>()) {
1758 if (!meshComp->isVisible()) continue;
1759
1760 MeshAsset* asset = meshComp->getMeshAsset();
1761 if (!asset) continue;
1762
1763 auto* meshData = asset->getMeshPrimitiveData();
1764 if (!meshData) continue;
1765
1766 for (const auto& primitive : meshData->primitives) {
1767 if (!primitive.meshletData.has_value()) continue;
1768
1769 // Look up material and get pipeline type
1770 PipelineNames pipelineType = NORMALS_SHADER; // default fallback
1771
1772 if (!primitive.materialPath.empty() && assetManager) {
1773 std::optional<MaterialAsset*> materialAsset =
1774 assetManager->getMaterialAssetManager()->getAsset(primitive.materialPath);
1775
1776 if (materialAsset.has_value()) {
1777 pipelineType = materialAsset.value()->getType();
1778 }
1779 }
1780
1781 // Store mapping
1782 primitiveToPipeline[primitiveId] = pipelineType;
1783
1784 // Increment count for this pipeline
1785 pipelinePrimitiveCounts[pipelineType]++;
1786
1787 primitiveId++;
1788 }
1789 }
1790 }
1791
1792 PLOGI << "RenderingDataManager: Collected materials for " << primitiveId << " primitives";
1793
1794 // Log per-pipeline counts
1795 for (const auto& [pipeline, count] : pipelinePrimitiveCounts) {
1796 PLOGD << " Pipeline " << static_cast<int>(pipeline) << ": " << count << " primitives";
1797 }
1798 }
1799
1801 {
1802 auto it = primitiveToPipeline.find(primitiveId);
1803 if (it != primitiveToPipeline.end()) {
1804 return it->second;
1805 }
1806 return NORMALS_SHADER; // default fallback
1807 }
1808
1810 {
1811 PLOGD << "markDirty() called - setting isDirty=true";
1812 isDirty = true;
1813 transformCacheValid_ = false; // Force full transform rebuild on next update
1814 }
1815
1817 {
1818 if (pendingMeshLoads_.empty()) {
1819 return;
1820 }
1821
1822 PLOGI << "Processing " << pendingMeshLoads_.size() << " pending mesh loads";
1823
1824 // Mark dirty since we have new meshes to include
1825 markDirty();
1826
1827 // Clear the queue - the meshes are now acknowledged
1828 // They will be picked up in snapshotRenderableMeshes()
1829 pendingMeshLoads_.clear();
1830 }
1831
1833 std::vector<Ecs::CompletedMeshletGeneration>&& completions)
1834 {
1835 if (completions.empty()) {
1836 return;
1837 }
1838
1839 PLOGI << "Processing " << completions.size() << " completed meshlet generations";
1840
1841 auto& registry = Ecs::RegistryManager::get();
1842 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
1843
1844 for (auto& completion : completions) {
1845 // Get the MeshPrimitiveData from the registry
1846 Ecs::MeshPrimitiveData* primitiveData = registry.try_get<Ecs::MeshPrimitiveData>(completion.meshEntity);
1847 if (!primitiveData) {
1848 PLOGW << "MeshPrimitiveData not found for entity during meshlet completion processing";
1849 continue;
1850 }
1851
1852 // Ensure we have matching primitive counts
1853 if (completion.primitiveResults.size() != primitiveData->primitives.size()) {
1854 PLOGW << "Primitive count mismatch: " << completion.primitiveResults.size()
1855 << " vs " << primitiveData->primitives.size();
1856 continue;
1857 }
1858
1859 // Write the completed data to the registry
1860 PLOGI << "processCompletedMeshletGenerations: Writing meshlet data for " << completion.meshPath.getAssetHandle()
1861 << " entity=" << static_cast<uint32_t>(completion.meshEntity)
1862 << " (" << completion.primitiveResults.size() << " primitives)";
1863 for (size_t i = 0; i < completion.primitiveResults.size(); ++i) {
1864 auto& result = completion.primitiveResults[i];
1865 auto& prim = primitiveData->primitives[i];
1866
1867 // Log materialPath BEFORE writing meshlet data
1868 PLOGI << " primitive[" << i << "] materialPath BEFORE write: "
1869 << (prim.materialPath.empty() ? "(empty)" : prim.materialPath.getAssetHandle());
1870
1871 // Write optimized vertex/index data (raw data was moved directly to background task)
1872 prim.data = std::move(result.optimizedData);
1873
1874 // Write meshlet data
1875 prim.meshletData = std::move(result.meshletData);
1876
1877 // Write pre-unpacked GPU-ready data (generated on background thread)
1878 prim.unpackedData = std::move(result.unpackedData);
1879
1880 // Write LOD hierarchy (if generated)
1881 prim.lodHierarchy = std::move(result.lodHierarchy);
1882
1883 // Write bounding sphere
1884 prim.boundingSphere = result.boundingSphere;
1885
1886 // Log materialPath AFTER writing meshlet data (should be unchanged)
1887 PLOGI << " primitive[" << i << "] materialPath AFTER write: "
1888 << (prim.materialPath.empty() ? "(empty)" : prim.materialPath.getAssetHandle());
1889 }
1890
1891 // Write mesh bounding sphere
1892 primitiveData->boundingSphere = completion.meshBoundingSphere;
1893
1894 PLOGD << "Wrote meshlet data to registry for: " << completion.meshPath.getFilePath();
1895
1896 // Now notify that the mesh is loaded (this queues for next safe point)
1897 if (assetManager) {
1898 auto meshAssetOpt = assetManager->getMeshAssetManager()->getAsset(completion.meshPath);
1899 if (meshAssetOpt.has_value()) {
1900 onMeshLoaded(meshAssetOpt.value());
1901 }
1902 }
1903 }
1904 }
1905
1907 {
1908 TRACY_ZONE_SCOPED_NAMED("Snapshot renderable meshes");
1910
1911 // Use ECS view for efficient iteration - directly queries MeshComponents
1912 // without iterating all actors
1913 entt::registry& registry = Ecs::RegistryManager::get();
1914 auto meshView = registry.view<Ecs::MeshComponentRef>();
1915
1916 for (auto entity : meshView) {
1917 MeshComponent* meshComp = meshView.get<Ecs::MeshComponentRef>(entity).component;
1918 if (!meshComp || !meshComp->isVisible()) continue;
1919
1920 MeshAsset* asset = meshComp->getMeshAsset();
1921 if (!asset) continue;
1922
1923 // Only include if the mesh has valid primitive data
1924 auto* meshData = asset->getMeshPrimitiveData();
1925 if (!meshData) continue;
1926
1927 renderCycleMeshSnapshot_.insert(asset);
1928 }
1929
1930 PLOGD << "Snapshot captured " << renderCycleMeshSnapshot_.size() << " render-ready meshes";
1931 }
1932
1934 {
1935 return renderCycleMeshSnapshot_.contains(asset);
1936 }
1937
1939 {
1940 if (texture) {
1941 texturesToUpload.push_back(texture);
1942 }
1943 }
1944
1946 {
1947 return !texturesToUpload.empty();
1948 }
1949
1950 std::vector<Texture*> RenderingDataManager::getTexturesToUpload() const
1951 {
1952 return texturesToUpload;
1953 }
1954
1956 {
1957 // Move uploaded textures to GPU texture list
1958 gpuTextures.insert(gpuTextures.end(), texturesToUpload.begin(), texturesToUpload.end());
1959 texturesToUpload.clear();
1960 }
1961
1962 std::vector<VkDescriptorImageInfo> RenderingDataManager::generateTextureDescriptorInfos() const
1963 {
1964 std::vector<VkDescriptorImageInfo> descriptorInfos;
1965 descriptorInfos.reserve(gpuTextures.size());
1966
1967 for (const Texture* texture : gpuTextures) {
1968 if (texture && texture->getVkImageView() != VK_NULL_HANDLE) {
1969 VkDescriptorImageInfo imageInfo{
1970 .sampler = texture->getVkImageSampler(),
1971 .imageView = texture->getVkImageView(),
1972 .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
1973 };
1974 descriptorInfos.push_back(imageInfo);
1975 }
1976 }
1977
1978 return descriptorInfos;
1979 }
1980
1982 {
1983 return static_cast<uint32_t>(gpuTextures.size());
1984 }
1985
1987 {
1988 auto it = materialBuffersByPipeline.find(pipelineName);
1989 if (it != materialBuffersByPipeline.end() && it->second.has_value()) {
1990 return it->second.value();
1991 }
1992
1993 // Should never happen if initialized properly
1994 throw std::runtime_error("Material buffer for pipeline " + std::to_string(static_cast<int>(pipelineName)) + " not found");
1995 }
1996
1998 {
1999 // List of all pipeline types that need material buffers
2000 const std::vector<PipelineNames> pipelineTypes = {
2005 L0_SHADER,
2006 L1_SHADER,
2007 L2_SHADER,
2010 };
2011
2012 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
2013 const VkMemoryPropertyFlags memProps = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
2014 constexpr size_t MATERIAL_BUFFER_SIZE = 256; // Minimal placeholder size for now
2015
2016 for (PipelineNames pipelineName : pipelineTypes) {
2017 materialBuffersByPipeline[pipelineName].emplace(context);
2018 materialBuffersByPipeline[pipelineName]->create(MATERIAL_BUFFER_SIZE, storageUsage, memProps);
2019
2020 // Set debug name based on pipeline type
2021 std::string bufferName = "Material Buffer Pipeline " + std::to_string(static_cast<int>(pipelineName));
2022 materialBuffersByPipeline[pipelineName]->setDebugName(bufferName);
2023 }
2024
2025 PLOGI << "Initialized " << materialBuffersByPipeline.size() << " material buffers";
2026 }
2027
2029 {
2030 // Resolve dependencies from Engine or injected members
2031 AssetManager* assetManager = engine ? engine->getAssetManager().get() : injectedAssetManager;
2032
2033 // Skip if required dependencies are not available
2034 if (!assetManager) {
2035 PLOGD << "AssetManager not available, skipping material buffer upload";
2036 return;
2037 }
2038
2039 // Clear working buffers for each pipeline type
2042 workingPbrMaterials_.clear();
2044
2045 // Clear material path to buffer index maps
2050
2051 // Add default fallback materials at index 0 for each buffer type
2052 // This ensures that when material lookups fail (returning materialId=0),
2053 // the shader gets a safe material with all texture indices set to TEXTURE_NOT_PRESENT
2054 {
2055 GpuDiffuseFlatColorMaterial fallbackFlatColor{};
2056 fallbackFlatColor.color = glm::vec3(1.0f, 0.0f, 1.0f); // Magenta for visibility
2057 fallbackFlatColor._padding = 0.0f;
2058 workingFlatColorMaterials_.push_back(fallbackFlatColor);
2059
2060 GpuDiffuseShaderMaterial fallbackDiffuse{};
2061 fallbackDiffuse.baseColorTextureIndex = 0xFFFFFFFF;
2062 fallbackDiffuse.baseColorFactor = glm::vec4(1.0f, 0.0f, 1.0f, 1.0f); // Magenta
2063 workingDiffuseMaterials_.push_back(fallbackDiffuse);
2064
2065 GpuPbrMaterial fallbackPbr{};
2066 fallbackPbr.baseColorTextureIndex = 0xFFFFFFFF;
2067 fallbackPbr.normalTextureIndex = 0xFFFFFFFF;
2068 fallbackPbr.roughnessMetallicTextureIndex = 0xFFFFFFFF;
2069 fallbackPbr.emissiveTextureIndex = 0xFFFFFFFF;
2070 fallbackPbr.lightmapTextureIndex = 0xFFFFFFFF;
2071 fallbackPbr.baseColorFactor = glm::vec4(1.0f, 0.0f, 1.0f, 1.0f); // Magenta for visibility
2072 fallbackPbr.emissiveFactor = glm::vec3(0.0f);
2073 fallbackPbr.roughnessFactor = 1.0f;
2074 fallbackPbr.metallicFactor = 0.0f;
2075 fallbackPbr.normalScale = 1.0f;
2076 fallbackPbr.sh_scale = 0.15f;
2077 fallbackPbr.ambient_term = 1.0f;
2078 fallbackPbr.debug_mode = 0;
2079 fallbackPbr.tone_mapping = 0;
2080 fallbackPbr.hasLightmap = 0;
2081 workingPbrMaterials_.push_back(fallbackPbr);
2082
2083 GpuNormalsMaterial fallbackNormals{};
2084 fallbackNormals.debug = 0;
2085 workingNormalsMaterials_.push_back(fallbackNormals);
2086
2087 PLOGD << "Added fallback materials at index 0 for all material buffers";
2088 }
2089
2090 // Collect materials directly from MaterialAssetManager
2091 // This decouples material collection from MeshPrimitiveData availability,
2092 // allowing materials to be collected as soon as they're loaded
2093 auto* materialAssetManager = assetManager->getMaterialAssetManager();
2094
2095 size_t totalMaterials = 0;
2096 size_t loadedMaterials = 0;
2097
2098 materialAssetManager->forEachAsset([&](const Asset::Path& matPath, MaterialAsset* materialAsset) {
2099 totalMaterials++;
2100
2101 // Only process loaded materials
2102 if (materialAsset->getLoadingState() != Asset::LOADED) {
2103 return; // Skip unloaded materials
2104 }
2105 loadedMaterials++;
2106
2107 PipelineNames pipelineType = materialAsset->getType();
2108 std::string matPathKey = matPath.getAssetHandle();
2109
2110 PLOGD << "uploadMaterialBuffers: processing material " << matPathKey
2111 << " pipelineType=" << static_cast<int>(pipelineType);
2112
2113 // Convert material data to GPU format based on pipeline type
2114 switch (pipelineType) {
2115 case DIFFUSE_FLAT_COLOR: {
2116 // Check if material already added to buffer (shouldn't happen with forEachAsset, but safety check)
2117 if (materialPathToFlatColorIndex_.contains(matPathKey)) {
2118 return;
2119 }
2120 uint32_t materialId = static_cast<uint32_t>(workingFlatColorMaterials_.size());
2121 materialPathToFlatColorIndex_[matPathKey] = materialId;
2122
2124 auto& matData = materialAsset->getData<DiffuseFlatColorMaterialData>();
2125 gpuMat.color = matData.getColor();
2126 gpuMat._padding = 0.0f;
2127 workingFlatColorMaterials_.push_back(gpuMat);
2128 PLOGW << "[DEBUG] Added FLAT_COLOR material: path='" << matPathKey
2129 << "' -> materialId=" << materialId
2130 << " color=(" << gpuMat.color.r << ", " << gpuMat.color.g << ", " << gpuMat.color.b << ")";
2131 break;
2132 }
2133
2134 case DIFFUSE_SHADER: {
2135 if (materialPathToDiffuseIndex_.contains(matPathKey)) {
2136 return;
2137 }
2138 uint32_t materialId = static_cast<uint32_t>(workingDiffuseMaterials_.size());
2139 materialPathToDiffuseIndex_[matPathKey] = materialId;
2140
2141 GpuDiffuseShaderMaterial gpuMat{};
2142 auto& matData = materialAsset->getData<DiffuseShaderMaterialData>();
2143 gpuMat.baseColorTextureIndex = matData.base_color_texture_index();
2144 gpuMat.baseColorFactor = matData.base_color_factor();
2145 workingDiffuseMaterials_.push_back(gpuMat);
2146 break;
2147 }
2148
2150 case L0_SHADER:
2151 case L1_SHADER:
2152 case L2_SHADER:
2153 case DYNAMIC_TEXTURES:
2154 case STATIC_LIGHTMAP: {
2155 // All PBR-like shaders use the same GPU struct and buffer
2156 if (materialPathToPbrIndex_.contains(matPathKey)) {
2157 return;
2158 }
2159 uint32_t materialId = static_cast<uint32_t>(workingPbrMaterials_.size());
2160 materialPathToPbrIndex_[matPathKey] = materialId;
2161
2162 GpuPbrMaterial gpuMat{};
2163 gpuMat.baseColorTextureIndex = 0xFFFFFFFF; // TEXTURE_NOT_PRESENT
2164 gpuMat.normalTextureIndex = 0xFFFFFFFF;
2165 gpuMat.roughnessMetallicTextureIndex = 0xFFFFFFFF;
2166 gpuMat.emissiveTextureIndex = 0xFFFFFFFF;
2167 gpuMat.lightmapTextureIndex = 0xFFFFFFFF;
2168 gpuMat.hasLightmap = 0;
2169 gpuMat.emissiveFactor = glm::vec3(0.0f);
2170 gpuMat.roughnessFactor = 1.0f;
2171 gpuMat.metallicFactor = 0.0f;
2172 gpuMat.normalScale = 1.0f;
2173
2174 // Get TextureHandleRegistry for O(1) descriptor index resolution
2175 TextureHandleRegistry* handleRegistry = assetManager->getTextureHandleRegistry();
2176
2177 // Resolve texture handles to descriptor indices (O(1) array lookup instead of map lookup)
2178 // Handles were set at import time with correct type information
2179
2180 // Base color texture (SRGB format)
2181 AlbedoTextureHandle baseColorHandle = materialAsset->getBaseColorTextureHandle();
2182 const auto& baseColorPath = materialAsset->getBaseColorTexturePath();
2183 if (baseColorHandle.isValid()) {
2184 // Validate handle points to expected path (detect race conditions)
2185 auto registeredPath = handleRegistry->getPathForHandle(baseColorHandle);
2186 if (!baseColorPath.empty() && registeredPath != baseColorPath) {
2187 PLOGE << " [" << matPathKey << "] HANDLE MISMATCH! baseColor handle=" << baseColorHandle.getId()
2188 << " points to '" << registeredPath << "' but material expects '" << baseColorPath << "'";
2189 }
2190
2191 uint32_t texIndex = handleRegistry->resolveDescriptorIndex(baseColorHandle);
2192 if (texIndex != 0xFFFFFFFF) {
2193 gpuMat.baseColorTextureIndex = texIndex;
2194 PLOGI << " [" << matPathKey << "] baseColor: handle=" << baseColorHandle.getId()
2195 << " -> descriptorIdx=" << texIndex << " path=" << baseColorPath;
2196 } else {
2197 PLOGW << " [" << matPathKey << "] baseColor: handle=" << baseColorHandle.getId()
2198 << " valid but no descriptor yet, path=" << baseColorPath;
2199 }
2200 } else {
2201 // Fallback to path-based lookup for backward compatibility
2202 const auto& baseColorPath = materialAsset->getBaseColorTexturePath();
2203 if (!baseColorPath.empty()) {
2204 uint32_t texIndex = assetManager->getTextureDescriptorIndex(baseColorPath);
2205 if (texIndex != 0xFFFFFFFF) {
2206 gpuMat.baseColorTextureIndex = texIndex;
2207 PLOGI << " Resolved base color texture via path: " << baseColorPath << " -> " << texIndex;
2208 }
2209 }
2210 }
2211
2212 // Normal texture (LINEAR format - critical for correct rendering!)
2213 NormalTextureHandle normalHandle = materialAsset->getNormalTextureHandle();
2214 if (normalHandle.isValid()) {
2215 uint32_t texIndex = handleRegistry->resolveDescriptorIndex(normalHandle);
2216 if (texIndex != 0xFFFFFFFF) {
2217 gpuMat.normalTextureIndex = texIndex;
2218 PLOGD << " Resolved normal texture via handle -> " << texIndex;
2219 }
2220 } else {
2221 const auto& normalPath = materialAsset->getNormalTexturePath();
2222 if (!normalPath.empty()) {
2223 uint32_t texIndex = assetManager->getTextureDescriptorIndex(normalPath);
2224 if (texIndex != 0xFFFFFFFF) {
2225 gpuMat.normalTextureIndex = texIndex;
2226 PLOGD << " Resolved normal texture via path: " << normalPath << " -> " << texIndex;
2227 }
2228 }
2229 }
2230
2231 // Metallic-roughness texture (LINEAR format)
2232 MetallicRoughnessTextureHandle metallicRoughnessHandle = materialAsset->getMetallicRoughnessTextureHandle();
2233 if (metallicRoughnessHandle.isValid()) {
2234 uint32_t texIndex = handleRegistry->resolveDescriptorIndex(metallicRoughnessHandle);
2235 if (texIndex != 0xFFFFFFFF) {
2236 gpuMat.roughnessMetallicTextureIndex = texIndex;
2237 PLOGD << " Resolved metallic-roughness texture via handle -> " << texIndex;
2238 }
2239 } else {
2240 const auto& metallicRoughnessPath = materialAsset->getMetallicRoughnessTexturePath();
2241 if (!metallicRoughnessPath.empty()) {
2242 uint32_t texIndex = assetManager->getTextureDescriptorIndex(metallicRoughnessPath);
2243 if (texIndex != 0xFFFFFFFF) {
2244 gpuMat.roughnessMetallicTextureIndex = texIndex;
2245 PLOGD << " Resolved metallic-roughness texture via path: " << metallicRoughnessPath << " -> " << texIndex;
2246 }
2247 }
2248 }
2249
2250 // Emissive texture (SRGB format)
2251 EmissiveTextureHandle emissiveHandle = materialAsset->getEmissiveTextureHandle();
2252 if (emissiveHandle.isValid()) {
2253 uint32_t texIndex = handleRegistry->resolveDescriptorIndex(emissiveHandle);
2254 if (texIndex != 0xFFFFFFFF) {
2255 gpuMat.emissiveTextureIndex = texIndex;
2256 PLOGD << " Resolved emissive texture via handle -> " << texIndex;
2257 }
2258 } else {
2259 const auto& emissivePath = materialAsset->getEmissiveTexturePath();
2260 if (!emissivePath.empty()) {
2261 uint32_t texIndex = assetManager->getTextureDescriptorIndex(emissivePath);
2262 if (texIndex != 0xFFFFFFFF) {
2263 gpuMat.emissiveTextureIndex = texIndex;
2264 PLOGD << " Resolved emissive texture via path: " << emissivePath << " -> " << texIndex;
2265 }
2266 }
2267 }
2268
2269 // Lightmap texture (HDR float format)
2270 LightmapTextureHandle lightmapHandle = materialAsset->getLightmapTextureHandle();
2271 if (lightmapHandle.isValid()) {
2272 uint32_t texIndex = handleRegistry->resolveDescriptorIndex(lightmapHandle);
2273 if (texIndex != 0xFFFFFFFF) {
2274 gpuMat.lightmapTextureIndex = texIndex;
2275 gpuMat.hasLightmap = 1;
2276 PLOGI << " Resolved lightmap texture via handle -> " << texIndex;
2277 } else {
2278 PLOGW << " Lightmap texture not loaded yet (handle valid but no descriptor)";
2279 }
2280 } else {
2281 const auto& lightmapPath = materialAsset->getLightmapTexturePath();
2282 if (!lightmapPath.empty()) {
2283 uint32_t texIndex = assetManager->getTextureDescriptorIndex(lightmapPath);
2284 if (texIndex != 0xFFFFFFFF) {
2285 gpuMat.lightmapTextureIndex = texIndex;
2286 gpuMat.hasLightmap = 1;
2287 PLOGI << " Resolved lightmap texture via path: " << lightmapPath << " -> " << texIndex;
2288 }
2289 }
2290 }
2291
2292 // Extract fields based on specific pipeline type
2293 // NOTE: Texture indices are resolved from paths above - do NOT fall back to
2294 // matData.*_texture_index() as those contain GLTF-local indices, not descriptor indices.
2295 // If a texture path couldn't be resolved, the index remains 0xFFFFFFFF (TEXTURE_NOT_PRESENT).
2296 if (pipelineType == MOVABLE_DIFFUSE_SHADER) {
2297 auto& matData = materialAsset->getData<MovableDiffuseShaderMaterialData>();
2298 gpuMat.baseColorFactor = matData.base_color_factor();
2299 gpuMat.sh_scale = matData.sh_scale1();
2300 gpuMat.ambient_term = matData.ambient_term1();
2301 gpuMat.debug_mode = matData.debug_mode1();
2302 gpuMat.tone_mapping = matData.tone_mapping1();
2303 gpuMat.roughnessFactor = matData.roughness_factor();
2304 gpuMat.metallicFactor = matData.metallic_factor();
2305 gpuMat.normalScale = matData.normal_scale();
2306 gpuMat.emissiveFactor = matData.emissive_factor();
2307 gpuMat.lightmapTextureIndex = matData.lightmap_texture_index();
2308 gpuMat.hasLightmap = (matData.lightmap_texture_index() != 0xFFFFFFFF) ? 1 : 0;
2309 } else if (pipelineType == L0_SHADER) {
2310 auto& matData = materialAsset->getData<L0ShaderMaterialData>();
2311 gpuMat.baseColorFactor = matData.base_color_factor();
2312 gpuMat.sh_scale = matData.sh_scale1();
2313 gpuMat.ambient_term = matData.ambient_term1();
2314 gpuMat.debug_mode = 0;
2315 gpuMat.tone_mapping = 1;
2316 } else if (pipelineType == L1_SHADER) {
2317 auto& matData = materialAsset->getData<L1ShaderMaterialData>();
2318 gpuMat.baseColorFactor = matData.base_color_factor();
2319 gpuMat.sh_scale = matData.sh_scale1();
2320 gpuMat.ambient_term = matData.ambient_term1();
2321 gpuMat.debug_mode = 0;
2322 gpuMat.tone_mapping = 1;
2323 } else if (pipelineType == L2_SHADER) {
2324 auto& matData = materialAsset->getData<L2ShaderMaterialData>();
2325 gpuMat.baseColorFactor = matData.base_color_factor();
2326 gpuMat.sh_scale = matData.sh_scale1();
2327 gpuMat.ambient_term = matData.ambient_term1();
2328 gpuMat.debug_mode = 0;
2329 gpuMat.tone_mapping = 1;
2330 } else if (pipelineType == DYNAMIC_TEXTURES) {
2331 auto& matData = materialAsset->getData<DynamicTexturesMaterialData>();
2332 gpuMat.baseColorFactor = matData.base_color_factor();
2333 gpuMat.sh_scale = matData.sh_scale1();
2334 gpuMat.ambient_term = matData.ambient_term1();
2335 gpuMat.debug_mode = matData.debug_mode1();
2336 gpuMat.tone_mapping = matData.tone_mapping1();
2337 gpuMat.roughnessFactor = matData.roughness_factor();
2338 gpuMat.metallicFactor = matData.metallic_factor();
2339 gpuMat.normalScale = matData.normal_scale();
2340 gpuMat.emissiveFactor = matData.emissive_factor();
2341 // NOTE: lightmapTextureIndex is resolved from path above, not from matData
2342 } else if (pipelineType == STATIC_LIGHTMAP) {
2343 auto& matData = materialAsset->getData<StaticLightmapMaterialData>();
2344
2345 gpuMat.baseColorFactor = matData.base_color_factor();
2346 gpuMat.sh_scale = matData.sh_scale1();
2347 gpuMat.ambient_term = matData.ambient_term1();
2348 gpuMat.debug_mode = matData.debug_mode1();
2349 gpuMat.tone_mapping = matData.tone_mapping1();
2350 gpuMat.roughnessFactor = matData.roughness_factor();
2351 gpuMat.metallicFactor = matData.metallic_factor();
2352 gpuMat.normalScale = matData.normal_scale();
2353 gpuMat.emissiveFactor = matData.emissive_factor();
2354 // NOTE: lightmapTextureIndex is resolved from path above, not from matData
2355
2356 PLOGI << "STATIC_LIGHTMAP material: " << matPathKey
2357 << " bufferIndex=" << materialId
2358 << " baseColorTex=" << gpuMat.baseColorTextureIndex
2359 << " normalTex=" << gpuMat.normalTextureIndex
2360 << " metallicRoughnessTex=" << gpuMat.roughnessMetallicTextureIndex
2361 << " emissiveTex=" << gpuMat.emissiveTextureIndex
2362 << " lightmapTex=" << gpuMat.lightmapTextureIndex;
2363 }
2364 workingPbrMaterials_.push_back(gpuMat);
2365 break;
2366 }
2367
2368 case NORMALS_SHADER:
2369 default: {
2370 if (materialPathToNormalsIndex_.contains(matPathKey)) {
2371 return;
2372 }
2373 uint32_t materialId = static_cast<uint32_t>(workingNormalsMaterials_.size());
2374 materialPathToNormalsIndex_[matPathKey] = materialId;
2375
2376 GpuNormalsMaterial gpuMat{};
2377 auto& matData = materialAsset->getData<NormalMaterialData>();
2378 gpuMat.debug = matData.getDebug() ? 1 : 0;
2379 workingNormalsMaterials_.push_back(gpuMat);
2380 break;
2381 }
2382 }
2383 });
2384
2385 // Also add a default normals material for primitives without material paths
2386 if (!materialPathToNormalsIndex_.contains("__default__")) {
2387 uint32_t defaultId = static_cast<uint32_t>(workingNormalsMaterials_.size());
2388 materialPathToNormalsIndex_["__default__"] = defaultId;
2389 GpuNormalsMaterial defaultMat{};
2390 defaultMat.debug = 0;
2391 workingNormalsMaterials_.push_back(defaultMat);
2392 }
2393
2394 PLOGW << "[DEBUG] uploadMaterialBuffers summary: iterated " << totalMaterials << " materials"
2395 << " (" << loadedMaterials << " loaded)"
2396 << " collected: flatColor=" << workingFlatColorMaterials_.size()
2397 << " diffuse=" << workingDiffuseMaterials_.size()
2398 << " pbr=" << workingPbrMaterials_.size()
2399 << " normals=" << workingNormalsMaterials_.size();
2400
2401 // Upload each working buffer to its corresponding material buffer
2402 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
2403 const VkMemoryPropertyFlags memProps = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
2404
2405 // Helper lambda to resize and upload material buffer
2406 auto uploadMaterialBuffer = [&](PipelineNames pipeline, const void* data, size_t dataSize, const char* name) {
2407 auto it = materialBuffersByPipeline.find(pipeline);
2408 if (it == materialBuffersByPipeline.end() || !it->second.has_value()) return;
2409
2410 VulkanBuffer& buffer = it->second.value();
2411
2412 // Resize buffer if needed (minimum 64 bytes to avoid zero-size)
2413 size_t requiredSize = std::max(dataSize, MIN_BUFFER_SIZE);
2414 if (requiredSize > buffer.getBufferSize()) {
2415 buffer.destroy();
2416 buffer.create(requiredSize, storageUsage, memProps);
2417 buffer.setDebugName(std::string(name));
2418 PLOGI << "Resized material buffer " << name << " to " << requiredSize << " bytes";
2419 }
2420
2421 // Upload data if we have any
2422 if (dataSize > 0) {
2423 void* mappedData = buffer.map();
2424 if (mappedData) {
2425 std::memcpy(mappedData, data, dataSize);
2426 buffer.unmap();
2427 }
2428 }
2429 };
2430
2431 // Upload flat color materials
2432 if (!workingFlatColorMaterials_.empty()) {
2433 uploadMaterialBuffer(DIFFUSE_FLAT_COLOR,
2436 "Material_DiffuseFlatColor");
2437 }
2438
2439 // Upload diffuse shader materials
2440 if (!workingDiffuseMaterials_.empty()) {
2441 uploadMaterialBuffer(DIFFUSE_SHADER,
2444 "Material_DiffuseShader");
2445 }
2446
2447 // Upload PBR materials (shared buffer for all PBR-like pipelines)
2448 // Note: MovableDiffuse, L0, L1, L2, DynamicTextures, StaticLightmap all use GpuPbrMaterial
2449 PLOGI << "[DEBUG] workingPbrMaterials_ count: " << workingPbrMaterials_.size()
2450 << " (sizeof GpuPbrMaterial=" << sizeof(GpuPbrMaterial) << ")";
2451 if (!workingPbrMaterials_.empty()) {
2452 const size_t pbrDataSize = workingPbrMaterials_.size() * sizeof(GpuPbrMaterial);
2453 PLOGI << "[DEBUG] PBR material data size: " << pbrDataSize << " bytes";
2454 uploadMaterialBuffer(MOVABLE_DIFFUSE_SHADER, workingPbrMaterials_.data(), pbrDataSize, "Material_MovableDiffuse");
2455 uploadMaterialBuffer(L0_SHADER, workingPbrMaterials_.data(), pbrDataSize, "Material_L0");
2456 uploadMaterialBuffer(L1_SHADER, workingPbrMaterials_.data(), pbrDataSize, "Material_L1");
2457 uploadMaterialBuffer(L2_SHADER, workingPbrMaterials_.data(), pbrDataSize, "Material_L2");
2458 uploadMaterialBuffer(DYNAMIC_TEXTURES, workingPbrMaterials_.data(), pbrDataSize, "Material_DynamicTextures");
2459 uploadMaterialBuffer(STATIC_LIGHTMAP, workingPbrMaterials_.data(), pbrDataSize, "Material_StaticLightmap");
2460 } else {
2461 PLOGW << "[DEBUG] workingPbrMaterials_ is EMPTY - no PBR materials to upload!";
2462 }
2463
2464 // Upload normals materials
2465 if (!workingNormalsMaterials_.empty()) {
2466 uploadMaterialBuffer(NORMALS_SHADER,
2469 "Material_Normals");
2470 }
2471
2472 materialsDirty_ = false;
2473 PLOGI << "Uploaded material buffers: "
2474 << workingFlatColorMaterials_.size() << " flat color, "
2475 << workingDiffuseMaterials_.size() << " diffuse, "
2476 << workingPbrMaterials_.size() << " PBR, "
2477 << workingNormalsMaterials_.size() << " normals";
2478 }
2479
2481 {
2482 // Process pending deletions for all VulkanStagedBuffers
2483 // This should be called after all frames have updated their descriptor sets
2484 if (primitiveCullingData.has_value()) primitiveCullingData->processPendingDeletions();
2485 if (primitiveMeshletData.has_value()) primitiveMeshletData->processPendingDeletions();
2486 if (perObjectDataBuffer.has_value()) perObjectDataBuffer->processPendingDeletions();
2487 if (primitiveRenderData.has_value()) primitiveRenderData->processPendingDeletions();
2488 if (meshletBuffer.has_value()) meshletBuffer->processPendingDeletions();
2489 if (meshletBoundsData.has_value()) meshletBoundsData->processPendingDeletions();
2490 if (vertexBuffer.has_value()) vertexBuffer->processPendingDeletions();
2491 if (triangleBuffer.has_value()) triangleBuffer->processPendingDeletions();
2492
2493 // Process pending deletions for instancing buffers
2494 if (meshGeometryDataBuffer.has_value()) meshGeometryDataBuffer->processPendingDeletions();
2495 if (instanceDataBuffer.has_value()) instanceDataBuffer->processPendingDeletions();
2496 if (instanceCullingDataBuffer.has_value()) instanceCullingDataBuffer->processPendingDeletions();
2497
2498 // Process pending deletions for vertex shader path buffers
2499 if (vsIndexBuffer_.has_value()) vsIndexBuffer_->processPendingDeletions();
2500 if (singleMeshletGeometryBuffer_.has_value()) singleMeshletGeometryBuffer_->processPendingDeletions();
2501 }
2502
2504 {
2505 const VkBufferUsageFlags storageUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
2506 const VkMemoryPropertyFlags memProps = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
2507
2508 // Create default probe with hardcoded SH coefficients (matching shader defaults)
2509 // These coefficients come from the Python script output in the original shader
2510 SHProbeData defaultProbe{};
2511
2512 // L0^0 (constant ambient)
2513 defaultProbe.coefficients[0] = glm::vec4(7.197292f, 6.285527f, 4.887862f, 0.0f);
2514 // L1^-1
2515 defaultProbe.coefficients[1] = glm::vec4(-1.463182f, -0.987888f, -0.403854f, 0.0f);
2516 // L1^0
2517 defaultProbe.coefficients[2] = glm::vec4(2.734015f, 2.181526f, 1.884693f, 0.0f);
2518 // L1^1
2519 defaultProbe.coefficients[3] = glm::vec4(1.463182f, 0.987888f, 0.403854f, 0.0f);
2520 // L2^-2
2521 defaultProbe.coefficients[4] = glm::vec4(0.051453f, -0.015701f, -0.085487f, 0.0f);
2522 // L2^-1
2523 defaultProbe.coefficients[5] = glm::vec4(-0.278889f, -0.220688f, -0.151132f, 0.0f);
2524 // L2^0
2525 defaultProbe.coefficients[6] = glm::vec4(-1.445439f, -0.174282f, 1.112568f, 0.0f);
2526 // L2^1
2527 defaultProbe.coefficients[7] = glm::vec4(0.278889f, 0.220688f, 0.151132f, 0.0f);
2528 // L2^2
2529 defaultProbe.coefficients[8] = glm::vec4(0.051453f, -0.015701f, -0.085487f, 0.0f);
2530
2531 // Default scale and ambient
2532 defaultProbe.scale = 0.15f;
2533 defaultProbe.ambientTerm = 0.1f;
2534
2535 shProbeData_.clear();
2536 shProbeData_.push_back(defaultProbe);
2537
2538 // Create and upload buffer
2539 const size_t bufferSize = shProbeData_.size() * sizeof(SHProbeData);
2540
2541 shProbeBuffer_.emplace(context);
2542 shProbeBuffer_->create(bufferSize, storageUsage, memProps);
2543 shProbeBuffer_->setDebugName("SHProbeBuffer");
2544
2545 // Upload data
2546 void* mappedData = shProbeBuffer_->map();
2547 if (mappedData) {
2548 std::memcpy(mappedData, shProbeData_.data(), bufferSize);
2549 shProbeBuffer_->unmap();
2550 }
2551
2552 PLOGI << "Initialized SH probe buffer with " << shProbeData_.size() << " probe(s)";
2553 }
2554
2556 {
2557 if (!shProbeBuffer_.has_value()) {
2558 throw std::runtime_error("SH probe buffer not initialized");
2559 }
2560 return shProbeBuffer_.value();
2561 }
2562
2564 {
2566 PLOGD << "Default textures already queued for loading";
2567 return;
2568 }
2569
2570 AssetManager* assetManager = injectedAssetManager;
2571 if (!assetManager && engine) {
2572 assetManager = engine->getAssetManager().get();
2573 }
2574
2575 if (!assetManager) {
2576 PLOGE << "Cannot initialize default textures: AssetManager not available";
2577 return;
2578 }
2579
2580 PLOGI << "Queueing default PBR textures for loading via texture pipeline...";
2581
2582 // Pre-register handles with correct texture types BEFORE loading
2583 // This ensures onTextureLoaded can find them and assign correct VkFormat
2584 TextureHandleRegistry* handleRegistry = assetManager->getTextureHandleRegistry();
2585 if (handleRegistry) {
2586 // Normal map needs LINEAR format
2588 // White/Black are typically used for roughness/metallic (LINEAR) or as generic fallbacks
2591 }
2592
2593 // Queue default textures through the normal texture loading pipeline
2594 // These will be loaded asynchronously and available via getTextureDescriptorIndex()
2598
2600
2601 PLOGI << "Default textures queued: "
2605 }
2606
2607 uint32_t RenderingDataManager::getValidTextureIndex(uint32_t textureIndex, DefaultTextureType defaultType)
2608 {
2609 // If texture is valid, return it
2610 if (textureIndex != 0xFFFFFFFF) {
2611 return textureIndex;
2612 }
2613
2614 // Get AssetManager to query default texture indices
2615 AssetManager* assetManager = injectedAssetManager;
2616 if (!assetManager && engine) {
2617 assetManager = engine->getAssetManager().get();
2618 }
2619
2620 if (!assetManager) {
2621 return 0xFFFFFFFF;
2622 }
2623
2624 // Return appropriate default based on type (queried from AssetManager)
2625 switch (defaultType) {
2632 default:
2633 return 0xFFFFFFFF;
2634 }
2635 }
2636} // namespace EngineCore
::EngineCore::LogCategory LogLOD("LogLOD", ::EngineCore::LogVerbosity::Info, ::EngineCore::LogVerbosity::Verbose)
#define VS_LOG(Category, Verbosity, Format,...)
Log a message with category and verbosity.
Definition LogMacros.h:122
#define primitive_rendering_id
#define meshlet_rendering_id
constexpr EngineCore::PipelineNames DEFAULT_MATERIAL_PIPELINE
Definition Settings.h:59
#define TRACY_ZONE_SCOPED_NAMED(name)
entt::entity getEntity() const
Getter for data.
Definition Asset.h:125
CpuLoadingState getLoadingState()
Gets the loading state of this asset.
Definition Asset.h:106
std::optional< AssetClass * > getAsset(const Key &path)
Try's to get an asset. If the asset does not exist (can be checked with exists) it will return a std:...
Definition Asset.h:266
void forEachAsset(Func &&func) const
Iterates over all assets and calls the provided callback for each.
Definition Asset.h:222
static entt::registry & get()
Gets the registry for all components.
An Actor is similar to an EngineCore::Entity. An actor is an Entity with a transform.
Definition Actor.h:24
The application context is the core class which stores the basic openxr and vulkan objects.
MeshAssetManager * getMeshAssetManager() const
Gets the mesh asset manager for mesh asset lookups.
uint32_t getTextureDescriptorIndex(const std::filesystem::path &path)
Gets the descriptor index of a loaded texture by path.
MaterialAssetManager * getMaterialAssetManager() const
Gets the material asset manager for material lookups.
void loadEcsTexture(const std::filesystem::path &path)
Loads a texture (.png / .jpg / .exr)
TextureHandleRegistry * getTextureHandleRegistry()
Gets the texture handle registry for O(1) descriptor index lookups.
Texture * registerTexture(const std::filesystem::path &path, Texture texture)
Registers a texture with the texture manager which prevents the same texture from being loaded twice.
Material data for an Object which displays a flat color.
Dynamic textures material with PBR and lightmap support.
L1 Spherical Harmoics shader.
the material asset is another wrapper for asset data which is stored in entt. It has the EngineCore::...
const std::filesystem::path & getBaseColorTexturePath() const
Gets the base color texture path for this material.
MetallicRoughnessTextureHandle getMetallicRoughnessTextureHandle() const
Get the type-safe metallic-roughness texture handle.
EmissiveTextureHandle getEmissiveTextureHandle() const
Get the type-safe emissive texture handle.
const std::filesystem::path & getMetallicRoughnessTexturePath() const
LightmapTextureHandle getLightmapTextureHandle() const
Get the type-safe lightmap texture handle.
NormalTextureHandle getNormalTextureHandle() const
Get the type-safe normal texture handle.
const std::filesystem::path & getLightmapTexturePath() const
const std::filesystem::path & getEmissiveTexturePath() const
T & getData()
Method to get the material data from the ecs with the corresponding type.
const std::filesystem::path & getNormalTexturePath() const
AlbedoTextureHandle getBaseColorTextureHandle() const
Get the type-safe base color texture handle.
PipelineNames getType() const
Gets the material type for pipeline lookup.
The mesh asset stores geometry data and.
Definition MeshAsset.h:15
Ecs::MeshPrimitiveData * getMeshPrimitiveData()
Gets the mesh data for this asset.
Definition MeshAsset.cpp:35
A component which can be attached as many times to an actor as one wants. It makes it possible to ren...
MeshAsset * getMeshAsset() const
Gets the asset used by this mesh rendering component.
bool isVisible() const
If this mesh component should be considered for rendering.
glm::mat4 getWorldTransform() const
Getter for the world transform of the mesh component.
Moveable diffuse shader with PBR and lightmap support.
Material data for an object which displays its normals.
bool ensureOutputBufferSizes(uint32_t primitiveCount)
Ensures output buffers are sized to handle the given primitive count. Called by RenderingDataManager ...
bool getPipelineIndex(GraphicsPipeline *pipeline, uint32_t &pipelineIndex) const
Gets the index of a pipeline.
bool defaultTexturesLoading_
True if loadEcsTexture has been called.
std::unordered_map< uint32_t, PipelineNames > primitiveToPipeline
Maps primitive IDs to their pipeline.
std::vector< VkDescriptorImageInfo > generateTextureDescriptorInfos() const
Generate descriptor infos for all GPU-uploaded textures.
size_t committedSingleMeshletGeoCount_
Single-meshlet geometries already on GPU.
std::vector< Texture * > getTexturesToUpload() const
Get the list of textures waiting for GPU upload.
std::optional< VulkanStagedBuffer > vertexBuffer
Optimized vertex data (Vertex structs)
uint32_t getValidTextureIndex(uint32_t textureIndex, DefaultTextureType defaultType)
void processCompletedMeshletGenerations(std::vector< Ecs::CompletedMeshletGeneration > &&completions)
Process completed meshlet generations and write them to the registry. This should be called at a safe...
void processPendingMeshLoads()
Process pending mesh loads at a safe point in the frame. This should be called BEFORE updateIfDirty()...
std::vector< MeshAsset * > pendingMeshLoads_
uint32_t instanceCount_
Number of instances in buffers.
std::unordered_map< PipelineNames, std::optional< VulkanBuffer > > materialBuffersByPipeline
std::optional< VulkanStagedBuffer > localBoundsBuffer
LocalBoundsData per primitive (static, uploaded once)
bool updatePrimitiveDataInstanced()
Instanced version of updatePrimitiveData using geometry deduplication. Separates geometry upload (onc...
size_t committedVertexCount_
Vertices already on GPU.
const VulkanBuffer & getSHProbeBuffer() const
Get the SH probe buffer for spherical harmonic lighting.
bool hasTexturesToUpload() const
Check if there are textures waiting to be uploaded to GPU.
uint32_t nextMeshletId_
Next meshlet ID to assign.
bool updatePrimitiveData()
Regenerates all buffers and the rendering ids of all objects, primitives and meshlets.
std::optional< VulkanStagedBuffer > instanceDataBuffer
InstanceData per visible instance.
uint32_t getTextureCount() const
Get the count of textures currently on the GPU.
std::vector< InstanceData > workingInstanceData_
void invalidateGeometryCache()
Clears the geometry cache and marks for full rebuild. Called when meshes are unloaded.
std::unordered_map< PipelineNames, uint32_t > pipelinePrimitiveCounts
Count of primitives per pipeline.
bool instancingEnabled_
Feature flag for instanced rendering.
void clearTexturesToUpload()
Clear the texture upload queue after upload is complete.
std::unordered_map< std::string, uint32_t > materialPathToDiffuseIndex_
std::vector< VulkanStagedBufferSyncObjects > pendingTransformSyncObjects_
std::unordered_map< GeometryCacheKey, MeshGeometryMapping, GeometryCacheKeyHash > geometryCache_
std::unordered_set< MeshAsset * > renderCycleMeshSnapshot_
bool isDirty
a flag which defers the update of the buffer updates to the end of the frame.
void processPendingDeletions()
Process deferred buffer deletions Call this after all frames have completed and descriptor sets are u...
std::vector< Texture * > gpuTextures
Textures currently on GPU.
static constexpr const char * DEFAULT_NORMAL_TEXTURE_PATH
void initializeDefaultTextures()
Queue default textures for loading via the texture pipeline.
void snapshotRenderableMeshes()
Take a snapshot of currently render-ready meshes. Called at the start of update cycle to ensure consi...
void setRenderer(Renderer *renderer)
Set the renderer for pipeline index lookups. Required when Engine is null.
void markDirty()
Flags the Rendering Data Manager as dirty so that all buffers managed by this manager are in need of ...
bool updateTransforms()
Updates only transform-related buffers (world matrices, bounding spheres)
std::optional< VulkanStagedBuffer > clusterGroupDataBuffer_
ClusterGroupData per LOD group.
std::optional< VulkanStagedBuffer > triangleBuffer
Triangle indices for meshlets.
void setSceneManager(SceneManager *sceneManager)
Set the scene manager for retrieving actors. Required when Engine is null.
std::vector< SingleMeshletGeometryData > workingSingleMeshletGeoData_
std::unordered_map< uint32_t, MeshComponent * > primitiveIdToComponent_
uint32_t clusterGroupCount_
Total cluster groups.
std::vector< Ecs::PackedTriangle > workingTriangleBuffer_
std::vector< Texture * > texturesToUpload
Textures waiting for GPU upload.
uint32_t clusterCount_
Total clusters across all LOD levels.
std::vector< ClusterLodData > workingClusterLodData_
uint32_t multiMeshletGeometryCount_
Geometries with meshletCount > 1 (mesh shader path)
void onMeshLoaded(MeshAsset *asset)
Event hook which executes when an Mesh is loaded.
std::optional< VulkanStagedBuffer > primitiveRenderData
MeshPrimitiveRenderData per primitive (texture/material IDs)
size_t committedGeometryCount_
Geometries already on GPU.
std::vector< PrimitiveMeshletData > workingPrimitiveMeshletBuffer_
bool updateIfDirty()
Triggers an update of all GPU buffers if dirty.
std::vector< GpuDiffuseFlatColorMaterial > workingFlatColorMaterials_
DefaultTextureType
Get a valid texture index, substituting defaults for missing textures.
PipelineNames getPipelineForPrimitive(uint32_t primitiveId) const
Gets the pipeline ID (PipelineNames enum) for a given primitive.
void uploadMaterialBuffers()
Upload material data from MaterialAssets to GPU buffers.
std::optional< VulkanStagedBuffer > perObjectDataBuffer
mat4 world transform per primitive (matches shader PerObjectData)
std::vector< InstanceCullingData > workingInstanceCullingData_
void onMaterialUnloaded(MaterialAsset *materialAsset)
Event hook for when a material has been unloaded.
uint32_t uniqueGeometryCount_
Number of unique geometries in buffers.
std::unordered_map< std::string, uint32_t > materialPathToFlatColorIndex_
size_t committedVsIndexCount_
VS indices already on GPU.
std::vector< uint32_t > workingVsIndexBuffer_
RenderingDataManager(const Engine *engine, ApplicationContext *context)
std::vector< MeshGeometryData > workingGeometryDataBuffer_
std::vector< glm::mat4 > cachedTransforms_
bool isMeshInSnapshot(MeshAsset *asset) const
Check if a mesh asset is in the current render cycle snapshot.
std::vector< VulkanStagedBufferSyncObjects > pendingSyncObjects_
std::unordered_map< std::string, uint32_t > materialPathToNormalsIndex_
std::vector< glm::mat4 > workingPerObjectData_
std::optional< VulkanStagedBuffer > primitiveCullingData
ObjectCullingData per primitive (unused after GPU transform optimization)
~RenderingDataManager()
Destructor - cleans up all GPU buffers.
void collectMaterialsFromScene()
Collects all materials from loaded meshes and creates per-pipeline data This builds material-to-pipel...
void queueTextureForUpload(Texture *texture)
Queue a texture for GPU upload Called by AssetManager when a new texture is created and needs uploadi...
std::optional< VulkanStagedBuffer > meshletBuffer
UnifiedMeshlet data.
void onRenderableSpawned(MeshComponent *component)
Event hook which is executed when a renderable component gets added to an actor and can thus be rende...
std::vector< ObjectCullingData > workingPrimitiveCullingBuffer_
std::vector< MeshletBounds > workingMeshletBoundsBuffer_
void onRenderableDestroyed(MeshComponent *component)
Event hook which is executed when a renderable component gets removed from an actor and thus has to b...
void initializeSHProbeBuffer()
Initialize default SH probe data Sets up a single default probe with hardcoded coefficients.
std::optional< VulkanStagedBuffer > primitiveMeshletData
PrimitiveMeshletData per primitive.
std::optional< VulkanStagedBuffer > meshGeometryDataBuffer
MeshGeometryData per unique geometry.
std::optional< VulkanStagedBuffer > instanceCullingDataBuffer
InstanceCullingData per instance.
void onMaterialLoaded(MaterialAsset *materialAsset)
Event hook for when a material has been loaded.
std::unordered_map< entt::entity, std::vector< uint32_t > > entityToPrimitiveIds_
std::vector< GpuDiffuseShaderMaterial > workingDiffuseMaterials_
std::unordered_map< std::string, uint32_t > materialPathToPbrIndex_
bool geometryDirty_
Unique meshes changed (requires geometry buffer update)
void onTextureUnloaded(TextureAsset *textureAsset)
Event hook for when a texture has been unloaded.
std::vector< LocalBoundsData > workingLocalBoundsDataBuffer_
std::vector< GpuPbrMaterial > workingPbrMaterials_
std::optional< VulkanStagedBuffer > singleMeshletGeometryBuffer_
SingleMeshletGeometryData per single-meshlet geometry.
std::optional< VulkanStagedBuffer > clusterLodDataBuffer_
ClusterLodData per cluster.
const VulkanBuffer & getMaterialBufferForPipeline(PipelineNames pipelineName) const
Get the material buffer for a specific pipeline type.
static constexpr const char * DEFAULT_BLACK_TEXTURE_PATH
void onMeshUnloaded(MeshAsset *asset)
Event hook which executes when a Mesh is unloaded.
std::optional< VulkanStagedBuffer > vsIndexBuffer_
Contiguous uint32_t indices for vertex shader.
std::vector< ClusterGroupData > workingClusterGroupData_
std::vector< MeshPrimitiveRenderData > workingPrimitiveRenderData_
size_t committedTriangleCount_
Triangles already on GPU.
bool geometryNeedsFullRebuild_
True on startup or after mesh unload.
std::optional< VulkanStagedBuffer > meshletBoundsData
MeshletBounds for meshlet culling.
std::vector< GpuNormalsMaterial > workingNormalsMaterials_
void initializeMaterialBuffers()
Initialize material buffers for all pipeline types.
void setAssetManager(AssetManager *assetManager)
Set the asset manager for material lookups. Required when Engine is null.
std::vector< UnifiedMeshlet > workingMeshletDataBuffer_
std::vector< SHProbeData > shProbeData_
CPU-side SH probe data.
void onTextureLoaded(TextureAsset *textureAsset, const std::filesystem::path &texturePath)
Event hook for when a texture has been loaded by the asset pipeline. Creates a Vulkan Texture from th...
uint32_t nextGeometryId_
Next geometry ID to assign.
static constexpr const char * DEFAULT_WHITE_TEXTURE_PATH
std::optional< VulkanBuffer > shProbeBuffer_
SHProbeData buffer at binding 18.
uint32_t singleMeshletGeometryCount_
Geometries with meshletCount == 1 (vertex shader path)
size_t committedMeshletCount_
Meshlets already on GPU.
Manages game objects within a scene, handling registration, ID allocation, and GPU buffer synchroniza...
std::vector< Actor * > getAllActors()
Gets a list of pointers to all actors in the scene.
Static lightmap material with PBR support.
Wrapper for texture data.
Central registry for texture handles, providing O(1) descriptor index lookup.
TypedTextureHandle< Type > getOrCreateHandle(const std::filesystem::path &path)
Get or create a typed handle for a texture path.
std::filesystem::path getPathForHandle(TextureHandle handle) const
Get the path associated with a handle (for debugging/validation)
uint32_t resolveDescriptorIndex(TextureHandle handle) const
Resolve a handle to its descriptor index (O(1) operation)
void onTextureLoaded(const std::filesystem::path &path, Texture *texture, uint32_t descriptorIndex)
Called when a texture finishes loading.
TextureType getTextureTypeForPath(const std::filesystem::path &path) const
Get the texture type for a path (if registered)
uint32_t getDescriptorIndex() const
Definition Texture.cpp:278
uint32_t getId() const
Get the registry ID for this handle.
bool isValid() const
Check if this handle points to a valid registry entry.
RAII wrapper for Vulkan buffer and device memory.
void setDebugName(const std::string &name)
Sets the name shown in vulkan validation layer.
VkDeviceSize getBufferSize() const
void * map()
Maps the buffer to be writable by the cpu. The mapped memory range is also stored in the object.
void unmap()
Unmaps the memory if it is mapped.
void destroy()
Destroys all vulkan resources of the buffer.
void create(size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties)
Creates a buffer with a certain size, usage flags and memory properties.
@ LOADED
All components of the asset have been loaded, and it is ready to be used.
Definition Asset.h:39
Log category system implementation.
TypedTextureHandle< TextureType::MetallicRoughness > MetallicRoughnessTextureHandle
TypedTextureHandle< TextureType::Lightmap > LightmapTextureHandle
constexpr size_t MIN_BUFFER_SIZE
TypedTextureHandle< TextureType::Emissive > EmissiveTextureHandle
TypedTextureHandle< TextureType::Normal > NormalTextureHandle
@ Warning
Potential issue.
Definition LogCategory.h:31
@ Info
Important state changes.
Definition LogCategory.h:32
@ Debug
Development debugging.
Definition LogCategory.h:33
TypedTextureHandle< TextureType::BaseColor > AlbedoTextureHandle
TextureType
Represents the semantic type of a texture, determining its Vulkan format.
Definition TextureType.h:18
@ BaseColor
Color data with gamma (SRGB format for 8-bit)
Definition TextureType.h:19
@ Unknown
Fallback type, uses SRGB as default.
Definition TextureType.h:24
@ Normal
Linear data for normal maps (UNORM format for 8-bit)
Definition TextureType.h:20
@ MOVABLE_DIFFUSE_SHADER
bool empty() const
Checks whether this path doesn't point to anything. So it is empty.
Definition AssetPath.h:25
std::string getAssetHandle() const
Definition AssetPath.h:19
glm::vec3 center
Definition EcsData.h:194
tinygltf::Image image
Definition EcsData.h:90
Reference to a MeshComponent for ECS-based iteration. Allows efficient querying of all mesh component...
Definition EcsData.h:138
std::vector< Ecs::MeshPrimitive > primitives
Definition EcsData.h:284
Ecs::BoundingSphere boundingSphere
Definition EcsData.h:285
std::optional< UnpackedMeshletData > unpackedData
Pre-unpacked GPU-ready data.
Definition EcsData.h:276
std::optional< MeshletData > meshletData
Definition EcsData.h:275
Asset::Path materialPath
Reference to MaterialAsset for pipeline lookup.
Definition EcsData.h:279
std::optional< LodHierarchyResult > lodHierarchy
LOD hierarchy (if generated)
Definition EcsData.h:277
Ecs::BoundingSphere boundingSphere
Definition EcsData.h:278
GPU-ready packed triangle data (matches shader layout). Stores 3 x 8-bit local indices packed into a ...
Definition EcsData.h:204
Tag for transforms that need GPU upload. Separate from TransformDirty because the renderer clears thi...
Definition EcsData.h:123
Per-group LOD data representing a simplified version of child clusters. Groups are created by merging...
Definition RenderData.h:480
Per-cluster LOD data for GPU-driven LOD selection. Each cluster maps to exactly one meshlet and conta...
Definition RenderData.h:461
uint32_t meshletIndex
Index into UnifiedMeshlet buffer.
Definition RenderData.h:465
GPU-side layout for flat color material Matches GLSL struct in triangle_flat.frag.
Definition RenderData.h:356
GPU-side layout for diffuse shader material Used for basic textured materials.
Definition RenderData.h:369
GPU-side layout for normals debug material.
Definition RenderData.h:426
GPU-side layout for PBR material with spherical harmonics Used by MovableDiffuseShader,...
Definition RenderData.h:392
uint32_t roughnessMetallicTextureIndex
Definition RenderData.h:396
Instance culling data - replaces ObjectCullingData for instancing. Contains world-space bounding sphe...
Definition RenderData.h:298
uint32_t meshGeometryId
Index into MeshGeometryData buffer.
Definition RenderData.h:301
uint32_t instanceId
Index into InstanceData buffer.
Definition RenderData.h:300
glm::vec4 worldPositionAndRadius
xyz = world position, w = radius
Definition RenderData.h:299
Per-instance data - one per visible MeshComponent. References shared geometry via meshGeometryId.
Definition RenderData.h:282
uint32_t materialId
Material instance ID (for material variations)
Definition RenderData.h:285
uint32_t colorTextureId
Texture array index.
Definition RenderData.h:286
glm::mat4 worldMatrix
64 bytes - world transform
Definition RenderData.h:283
uint32_t meshGeometryId
Index into MeshGeometryData buffer.
Definition RenderData.h:284
Local-space bounding sphere data - uploaded once, never changes.
Definition RenderData.h:164
glm::vec4 localCenterAndRadius
xyz = local center, w = local radius
Definition RenderData.h:165
Shared geometry metadata - one per unique MeshAsset primitive. Allows multiple instances to reference...
Definition RenderData.h:261
uint32_t clusterCount
Total clusters (all LOD levels)
Definition RenderData.h:268
uint32_t meshletStartIndex
First meshlet in shared meshlet buffer.
Definition RenderData.h:262
uint32_t meshletCount
Number of meshlets for this geometry.
Definition RenderData.h:263
uint32_t pipelineIndex
Material pipeline index.
Definition RenderData.h:264
uint32_t groupCount
Total groups in LOD hierarchy.
Definition RenderData.h:270
uint32_t clusterStartIndex
First ClusterLodData (0xFFFFFFFF if no LOD)
Definition RenderData.h:267
uint32_t groupStartIndex
First ClusterGroupData.
Definition RenderData.h:269
uint32_t singleMeshletGeoIndex
Index into SingleMeshletGeometryData (0xFFFFFFFF if multi-meshlet)
Definition RenderData.h:265
Mapping from MeshAsset primitive to shared geometry buffer location. Used by RenderingDataManager to ...
Definition RenderData.h:313
bool isSingleMeshlet
True if meshletCount == 1 (use vertex shader path)
Definition RenderData.h:322
uint32_t vsIndexOffset
Offset into VS index buffer (contiguous uint32_t)
Definition RenderData.h:320
float localBoundsRadius
Cached local bounding sphere radius.
Definition RenderData.h:326
uint32_t meshGeometryId
Index into MeshGeometryData buffer.
Definition RenderData.h:314
glm::vec3 localBoundsCenter
Cached local bounding sphere center.
Definition RenderData.h:325
uint32_t pipelineIndex
Cached pipeline index from material.
Definition RenderData.h:324
uint32_t meshletStartIndex
First meshlet in shared buffer.
Definition RenderData.h:315
uint32_t triangleBaseOffset
Offset into triangle buffer.
Definition RenderData.h:318
uint32_t meshletCount
Number of meshlets.
Definition RenderData.h:316
uint32_t vertexBaseOffset
Offset into vertex buffer.
Definition RenderData.h:317
uint32_t vsIndexCount
Number of indices (triangleCount * 3)
Definition RenderData.h:321
Used in meshlet culling.
Definition RenderData.h:92
glm::vec4 worldPositionAndRadius
Definition RenderData.h:93
Data for object culling.
Definition RenderData.h:144
glm::vec4 worldPositionAndRadius
The center point of the bounding sphere (w component stores radius)
Definition RenderData.h:146
uint32_t objectIndex
Object index linking back to the Object Data.
Definition RenderData.h:148
Data for the primitive culling shader to assemble and pass on to the unpacking shader.
Definition RenderData.h:172
Key for geometry cache - uniquely identifies a primitive within a mesh. Uses MeshAsset pointer and pr...
SH Probe data structure for dynamic lighting Contains L0-L2 RGB spherical harmonic coefficients.
Definition RenderData.h:439
glm::vec4 coefficients[9]
Definition RenderData.h:440
GPU data for single-meshlet geometry using vertex shader path. One entry per unique single-meshlet ge...
Definition RenderData.h:338
uint32_t firstIndex
Offset into index buffer.
Definition RenderData.h:340
uint32_t pipelineIndex
Material pipeline index.
Definition RenderData.h:342
int32_t vertexOffset
Added to each index value.
Definition RenderData.h:341
uint32_t indexCount
Number of indices to draw.
Definition RenderData.h:339
Information about the wereabouts of a meshlet in memory on the gpu.
Definition RenderData.h:76
The fundamental building block of all meshes in this engine.
Definition Vertex.h:15