VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
batchManager.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 13.10.2025.
5//
6//
7//
8#include "batchManager.h"
9#include "skinBatchInstance.h"
10
11#include <algorithm>
12#include <unordered_map>
13
17#include "scene/scene.h"
18#include "spdlog/spdlog.h"
19
20namespace visutwin::canvas
21{
22
28 {
29 float px, py, pz; // position
30 float nx, ny, nz; // normal
31 float u, v; // uv0
32 float tx, ty, tz, tw; // tangent + handedness
33 float u1, v1; // uv1
34 };
35
36 static_assert(sizeof(PackedVertex) == 56, "PackedVertex must be 56 bytes (14 floats)");
37
46 {
47 float px, py, pz; // position
48 float nx, ny, nz; // normal
49 float u, v; // uv0
50 float tx, ty, tz, tw; // tangent + handedness
51 float u1, v1; // uv1
52 float boneIndex; // mesh instance index into palette
53 };
54
55 static_assert(sizeof(DynamicBatchVertex) == 60, "DynamicBatchVertex must be 60 bytes (15 floats)");
56
57 // -------------------------------------------------------------------------
58
59 BatchManager::BatchManager(GraphicsDevice* device) : _device(device) {}
60
62 {
63 _groups[group.id] = group;
64 }
65
66 void BatchManager::removeGroup(int groupId)
67 {
68 // Destroy any batches belonging to this group first.
69 for (auto it = _batches.begin(); it != _batches.end(); ) {
70 if ((*it)->batchGroupId == groupId) {
71 // Restore originals.
72 for (auto* mi : (*it)->origMeshInstances) {
73 mi->setVisible(true);
74 }
75 it = _batches.erase(it);
76 } else {
77 ++it;
78 }
79 }
80 _groups.erase(groupId);
81 }
82
83 const BatchGroup* BatchManager::getGroupById(int groupId) const
84 {
85 auto it = _groups.find(groupId);
86 return it != _groups.end() ? &it->second : nullptr;
87 }
88
89 // -------------------------------------------------------------------------
90 // prepare() —
91 // -------------------------------------------------------------------------
93 {
94 // 1. Destroy existing batches.
95 destroy(scene);
96
97 // 2. Collect mesh instances by (groupId, material pointer).
98 // Key: (batchGroupId << 32) | material pointer hash — but simpler to use a nested map.
99 struct MaterialKey {
100 int groupId;
101 Material* material;
102 bool operator==(const MaterialKey& o) const { return groupId == o.groupId && material == o.material; }
103 };
104 struct MaterialKeyHash {
105 size_t operator()(const MaterialKey& k) const {
106 auto h1 = std::hash<int>{}(k.groupId);
107 auto h2 = std::hash<void*>{}(static_cast<void*>(k.material));
108 return h1 ^ (h2 << 1);
109 }
110 };
111
112 std::unordered_map<MaterialKey, std::vector<MeshInstance*>, MaterialKeyHash> groups;
113
114 for (auto* rc : RenderComponent::instances()) {
115 if (!rc || !rc->enabled()) continue;
116
117 const int groupId = rc->batchGroupId();
118 if (groupId < 0) continue; // Not tagged for batching.
119 if (_groups.find(groupId) == _groups.end()) continue; // Unknown group.
120
121 for (auto* mi : rc->meshInstances()) {
122 if (!mi || !mi->mesh() || !mi->mesh()->getVertexBuffer()) continue;
123
124 MaterialKey key{groupId, mi->material()};
125 groups[key].push_back(mi);
126 }
127 }
128
129 // 3. Build a batch for each (group, material) bucket with >=2 mesh instances.
130 int batchCount = 0;
131 int totalOrigMeshInstances = 0;
132 for (auto& [key, meshInstances] : groups) {
133 if (meshInstances.size() < 2) continue; // No point batching a single mesh.
134
135 // Dispatch to dynamic or static batch creation based on group config.
136 const auto* group = getGroupById(key.groupId);
137 std::unique_ptr<Batch> batch;
138 if (group && group->dynamic) {
139 batch = createDynamicBatch(meshInstances, key.groupId);
140 } else {
141 batch = createBatch(meshInstances, key.groupId);
142 }
143 if (batch) {
144 totalOrigMeshInstances += static_cast<int>(meshInstances.size());
145 batchCount++;
146
147 // Register batch MeshInstance with scene layers.
148 if (scene && scene->layers()) {
149 const auto* group = getGroupById(key.groupId);
150 if (group && !group->layers.empty()) {
151 for (int layerId : group->layers) {
152 auto layer = scene->layers()->getLayerById(layerId);
153 if (layer) {
154 layer->addMeshInstances({batch->meshInstance.get()});
155 }
156 }
157 } else {
158 // Default: add to WORLD layer (id=1).
159 auto worldLayer = scene->layers()->getLayerById(1);
160 if (worldLayer) {
161 worldLayer->addMeshInstances({batch->meshInstance.get()});
162 }
163 }
164 }
165
166 _batches.push_back(std::move(batch));
167 }
168 }
169
170 if (batchCount > 0) {
171 spdlog::info("[BatchManager] Created {} batches from {} mesh instances",
172 batchCount, totalOrigMeshInstances);
173 }
174 }
175
176 // -------------------------------------------------------------------------
177 // destroy() —
178 // -------------------------------------------------------------------------
180 {
181 for (auto& batch : _batches) {
182 // Remove batch MeshInstance from layers.
183 if (scene && scene->layers() && batch->meshInstance) {
184 const auto* group = getGroupById(batch->batchGroupId);
185 if (group && !group->layers.empty()) {
186 for (int layerId : group->layers) {
187 auto layer = scene->layers()->getLayerById(layerId);
188 if (layer) {
189 layer->removeMeshInstances({batch->meshInstance.get()});
190 }
191 }
192 } else {
193 auto worldLayer = scene->layers()->getLayerById(1);
194 if (worldLayer) {
195 worldLayer->removeMeshInstances({batch->meshInstance.get()});
196 }
197 }
198 }
199
200 // Restore visibility of original mesh instances.
201 for (auto* mi : batch->origMeshInstances) {
202 mi->setVisible(true);
203 }
204 }
205 _batches.clear();
206 }
207
208 // -------------------------------------------------------------------------
209 // createBatch() —
210 // -------------------------------------------------------------------------
211 std::unique_ptr<Batch> BatchManager::createBatch(
212 const std::vector<MeshInstance*>& meshInstances, int batchGroupId)
213 {
214 if (meshInstances.empty() || !_device) return nullptr;
215
216 // --- 1. Count total vertices and indices. ---
217 int totalVertices = 0;
218 int totalIndices = 0;
219 for (auto* mi : meshInstances) {
220 auto vb = mi->mesh()->getVertexBuffer();
221 auto ib = mi->mesh()->getIndexBuffer();
222 if (!vb) continue;
223
224 totalVertices += vb->numVertices();
225 if (ib) {
226 totalIndices += ib->numIndices();
227 } else {
228 // Non-indexed: treat vertex count as index count (identity indices).
229 totalIndices += vb->numVertices();
230 }
231 }
232
233 if (totalVertices == 0) return nullptr;
234
235 // --- 2. Allocate merged buffers. ---
236 std::vector<PackedVertex> mergedVertices;
237 mergedVertices.reserve(totalVertices);
238
239 std::vector<uint32_t> mergedIndices;
240 mergedIndices.reserve(totalIndices);
241
242 BoundingBox mergedAabb;
243 bool aabbInitialized = false;
244
245 uint32_t vertexOffset = 0;
246
247 // --- 3. Merge geometry. ---
248 for (auto* mi : meshInstances) {
249 auto vb = mi->mesh()->getVertexBuffer();
250 auto ib = mi->mesh()->getIndexBuffer();
251 if (!vb || vb->storage().empty()) continue;
252
253 const int vertCount = vb->numVertices();
254 const auto* srcVerts = reinterpret_cast<const PackedVertex*>(vb->storage().data());
255
256 // Get world transform.
257 Matrix4 worldMatrix = Matrix4::identity();
258 if (mi->node()) {
259 worldMatrix = mi->node()->worldTransform();
260 }
261
262 // Compute normal matrix (inverse-transpose of upper-left 3x3).
263 // For uniform-scale transforms, the normal matrix == the rotation part.
264 // For non-uniform scale, we need the full inverse-transpose.
265 Matrix4 normalMatrix = worldMatrix.inverse().transpose();
266
267 // Transform vertices.
268 for (int i = 0; i < vertCount; i++) {
269 PackedVertex v = srcVerts[i];
270
271 // Transform position by world matrix.
272 Vector3 pos = worldMatrix.transformPoint(Vector3(v.px, v.py, v.pz));
273 v.px = pos.getX(); v.py = pos.getY(); v.pz = pos.getZ();
274
275 // Transform normal by normal matrix (3x3 part only, no translation).
276 float nnx = normalMatrix.getElement(0, 0) * v.nx +
277 normalMatrix.getElement(1, 0) * v.ny +
278 normalMatrix.getElement(2, 0) * v.nz;
279 float nny = normalMatrix.getElement(0, 1) * v.nx +
280 normalMatrix.getElement(1, 1) * v.ny +
281 normalMatrix.getElement(2, 1) * v.nz;
282 float nnz = normalMatrix.getElement(0, 2) * v.nx +
283 normalMatrix.getElement(1, 2) * v.ny +
284 normalMatrix.getElement(2, 2) * v.nz;
285 Vector3 transformedNormal = Vector3(nnx, nny, nnz).normalized();
286 v.nx = transformedNormal.getX(); v.ny = transformedNormal.getY(); v.nz = transformedNormal.getZ();
287
288 // Transform tangent.xyz by normal matrix (3x3 part only), preserve w (handedness).
289 float ttx = normalMatrix.getElement(0, 0) * v.tx +
290 normalMatrix.getElement(1, 0) * v.ty +
291 normalMatrix.getElement(2, 0) * v.tz;
292 float tty = normalMatrix.getElement(0, 1) * v.tx +
293 normalMatrix.getElement(1, 1) * v.ty +
294 normalMatrix.getElement(2, 1) * v.tz;
295 float ttz = normalMatrix.getElement(0, 2) * v.tx +
296 normalMatrix.getElement(1, 2) * v.ty +
297 normalMatrix.getElement(2, 2) * v.tz;
298 Vector3 transformedTangent = Vector3(ttx, tty, ttz).normalized();
299 v.tx = transformedTangent.getX(); v.ty = transformedTangent.getY(); v.tz = transformedTangent.getZ();
300 // v.tw (handedness) is preserved unchanged.
301
302 // UVs are unchanged.
303 mergedVertices.push_back(v);
304 }
305
306 // Remap indices with vertex offset.
307 if (ib && !ib->storage().empty()) {
308 const int idxCount = ib->numIndices();
309 const auto* idxData = ib->storage().data();
310
311 if (ib->format() == INDEXFORMAT_UINT16) {
312 const auto* idx16 = reinterpret_cast<const uint16_t*>(idxData);
313 for (int i = 0; i < idxCount; i++) {
314 mergedIndices.push_back(static_cast<uint32_t>(idx16[i]) + vertexOffset);
315 }
316 } else if (ib->format() == INDEXFORMAT_UINT32) {
317 const auto* idx32 = reinterpret_cast<const uint32_t*>(idxData);
318 for (int i = 0; i < idxCount; i++) {
319 mergedIndices.push_back(idx32[i] + vertexOffset);
320 }
321 } else {
322 // UINT8
323 for (int i = 0; i < idxCount; i++) {
324 mergedIndices.push_back(static_cast<uint32_t>(idxData[i]) + vertexOffset);
325 }
326 }
327 } else {
328 // Non-indexed: generate identity indices.
329 for (int i = 0; i < vertCount; i++) {
330 mergedIndices.push_back(vertexOffset + static_cast<uint32_t>(i));
331 }
332 }
333
334 // Merge AABB.
335 BoundingBox instanceAabb = mi->aabb();
336 if (!aabbInitialized) {
337 mergedAabb = instanceAabb;
338 aabbInitialized = true;
339 } else {
340 mergedAabb.add(instanceAabb);
341 }
342
343 vertexOffset += static_cast<uint32_t>(vertCount);
344 }
345
346 // --- 4. Create GPU buffers. ---
347 const int mergedVertCount = static_cast<int>(mergedVertices.size());
348 const int mergedIdxCount = static_cast<int>(mergedIndices.size());
349
350 // Vertex buffer: same format as original (PackedVertex = 56 bytes).
351 auto vertFormat = meshInstances[0]->mesh()->getVertexBuffer()->format();
352 VertexBufferOptions vbOpts;
353 vbOpts.data.resize(mergedVertCount * sizeof(PackedVertex));
354 std::memcpy(vbOpts.data.data(), mergedVertices.data(), vbOpts.data.size());
355
356 auto mergedVB = _device->createVertexBuffer(vertFormat, mergedVertCount, vbOpts);
357 if (!mergedVB) {
358 spdlog::warn("[BatchManager] Failed to create merged vertex buffer ({} verts)", mergedVertCount);
359 return nullptr;
360 }
361 mergedVB->unlock();
362
363 // Index buffer: always UINT32 for merged geometry (may exceed 65535 vertices).
364 std::vector<uint8_t> idxData(mergedIdxCount * sizeof(uint32_t));
365 std::memcpy(idxData.data(), mergedIndices.data(), idxData.size());
366
367 auto mergedIB = _device->createIndexBuffer(INDEXFORMAT_UINT32, mergedIdxCount, idxData);
368 if (!mergedIB) {
369 spdlog::warn("[BatchManager] Failed to create merged index buffer ({} indices)", mergedIdxCount);
370 return nullptr;
371 }
372
373 // --- 5. Create Mesh. ---
374 auto mergedMesh = std::make_shared<Mesh>();
375 mergedMesh->setVertexBuffer(mergedVB);
376 mergedMesh->setIndexBuffer(mergedIB);
377 mergedMesh->setAabb(mergedAabb);
378
379 Primitive prim;
380 prim.type = PRIMITIVE_TRIANGLES;
381 prim.base = 0;
382 prim.count = mergedIdxCount;
383 prim.indexed = true;
384 mergedMesh->setPrimitive(prim);
385
386 // --- 6. Create MeshInstance. ---
387 auto batch = std::make_unique<Batch>();
388 batch->batchGroupId = batchGroupId;
389 batch->mesh = mergedMesh;
390 batch->vertexBuffer = mergedVB;
391 batch->indexBuffer = mergedIB;
392
393 // Use identity transform — geometry is already in world space.
394 batch->node.setPosition(Vector3(0, 0, 0));
395
396 Material* sharedMaterial = meshInstances[0]->material();
397 batch->meshInstance = std::make_unique<MeshInstance>(
398 mergedMesh.get(), sharedMaterial, &batch->node);
399
400 // Inherit shadow flags from first original.
401 batch->meshInstance->setCastShadow(meshInstances[0]->castShadow());
402 batch->meshInstance->setReceiveShadow(meshInstances[0]->receiveShadow());
403
404 // Hide original mesh instances.
405 for (auto* mi : meshInstances) {
406 mi->setVisible(false);
407 batch->origMeshInstances.push_back(mi);
408 }
409
410 return batch;
411 }
412 // -------------------------------------------------------------------------
413 // updateAll() —
414 // -------------------------------------------------------------------------
416 {
417 for (auto& batch : _batches) {
418 if (!batch->dynamic) continue;
419 if (batch->skinBatchInstance) {
420 batch->skinBatchInstance->updateMatrices();
421 }
422 batch->updateBoundingBox();
423 }
424 }
425
426 // -------------------------------------------------------------------------
427 // createDynamicBatch() — (dynamic path)
428 // Uses Metal buffer (slot 6) for bone data.
429 // -------------------------------------------------------------------------
430 std::unique_ptr<Batch> BatchManager::createDynamicBatch(
431 const std::vector<MeshInstance*>& meshInstances, int batchGroupId)
432 {
433 if (meshInstances.empty() || !_device) return nullptr;
434
435 // --- 1. Count total vertices and indices. ---
436 int totalVertices = 0;
437 int totalIndices = 0;
438 for (auto* mi : meshInstances) {
439 auto vb = mi->mesh()->getVertexBuffer();
440 auto ib = mi->mesh()->getIndexBuffer();
441 if (!vb) continue;
442
443 totalVertices += vb->numVertices();
444 if (ib) {
445 totalIndices += ib->numIndices();
446 } else {
447 totalIndices += vb->numVertices();
448 }
449 }
450
451 if (totalVertices == 0) return nullptr;
452
453 // --- 2. Allocate merged buffers (DynamicBatchVertex = 60 bytes). ---
454 std::vector<DynamicBatchVertex> mergedVertices;
455 mergedVertices.reserve(totalVertices);
456
457 std::vector<uint32_t> mergedIndices;
458 mergedIndices.reserve(totalIndices);
459
460 // Collect node pointers for SkinBatchInstance.
461 std::vector<GraphNode*> boneNodes;
462 boneNodes.reserve(meshInstances.size());
463
464 uint32_t vertexOffset = 0;
465
466 // --- 3. Merge geometry (local space — no world transform). ---
467 for (int instIdx = 0; instIdx < static_cast<int>(meshInstances.size()); ++instIdx) {
468 auto* mi = meshInstances[instIdx];
469 auto vb = mi->mesh()->getVertexBuffer();
470 auto ib = mi->mesh()->getIndexBuffer();
471 if (!vb || vb->storage().empty()) continue;
472
473 const int vertCount = vb->numVertices();
474 const auto* srcVerts = reinterpret_cast<const PackedVertex*>(vb->storage().data());
475
476 // Copy vertices in local space (no world transform) + set bone index.
477 for (int i = 0; i < vertCount; i++) {
478 const PackedVertex& sv = srcVerts[i];
479 DynamicBatchVertex dv;
480 dv.px = sv.px; dv.py = sv.py; dv.pz = sv.pz;
481 dv.nx = sv.nx; dv.ny = sv.ny; dv.nz = sv.nz;
482 dv.u = sv.u; dv.v = sv.v;
483 dv.tx = sv.tx; dv.ty = sv.ty; dv.tz = sv.tz; dv.tw = sv.tw;
484 dv.u1 = sv.u1; dv.v1 = sv.v1;
485 dv.boneIndex = static_cast<float>(instIdx);
486 mergedVertices.push_back(dv);
487 }
488
489 // Remap indices with vertex offset.
490 if (ib && !ib->storage().empty()) {
491 const int idxCount = ib->numIndices();
492 const auto* idxData = ib->storage().data();
493
494 if (ib->format() == INDEXFORMAT_UINT16) {
495 const auto* idx16 = reinterpret_cast<const uint16_t*>(idxData);
496 for (int i = 0; i < idxCount; i++) {
497 mergedIndices.push_back(static_cast<uint32_t>(idx16[i]) + vertexOffset);
498 }
499 } else if (ib->format() == INDEXFORMAT_UINT32) {
500 const auto* idx32 = reinterpret_cast<const uint32_t*>(idxData);
501 for (int i = 0; i < idxCount; i++) {
502 mergedIndices.push_back(idx32[i] + vertexOffset);
503 }
504 } else {
505 for (int i = 0; i < idxCount; i++) {
506 mergedIndices.push_back(static_cast<uint32_t>(idxData[i]) + vertexOffset);
507 }
508 }
509 } else {
510 for (int i = 0; i < vertCount; i++) {
511 mergedIndices.push_back(vertexOffset + static_cast<uint32_t>(i));
512 }
513 }
514
515 // Collect node for matrix palette.
516 boneNodes.push_back(mi->node());
517
518 vertexOffset += static_cast<uint32_t>(vertCount);
519 }
520
521 // --- 4. Create GPU buffers (DynamicBatchVertex = 60 bytes). ---
522 const int mergedVertCount = static_cast<int>(mergedVertices.size());
523 const int mergedIdxCount = static_cast<int>(mergedIndices.size());
524
525 // Build vertex format for dynamic batch (60 bytes stride).
526 auto dynamicFormat = std::make_shared<VertexFormat>(
527 static_cast<int>(sizeof(DynamicBatchVertex)));
528
529 VertexBufferOptions vbOpts;
530 vbOpts.data.resize(mergedVertCount * sizeof(DynamicBatchVertex));
531 std::memcpy(vbOpts.data.data(), mergedVertices.data(), vbOpts.data.size());
532
533 auto mergedVB = _device->createVertexBuffer(dynamicFormat, mergedVertCount, vbOpts);
534 if (!mergedVB) {
535 spdlog::warn("[BatchManager] Failed to create dynamic batch vertex buffer ({} verts)", mergedVertCount);
536 return nullptr;
537 }
538 mergedVB->unlock();
539
540 // Index buffer: always UINT32.
541 std::vector<uint8_t> idxData(mergedIdxCount * sizeof(uint32_t));
542 std::memcpy(idxData.data(), mergedIndices.data(), idxData.size());
543
544 auto mergedIB = _device->createIndexBuffer(INDEXFORMAT_UINT32, mergedIdxCount, idxData);
545 if (!mergedIB) {
546 spdlog::warn("[BatchManager] Failed to create dynamic batch index buffer ({} indices)", mergedIdxCount);
547 return nullptr;
548 }
549
550 // --- 5. Create Mesh. ---
551 auto mergedMesh = std::make_shared<Mesh>();
552 mergedMesh->setVertexBuffer(mergedVB);
553 mergedMesh->setIndexBuffer(mergedIB);
554
555 Primitive prim;
556 prim.type = PRIMITIVE_TRIANGLES;
557 prim.base = 0;
558 prim.count = mergedIdxCount;
559 prim.indexed = true;
560 mergedMesh->setPrimitive(prim);
561
562 // --- 6. Create Batch + MeshInstance + SkinBatchInstance. ---
563 auto batch = std::make_unique<Batch>();
564 batch->batchGroupId = batchGroupId;
565 batch->dynamic = true;
566 batch->mesh = mergedMesh;
567 batch->vertexBuffer = mergedVB;
568 batch->indexBuffer = mergedIB;
569
570 // Identity transform — per-instance transforms come from palette.
571 batch->node.setPosition(Vector3(0, 0, 0));
572
573 Material* sharedMaterial = meshInstances[0]->material();
574 batch->meshInstance = std::make_unique<MeshInstance>(
575 mergedMesh.get(), sharedMaterial, &batch->node);
576
577 // Mark the batch MeshInstance as a dynamic batch for shader variant selection.
578 batch->meshInstance->setDynamicBatch(true);
579
580 // Inherit shadow flags from first original.
581 batch->meshInstance->setCastShadow(meshInstances[0]->castShadow());
582 batch->meshInstance->setReceiveShadow(meshInstances[0]->receiveShadow());
583
584 // Create SkinBatchInstance with node pointers.
585 batch->skinBatchInstance = std::make_unique<SkinBatchInstance>(std::move(boneNodes));
586 batch->meshInstance->setSkinBatchInstance(batch->skinBatchInstance.get());
587
588 // Initial matrix update + AABB.
589 batch->skinBatchInstance->updateMatrices();
590 batch->updateBoundingBox();
591
592 // Hide original mesh instances.
593 for (auto* mi : meshInstances) {
594 mi->setVisible(false);
595 batch->origMeshInstances.push_back(mi);
596 }
597
598 spdlog::debug("[BatchManager] Dynamic batch: {} instances, {} verts, {} indices, {} bones",
599 meshInstances.size(), mergedVertCount, mergedIdxCount, boneNodes.size());
600
601 return batch;
602 }
603
604} // visutwin::canvas
BatchManager(GraphicsDevice *device)
const BatchGroup * getGroupById(int groupId) const
void prepare(Scene *scene=nullptr)
void addGroup(const BatchGroup &group)
void destroy(Scene *scene=nullptr)
Abstract GPU interface for resource creation, state management, and draw submission.
Base class for GPU materials — owns uniform data, texture bindings, blend/depth state,...
Definition material.h:143
static const std::vector< RenderComponent * > & instances()
Container for the scene graph, lighting environment, fog, skybox, and layer composition.
Definition scene.h:29
const std::shared_ptr< LayerComposition > & layers() const
Definition scene.h:40
@ PRIMITIVE_TRIANGLES
Definition mesh.h:23
static Matrix4 identity()
Definition matrix4.h:108