Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
MeshGPUResources.cpp
Go to the documentation of this file.
2
3#include "LegacyShadow.hpp"
4
5#include <SDL3/SDL.h>
6
7#include <algorithm>
8#include <cstring>
9#include <limits>
10#include <new>
11#include <stdexcept>
12#include <unordered_map>
13#include <unordered_set>
14#include <utility>
15
16namespace monopoly::engine
17{
18 namespace
19 {
20 constexpr std::uint64_t D3D12TextureOffsetAlignment = 512U;
21 constexpr std::uint64_t D3D12TextureRowAlignment = 256U;
22 constexpr std::uint64_t RGBABytesPerPixel = 4U;
23
24 MeshGPUError error(MeshGPUErrorCode code, const char* detail = nullptr)
25 {
26 MeshGPUError result;
27 result.code = code;
28 if (detail != nullptr) result.detail = detail;
29 else if (const char* sdl = SDL_GetError(); sdl != nullptr) result.detail = sdl;
30 return result;
31 }
32
33 std::expected<std::uint32_t, MeshGPUError> byteSize(
34 std::size_t count, std::size_t elementSize)
35 {
36 if (count == 0 || elementSize == 0)
37 return std::unexpected(error(MeshGPUErrorCode::EmptyGeometry,
38 "GPU geometry buffers must be non-empty"));
39 if (count > std::numeric_limits<std::uint32_t>::max() / elementSize)
40 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
41 "GPU buffer byte size exceeds SDL Uint32 range"));
42 return static_cast<std::uint32_t>(count * elementSize);
43 }
44
45 std::uint64_t alignUp(std::uint64_t value, std::uint64_t alignment) noexcept
46 {
47 return (value + alignment - 1U) / alignment * alignment;
48 }
49
50 struct TextureUpload
51 {
52 std::uint64_t key{};
53 std::shared_ptr<const data::HmdTextureImage> source;
54 std::uint32_t width{};
55 std::uint32_t height{};
56 std::uint32_t rowPixels{};
57 std::uint32_t transferOffset{};
58 std::uint32_t transferBytes{};
60 };
61
62 std::expected<std::vector<TextureUpload>, MeshGPUError> textureUploads(
63 const data::MeshRenderData& renderData, std::uint32_t geometryBytes,
64 std::uint32_t& totalTransferBytes)
65 {
66 std::vector<TextureUpload> result;
67 std::unordered_map<std::uint64_t, std::size_t> keys;
68 std::uint64_t cursor = geometryBytes;
69
70 for (const auto& batch : renderData.batches)
71 {
72 if (!batch.texture) continue;
73 const auto& region = *batch.texture;
74 if (!region.sourceImage)
75 return std::unexpected(error(MeshGPUErrorCode::MissingTexturePixels,
76 "textured mesh batch has no immutable HMD RGBA source image"));
77
78 const auto& image = *region.sourceImage;
79 if (region.width == 0 || region.height == 0 || image.width == 0 ||
80 image.height == 0 || image.width != region.width ||
81 image.height != region.height)
82 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
83 "HMD texture region dimensions do not match its decoded image"));
84
85 const std::uint64_t pixelBytes =
86 static_cast<std::uint64_t>(image.width) * image.height * RGBABytesPerPixel;
87 if (pixelBytes != image.rgba.size())
88 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
89 "decoded HMD texture does not contain width*height RGBA8 bytes"));
90
91 if (const auto found = keys.find(region.key); found != keys.end())
92 {
93 const auto& previous = result[found->second];
94 if (previous.source != region.sourceImage ||
95 previous.width != image.width || previous.height != image.height)
96 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
97 "one mesh texture key refers to conflicting immutable images"));
98 continue;
99 }
100
101 const std::uint64_t rawRowBytes =
102 static_cast<std::uint64_t>(image.width) * RGBABytesPerPixel;
103 const std::uint64_t paddedRowBytes =
105 const std::uint64_t rowPixels = paddedRowBytes / RGBABytesPerPixel;
106 const std::uint64_t textureBytes = paddedRowBytes * image.height;
108 if (rowPixels > std::numeric_limits<std::uint32_t>::max() ||
109 textureBytes > std::numeric_limits<std::uint32_t>::max() ||
110 cursor > std::numeric_limits<std::uint32_t>::max() ||
111 textureBytes > std::numeric_limits<std::uint32_t>::max() - cursor)
112 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
113 "aligned HMD texture upload exceeds SDL Uint32 transfer range"));
114
115 TextureUpload upload;
116 upload.key = region.key;
117 upload.source = region.sourceImage;
118 upload.width = image.width;
119 upload.height = image.height;
120 upload.rowPixels = static_cast<std::uint32_t>(rowPixels);
121 upload.transferOffset = static_cast<std::uint32_t>(cursor);
122 upload.transferBytes = static_cast<std::uint32_t>(textureBytes);
123 keys.emplace(upload.key, result.size());
124 result.push_back(std::move(upload));
126 }
127
128 if (cursor > std::numeric_limits<std::uint32_t>::max())
129 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
130 "combined mesh and texture transfer exceeds SDL Uint32 range"));
131 totalTransferBytes = static_cast<std::uint32_t>(cursor);
132 return result;
133 }
134
136 {
139 }
141 {
142 switch (wrap)
143 {
146 default: return SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
147 }
148 }
150 {
152 return filter == Filter::Nearest || filter == Filter::NearestMipmapNearest ||
153 filter == Filter::NearestMipmapLinear ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
154 }
155 std::expected<MeshGPUModernTextureResource, MeshGPUError> uploadModernTexture(
156 SDL_GPUDevice* device, const data::ModernTextureBinding& binding)
157 {
158 if (!binding.image)
159 return std::unexpected(error(MeshGPUErrorCode::MissingTexturePixels,
160 "modern texture has no immutable RGBA source image"));
162 binding.sampler.magFilter > data::ModernTextureFilter::Linear ||
166 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
167 "modern texture contains an invalid sampler or color-space enum"));
168 const auto& image = *binding.image;
169 if (!image.width || !image.height ||
170 static_cast<std::uint64_t>(image.width) * 4U > std::numeric_limits<std::uint32_t>::max() ||
171 image.height > std::numeric_limits<std::uint32_t>::max() /
172 (static_cast<std::uint64_t>(image.width) * 4U))
173 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
174 "modern RGBA image exceeds Uint32 transfer range"));
175 const std::uint64_t rawBytes = static_cast<std::uint64_t>(image.width) * image.height * 4U;
176 if (rawBytes != image.rgba.size() || binding.texCoord != 0U)
177 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
178 "modern texture requires valid width*height RGBA8 and TEXCOORD_0"));
179 if (binding.sampler.magFilter != data::ModernTextureFilter::Nearest &&
180 binding.sampler.magFilter != data::ModernTextureFilter::Linear)
181 return std::unexpected(error(MeshGPUErrorCode::InvalidTexturePixels,
182 "modern magnification filter must be nearest or linear"));
183 const auto rowBytes = alignUp(static_cast<std::uint64_t>(image.width) * 4U,
185 if (image.height > std::numeric_limits<std::uint32_t>::max() / rowBytes)
186 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
187 "aligned modern texture upload exceeds Uint32 transfer range"));
188 const auto transferBytes = rowBytes * image.height;
189 if (transferBytes > std::numeric_limits<std::uint32_t>::max())
190 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
191 "modern texture upload exceeds Uint32 transfer range"));
192 MeshGPUModernTextureResource result;
193 result.source = binding;
194 const bool mipmaps = usesMipmaps(binding.sampler.minFilter);
195 if (mipmaps)
196 for (auto size = std::max(image.width, image.height); size > 1U; size >>= 1U)
197 ++result.mipLevels;
203 if (result.mipLevels > 1U) textureInfo.usage |= SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
204 if (!SDL_GPUTextureSupportsFormat(device, textureInfo.format,
205 textureInfo.type, textureInfo.usage))
207 "device does not support modern texture color space/mipmap render usage"));
208 textureInfo.width = image.width;
209 textureInfo.height = image.height;
210 textureInfo.layer_count_or_depth = 1U;
211 textureInfo.num_levels = result.mipLevels;
212 textureInfo.sample_count = SDL_GPU_SAMPLECOUNT_1;
213 result.texture = SDL_CreateGPUTexture(device, &textureInfo);
214 if (!result.texture)
215 return std::unexpected(error(MeshGPUErrorCode::TextureCreationFailed));
216 const auto cleanup = [&]()
217 {
218 if (result.sampler) SDL_ReleaseGPUSampler(device, result.sampler);
219 SDL_ReleaseGPUTexture(device, result.texture);
220 };
222 samplerInfo.min_filter = imageFilter(binding.sampler.minFilter);
223 samplerInfo.mag_filter = imageFilter(binding.sampler.magFilter);
225 samplerInfo.mipmap_mode = binding.sampler.minFilter == Filter::NearestMipmapLinear ||
226 binding.sampler.minFilter == Filter::LinearMipmapLinear
228 samplerInfo.address_mode_u = addressMode(binding.sampler.wrapS);
229 samplerInfo.address_mode_v = addressMode(binding.sampler.wrapT);
231 samplerInfo.min_lod = 0.0F;
232 samplerInfo.max_lod = static_cast<float>(result.mipLevels - 1U);
233 // Modern linear mipmapped maps use8x anisotropy; explicit0 opts out.
234 // Authored nearest/no-mip and retail filtering remain unchanged.
235 const auto* anisotropy = SDL_GetEnvironmentVariable(SDL_GetEnvironment(), "MONOPOLY_MODERN_TEXTURE_ANISOTROPY");
236 const bool linearMipmapped = result.mipLevels > 1U &&
238 samplerInfo.enable_anisotropy = linearMipmapped && (!anisotropy || std::strcmp(anisotropy, "8") == 0);
239 samplerInfo.max_anisotropy = samplerInfo.enable_anisotropy ? 8.0F : 1.0F;
240 result.sampler = SDL_CreateGPUSampler(device, &samplerInfo);
241 if (!result.sampler && samplerInfo.enable_anisotropy)
242 {
243 // An optional presentation filter must not reject valid geometry.
244 SDL_LogWarn(SDL_LOG_CATEGORY_RENDER,"Optional modern8x anisotropy unavailable; retrying authored sampler: %s",SDL_GetError());
245 samplerInfo.enable_anisotropy = false; samplerInfo.max_anisotropy = 1.0F;
246 result.sampler = SDL_CreateGPUSampler(device, &samplerInfo);
247 if(result.sampler) SDL_ClearError();
248 }
249 if(result.sampler && anisotropy)
250 SDL_LogInfo(SDL_LOG_CATEGORY_RENDER,"Modern sampler qualification: %ux%u mips=%u anisotropy=%u",
251 image.width,image.height,result.mipLevels,samplerInfo.enable_anisotropy ? 8U : 1U);
252 if (!result.sampler)
253 {
254 cleanup();
255 return std::unexpected(error(MeshGPUErrorCode::SamplerCreationFailed));
256 }
259 transferInfo.size = static_cast<Uint32>(transferBytes);
261 if (!transfer)
262 {
263 cleanup();
265 }
266 auto* mapped = static_cast<std::uint8_t*>(SDL_MapGPUTransferBuffer(device, transfer, false));
267 if (!mapped)
268 {
270 cleanup();
271 return std::unexpected(error(MeshGPUErrorCode::TransferMapFailed));
272 }
273 std::memset(mapped, 0, static_cast<std::size_t>(transferBytes));
274 for (std::uint32_t row = 0; row < image.height; ++row)
275 std::memcpy(mapped + static_cast<std::size_t>(row * rowBytes),
276 image.rgba.data() + static_cast<std::size_t>(row) * image.width * 4U,
277 static_cast<std::size_t>(image.width) * 4U);
279 auto* command = SDL_AcquireGPUCommandBuffer(device);
280 if (!command)
281 {
283 cleanup();
285 }
286 auto* copy = SDL_BeginGPUCopyPass(command);
287 if (!copy)
288 {
291 cleanup();
292 return std::unexpected(error(MeshGPUErrorCode::CopyPassCreationFailed));
293 }
295 source.transfer_buffer = transfer;
296 source.pixels_per_row = static_cast<Uint32>(rowBytes / 4U);
297 source.rows_per_layer = image.height;
299 destination.texture = result.texture;
300 destination.w = image.width;
301 destination.h = image.height;
302 destination.d = 1U;
305 if (result.mipLevels > 1U) SDL_GenerateMipmapsForGPUTexture(command, result.texture);
306 if (!SDL_SubmitGPUCommandBuffer(command))
307 {
309 cleanup();
310 return std::unexpected(error(MeshGPUErrorCode::SubmitFailed));
311 }
313 return result;
314 }
315 std::expected<void, MeshGPUError> uploadModernTextures(
316 SDL_GPUDevice* device, MeshGPUResource& resource, const data::MeshRenderData& renderData)
317 {
318 // Allocate CPU ownership storage before creating any modern GPU handles.
319 // Subsequent pushes move only shared ownership pointers and POD metadata.
320 std::size_t mapCount{};
321 for (const auto& batch : renderData.batches)
322 {
323 if (batch.material.model != data::MeshMaterialModel::MetallicRoughness) continue;
324 const std::size_t count = static_cast<std::size_t>(batch.material.baseColorTexture.has_value()) +
325 static_cast<std::size_t>(batch.material.metallicRoughnessTexture.has_value()) +
326 static_cast<std::size_t>(batch.material.normalTexture.has_value()) +
327 static_cast<std::size_t>(batch.material.emissiveTexture.has_value()) +
328 static_cast<std::size_t>(batch.material.occlusionTexture.has_value());
329 if (count > resource.modernTextures.max_size() - mapCount)
330 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
331 "modern texture ownership storage exceeds vector range"));
332 mapCount += count;
333 }
334 try
335 {
336 resource.modernTextures.reserve(mapCount);
337 }
338 catch (const std::bad_alloc&)
339 {
340 return std::unexpected(error(MeshGPUErrorCode::HostAllocationFailed,
341 "modern texture ownership allocation failed"));
342 }
343 catch (const std::length_error&)
344 {
345 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
346 "modern texture ownership allocation exceeds vector range"));
347 }
348 for (const auto& batch : renderData.batches)
349 {
350 if (batch.material.model != data::MeshMaterialModel::MetallicRoughness) continue;
351 const std::array<const std::optional<data::ModernTextureBinding>*, 5> maps{
352 &batch.material.baseColorTexture, &batch.material.metallicRoughnessTexture,
353 &batch.material.normalTexture, &batch.material.emissiveTexture,
354 &batch.material.occlusionTexture};
355 for (const auto* map : maps)
356 {
357 if (!*map || resource.modernTexture(**map)) continue;
358 auto uploaded = uploadModernTexture(device, **map);
359 if (!uploaded) return std::unexpected(uploaded.error());
360 resource.modernTextures.push_back(std::move(*uploaded));
361 }
362 }
363 return {};
364 }
365
366 std::expected<void, MeshGPUError> uploadDynamicVertexBuffer(
368 std::span<const MeshGPUVertex> vertices, std::uint32_t vertexBytes,
369 bool cycle)
370 {
373 transferInfo.size = vertexBytes;
376 if (transfer == nullptr)
377 return std::unexpected(error(
379
380 void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
381 if (mapped == nullptr)
382 {
384 return std::unexpected(error(MeshGPUErrorCode::TransferMapFailed));
385 }
386 std::memcpy(mapped, vertices.data(), vertexBytes);
388
390 if (command == nullptr)
391 {
393 return std::unexpected(error(
395 }
397 if (copy == nullptr)
398 {
401 return std::unexpected(error(MeshGPUErrorCode::CopyPassCreationFailed));
402 }
404 SDL_GPUBufferRegion destination{buffer, 0, vertexBytes};
407 if (!SDL_SubmitGPUCommandBuffer(command))
408 {
410 return std::unexpected(error(MeshGPUErrorCode::SubmitFailed));
411 }
413 return {};
414 }
415 }
416
417 std::expected<MeshGPUUploadPlan, MeshGPUError>
419 {
420 auto vertexBytes = byteSize(renderData.vertices.size(), sizeof(MeshGPUVertex));
421 if (!vertexBytes) return std::unexpected(vertexBytes.error());
422 auto indexBytes = byteSize(renderData.indices.size(), sizeof(std::uint32_t));
423 if (!indexBytes) return std::unexpected(indexBytes.error());
424 if (*vertexBytes > std::numeric_limits<std::uint32_t>::max() - *indexBytes)
425 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
426 "combined GPU geometry transfer exceeds SDL Uint32 range"));
427
428 for (const auto index : renderData.indices)
429 if (index >= renderData.vertices.size())
430 return std::unexpected(error(MeshGPUErrorCode::InvalidIndex,
431 "mesh render index references a missing vertex"));
432 for (const auto& batch : renderData.batches)
433 {
434 if (batch.firstIndex > renderData.indices.size() ||
435 batch.indexCount > renderData.indices.size() - batch.firstIndex)
436 return std::unexpected(error(MeshGPUErrorCode::InvalidBatchRange,
437 "mesh render batch exceeds the index array"));
438 }
439
441 plan.vertexBytes = *vertexBytes;
442 plan.indexBytes = *indexBytes;
443 plan.transferBytes = *vertexBytes + *indexBytes;
444 plan.vertices.reserve(renderData.vertices.size());
445 for (const auto& vertex : renderData.vertices)
446 {
448 for (std::size_t i = 0; i < 3; ++i)
449 {
450 packed.position[i] = vertex.position[i];
451 packed.normal[i] = vertex.normal[i];
452 }
453 packed.uv[0] = vertex.uv[0];
454 packed.uv[1] = vertex.uv[1];
455 for (std::size_t i = 0; i < 4; ++i) packed.tangent[i] = vertex.tangent[i];
456 plan.vertices.push_back(packed);
457 }
458 plan.indices = renderData.indices;
459 return plan;
460 }
461
464
465 void MeshGPUCache::release(MeshGPUResource& resource) noexcept
466 {
467 if (device_ != nullptr && resource.vertexBuffer != nullptr)
468 SDL_ReleaseGPUBuffer(device_, resource.vertexBuffer);
469 if (device_ != nullptr && resource.indexBuffer != nullptr)
470 SDL_ReleaseGPUBuffer(device_, resource.indexBuffer);
471 if (device_ != nullptr)
472 for (auto& [key, texture] : resource.textures)
473 {
474 (void)key;
475 if (texture.texture != nullptr)
476 SDL_ReleaseGPUTexture(device_, texture.texture);
477 texture.texture = nullptr;
478 texture.source.reset();
479 }
480 for (auto& texture : resource.modernTextures)
481 {
482 if (device_ && texture.texture) SDL_ReleaseGPUTexture(device_, texture.texture);
483 if (device_ && texture.sampler) SDL_ReleaseGPUSampler(device_, texture.sampler);
484 }
485 resource.modernTextures.clear();
486 resource.vertexBuffer = nullptr;
487 resource.indexBuffer = nullptr;
488 resource.textures.clear();
489 resource.source.reset();
490 }
491
492 void MeshGPUCache::release(MeshGPUDynamicVertexResource& resource) noexcept
493 {
494 if (device_ != nullptr && resource.vertexBuffer != nullptr)
495 SDL_ReleaseGPUBuffer(device_, resource.vertexBuffer);
496 resource.vertexBuffer = nullptr;
497 resource.sourceAsset.reset();
498 resource.sourceRenderData.reset();
499 }
500
501 void MeshGPUCache::eraseDynamicForDataId(data::DataId id) noexcept
502 {
503 for (auto iterator = dynamicVertices_.begin();
504 iterator != dynamicVertices_.end();)
505 {
506 if (iterator->second.dataId != id)
507 {
508 ++iterator;
509 continue;
510 }
511 release(iterator->second);
512 iterator = dynamicVertices_.erase(iterator);
513 }
514 }
515
516 std::expected<const MeshGPUResource*, MeshGPUError>
517 MeshGPUCache::resolve(std::shared_ptr<const data::MeshRuntimeAsset> asset)
518 {
519 return resolveImpl(std::move(asset), false);
520 }
521
522 std::expected<const MeshGPUResource*, MeshGPUError>
523 MeshGPUCache::resolveForScene(std::shared_ptr<const data::MeshRuntimeAsset> asset)
524 {
525 return resolveImpl(std::move(asset), true);
526 }
527
528 std::expected<const MeshGPUResource*, MeshGPUError>
529 MeshGPUCache::resolveImpl(std::shared_ptr<const data::MeshRuntimeAsset> asset, bool coexist)
530 {
531 if (device_ == nullptr)
532 return std::unexpected(error(MeshGPUErrorCode::MissingDevice,
533 "GPU mesh cache has no SDL_GPUDevice"));
534 if (!asset || !asset->renderData)
535 return std::unexpected(error(MeshGPUErrorCode::MissingAsset,
536 "GPU mesh upload requires immutable CPU render data"));
537
538 if (const auto found = resources_.find(asset.get()); found != resources_.end())
539 {
540 if (!coexist) eraseOtherAssets(asset->dataId, asset.get());
541 found->second.lastResolvedOrder = ++resolvedOrder_;
542 return &found->second;
543 }
544
545 auto plan = makeMeshGPUUploadPlan(*asset->renderData);
546 if (!plan) return std::unexpected(plan.error());
547 std::uint32_t totalTransferBytes = plan->transferBytes;
548 auto texturePlan = textureUploads(*asset->renderData,
549 plan->transferBytes, totalTransferBytes);
550 if (!texturePlan) return std::unexpected(texturePlan.error());
551
552 MeshGPUResource replacement;
553 replacement.dataId = asset->dataId;
554 replacement.vertexCount = static_cast<std::uint32_t>(plan->vertices.size());
555 replacement.indexCount = static_cast<std::uint32_t>(plan->indices.size());
556 replacement.source = asset;
557
558 auto cleanupReplacement = [&]() noexcept { release(replacement); };
559
562 vertexInfo.size = plan->vertexBytes;
563 replacement.vertexBuffer = SDL_CreateGPUBuffer(device_, &vertexInfo);
564 if (replacement.vertexBuffer == nullptr)
566
569 indexInfo.size = plan->indexBytes;
570 replacement.indexBuffer = SDL_CreateGPUBuffer(device_, &indexInfo);
571 if (replacement.indexBuffer == nullptr)
572 {
574 return std::unexpected(error(MeshGPUErrorCode::IndexBufferCreationFailed));
575 }
576
577 for (auto& upload : *texturePlan)
578 {
583 textureInfo.width = upload.width;
584 textureInfo.height = upload.height;
585 textureInfo.layer_count_or_depth = 1U;
586 textureInfo.num_levels = 1U;
587 textureInfo.sample_count = SDL_GPU_SAMPLECOUNT_1;
588 upload.texture = SDL_CreateGPUTexture(device_, &textureInfo);
589 if (upload.texture == nullptr)
590 {
591 for (auto& pending : *texturePlan)
592 if (pending.texture != nullptr)
593 {
594 SDL_ReleaseGPUTexture(device_, pending.texture);
595 pending.texture = nullptr;
596 }
598 return std::unexpected(error(MeshGPUErrorCode::TextureCreationFailed));
599 }
600 }
601
606 if (transfer == nullptr)
607 {
608 for (auto& upload : *texturePlan)
609 if (upload.texture != nullptr) SDL_ReleaseGPUTexture(device_, upload.texture);
612 }
613
614 void* mapped = SDL_MapGPUTransferBuffer(device_, transfer, false);
615 if (mapped == nullptr)
616 {
618 for (auto& upload : *texturePlan)
619 if (upload.texture != nullptr) SDL_ReleaseGPUTexture(device_, upload.texture);
621 return std::unexpected(error(MeshGPUErrorCode::TransferMapFailed));
622 }
623
624 std::memset(mapped, 0, totalTransferBytes);
625 std::memcpy(mapped, plan->vertices.data(), plan->vertexBytes);
626 std::memcpy(static_cast<std::byte*>(mapped) + plan->vertexBytes,
627 plan->indices.data(), plan->indexBytes);
628 const bool legacyShadow = data::isLegacyShadowMesh(asset->dataId);
629 for (const auto& upload : *texturePlan)
630 {
631 const auto sourceRowBytes = static_cast<std::size_t>(upload.width) * 4U;
632 const auto destinationRowBytes = static_cast<std::size_t>(upload.rowPixels) * 4U;
633 auto* destination = static_cast<std::byte*>(mapped) + upload.transferOffset;
634 for (std::uint32_t row = 0; row < upload.height; ++row)
635 {
636 const auto* source = upload.source->rgba.data() +
637 static_cast<std::size_t>(row) * sourceRowBytes;
639 static_cast<std::size_t>(row) * destinationRowBytes;
640 if (!legacyShadow)
641 {
642 std::memcpy(destinationRow, source, sourceRowBytes);
643 continue;
644 }
645
646 auto* shadow = reinterpret_cast<std::uint8_t*>(destinationRow);
647 for (std::uint32_t column = 0; column < upload.width; ++column)
648 {
649 const auto sourceOffset = static_cast<std::size_t>(column) * 4U;
652 source[sourceOffset + 2U]);
653 std::memcpy(shadow + sourceOffset,
654 converted.data(), converted.size());
655 }
656 }
657 }
659
661 if (command == nullptr)
662 {
664 for (auto& upload : *texturePlan)
665 if (upload.texture != nullptr) SDL_ReleaseGPUTexture(device_, upload.texture);
668 }
670 if (copy == nullptr)
671 {
674 for (auto& upload : *texturePlan)
675 if (upload.texture != nullptr) SDL_ReleaseGPUTexture(device_, upload.texture);
677 return std::unexpected(error(MeshGPUErrorCode::CopyPassCreationFailed));
678 }
679
682 replacement.vertexBuffer, 0, plan->vertexBytes};
686 replacement.indexBuffer, 0, plan->indexBytes};
688
689 for (const auto& upload : *texturePlan)
690 {
692 source.transfer_buffer = transfer;
693 source.offset = upload.transferOffset;
694 source.pixels_per_row = upload.rowPixels;
695 source.rows_per_layer = upload.height;
697 destination.texture = upload.texture;
698 destination.w = upload.width;
699 destination.h = upload.height;
700 destination.d = 1U;
702 }
704
705 if (!SDL_SubmitGPUCommandBuffer(command))
706 {
708 for (auto& upload : *texturePlan)
709 if (upload.texture != nullptr) SDL_ReleaseGPUTexture(device_, upload.texture);
711 return std::unexpected(error(MeshGPUErrorCode::SubmitFailed));
712 }
714
715 SDL_SetGPUBufferName(device_, replacement.vertexBuffer, "Monopoly mesh vertices");
716 SDL_SetGPUBufferName(device_, replacement.indexBuffer, "Monopoly mesh indices");
717 for (auto& upload : *texturePlan)
718 {
719 replacement.textures.emplace(upload.key, MeshGPUTextureResource{
720 upload.key, upload.texture, upload.width, upload.height, upload.source});
721 upload.texture = nullptr;
722 }
723
724 auto modernUploaded = uploadModernTextures(device_, replacement, *asset->renderData);
725 if (!modernUploaded)
726 {
728 return std::unexpected(modernUploaded.error());
729 }
730
731 // Allocate the empty cache node before transferring raw GPU handles.
732 // An emplace of the populated replacement can destroy its moved value
733 // during a failed rehash without releasing the SDL resources it owns.
734 static_assert(noexcept(std::declval<MeshGPUResource&>() =
735 std::declval<MeshGPUResource&&>()));
736 try
737 {
738 const auto* identity = replacement.source.get();
739 auto [inserted, created] = resources_.try_emplace(identity);
740 if (!created)
741 {
742 eraseDynamicForAsset(identity);
743 release(inserted->second);
744 }
745 inserted->second = std::move(replacement);
746 inserted->second.lastResolvedOrder = ++resolvedOrder_;
747 if (!coexist) eraseOtherAssets(inserted->second.dataId, identity);
748 return &inserted->second;
749 }
750 catch (const std::bad_alloc&)
751 {
753 return std::unexpected(error(MeshGPUErrorCode::HostAllocationFailed,
754 "GPU mesh cache ownership allocation failed"));
755 }
756 catch (const std::length_error&)
757 {
759 return std::unexpected(error(MeshGPUErrorCode::SizeOverflow,
760 "GPU mesh cache ownership exceeds container range"));
761 }
762 }
763
764 std::expected<const MeshGPUDynamicVertexResource*, MeshGPUError>
766 std::shared_ptr<const data::MeshRuntimeAsset> asset,
767 std::shared_ptr<const data::MeshRenderData> renderData)
768 {
769 if (device_ == nullptr)
770 return std::unexpected(error(MeshGPUErrorCode::MissingDevice,
771 "dynamic GPU mesh cache has no SDL_GPUDevice"));
772 if (!asset || !asset->renderData || !renderData)
773 return std::unexpected(error(MeshGPUErrorCode::MissingAsset,
774 "dynamic vertex upload requires static asset and evaluated render data"));
775
776 auto staticResource = resolveForScene(asset);
777 if (!staticResource)
778 return std::unexpected(staticResource.error());
779 auto plan = makeMeshGPUUploadPlan(*renderData);
780 if (!plan)
781 return std::unexpected(plan.error());
782 if (plan->vertices.size() != (*staticResource)->vertexCount ||
783 plan->indices != asset->renderData->indices ||
784 renderData->batches.size() != asset->renderData->batches.size())
786 "animated MESHX render data changed static vertex/index topology"));
787 for (std::size_t index = 0; index < renderData->batches.size(); ++index)
788 {
789 const auto& animated = renderData->batches[index];
790 const auto& base = asset->renderData->batches[index];
791 const auto animatedTexture = animated.texture
792 ? std::optional<std::uint64_t>{animated.texture->key} : std::nullopt;
793 const auto baseTexture = base.texture
794 ? std::optional<std::uint64_t>{base.texture->key} : std::nullopt;
795 if (animated.firstIndex != base.firstIndex ||
796 animated.indexCount != base.indexCount ||
798 return std::unexpected(error(
800 "animated MESHX batch topology or texture identity changed"));
801 }
802
803 if (auto found = dynamicVertices_.find(key); found != dynamicVertices_.end())
804 {
805 if (found->second.sourceAsset == asset &&
806 found->second.sourceRenderData == renderData)
807 return &found->second;
808 if (found->second.sourceAsset == asset &&
809 found->second.vertexCount == plan->vertices.size())
810 {
811 auto uploaded = uploadDynamicVertexBuffer(device_,
812 found->second.vertexBuffer, plan->vertices,
813 plan->vertexBytes, true);
814 if (!uploaded)
815 return std::unexpected(uploaded.error());
816 found->second.dataId = asset->dataId;
817 found->second.sourceRenderData = std::move(renderData);
818 return &found->second;
819 }
820 }
821
823 replacement.key = key;
824 replacement.dataId = asset->dataId;
825 replacement.vertexCount = static_cast<std::uint32_t>(plan->vertices.size());
826 replacement.sourceAsset = asset;
827 replacement.sourceRenderData = renderData;
830 bufferInfo.size = plan->vertexBytes;
831 replacement.vertexBuffer = SDL_CreateGPUBuffer(device_, &bufferInfo);
832 if (replacement.vertexBuffer == nullptr)
833 return std::unexpected(error(
835 auto uploaded = uploadDynamicVertexBuffer(device_, replacement.vertexBuffer,
836 plan->vertices, plan->vertexBytes, false);
837 if (!uploaded)
838 {
839 release(replacement);
840 return std::unexpected(uploaded.error());
841 }
842 SDL_SetGPUBufferName(device_, replacement.vertexBuffer,
843 "Monopoly animated mesh vertices");
844
845 if (auto found = dynamicVertices_.find(key); found != dynamicVertices_.end())
846 {
847 release(found->second);
848 found->second = std::move(replacement);
849 return &found->second;
850 }
851 auto [inserted, created] = dynamicVertices_.emplace(key,
852 std::move(replacement));
853 (void)created;
854 return &inserted->second;
855 }
856
858 std::uint64_t key) const noexcept
859 {
860 const auto found = dynamicVertices_.find(key);
861 return found == dynamicVertices_.end() ? nullptr : &found->second;
862 }
863
865 { return dynamicVertices_.size(); }
866
868 std::span<const std::uint64_t> activeKeys) noexcept
869 {
870 const std::unordered_set<std::uint64_t> active(
871 activeKeys.begin(), activeKeys.end());
872 for (auto iterator = dynamicVertices_.begin();
873 iterator != dynamicVertices_.end();)
874 {
875 if (active.contains(iterator->first))
876 {
877 ++iterator;
878 continue;
879 }
880 release(iterator->second);
881 iterator = dynamicVertices_.erase(iterator);
882 }
883 }
884
885 void MeshGPUCache::prune(std::span<const data::DataId> activeIds) noexcept
886 {
887 for (auto iterator = resources_.begin(); iterator != resources_.end();)
888 {
889 if (std::find(activeIds.begin(), activeIds.end(), iterator->second.dataId) != activeIds.end())
890 {
891 ++iterator;
892 continue;
893 }
894 if (retainsStaticAsset(iterator->first))
895 {
896 eraseDynamicForAsset(iterator->first);
897 ++iterator;
898 continue;
899 }
900 eraseDynamicForAsset(iterator->first);
901 // ReleaseGPUBuffer/Texture defer physical destruction until safe;
902 // dropping the CPU source reference needs no GPU idle wait.
903 release(iterator->second);
904 iterator = resources_.erase(iterator);
905 }
906 }
907
909 {
910 const MeshGPUResource* latest = nullptr;
911 for (const auto& [identity, resource] : resources_)
912 {
913 (void)identity;
914 if (resource.dataId == id && (!latest ||
915 resource.lastResolvedOrder > latest->lastResolvedOrder))
916 latest = &resource;
917 }
918 return latest;
919 }
920
922 { return resources_.size(); }
923
925 {
926 std::erase_if(retainedStaticAssets_, [id](const auto& asset) { return asset->dataId == id; });
927 eraseDynamicForDataId(id);
928 for (auto iterator = resources_.begin(); iterator != resources_.end();)
929 {
930 if (iterator->second.dataId != id)
931 {
932 ++iterator;
933 continue;
934 }
935 release(iterator->second);
936 iterator = resources_.erase(iterator);
937 }
938 }
939
940 void MeshGPUCache::eraseDynamicForAsset(const data::MeshRuntimeAsset* asset) noexcept
941 {
942 for (auto iterator = dynamicVertices_.begin(); iterator != dynamicVertices_.end();)
943 {
944 if (iterator->second.sourceAsset.get() != asset)
945 {
946 ++iterator;
947 continue;
948 }
949 release(iterator->second);
950 iterator = dynamicVertices_.erase(iterator);
951 }
952 }
953
954 void MeshGPUCache::eraseOtherAssets(data::DataId id,
955 const data::MeshRuntimeAsset* retained) noexcept
956 {
957 std::erase_if(retainedStaticAssets_, [id, retained](const auto& asset)
958 { return asset->dataId == id && asset.get() != retained; });
959 for (auto iterator = resources_.begin(); iterator != resources_.end();)
960 {
961 if (iterator->second.dataId != id || iterator->first == retained)
962 {
963 ++iterator;
964 continue;
965 }
966 eraseDynamicForAsset(iterator->first);
967 release(iterator->second);
968 iterator = resources_.erase(iterator);
969 }
970 }
971
973 std::vector<std::shared_ptr<const data::MeshRuntimeAsset>> assets) noexcept
974 {
975 if (assets.size() > MaximumRetainedStaticAssets ||
976 std::any_of(assets.begin(), assets.end(), [](const auto& asset)
977 { return !asset || !asset->renderData; }))
978 {
979 retainedStaticAssets_.clear();
980 return false;
981 }
982 retainedStaticAssets_ = std::move(assets);
983 return true;
984 }
985
986 bool MeshGPUCache::retainsStaticAsset(const data::MeshRuntimeAsset* asset) const noexcept
987 {
988 return std::any_of(retainedStaticAssets_.begin(), retainedStaticAssets_.end(),
989 [asset](const auto& retained) { return retained.get() == asset; });
990 }
991
993 std::span<const data::MeshRuntimeAsset* const> activeAssets) noexcept
994 {
995 for (auto iterator = resources_.begin(); iterator != resources_.end();)
996 {
997 if (std::find(activeAssets.begin(), activeAssets.end(), iterator->first) != activeAssets.end())
998 {
999 ++iterator;
1000 continue;
1001 }
1002 if (retainsStaticAsset(iterator->first))
1003 {
1004 eraseDynamicForAsset(iterator->first);
1005 ++iterator;
1006 continue;
1007 }
1008 eraseDynamicForAsset(iterator->first);
1009 release(iterator->second);
1010 iterator = resources_.erase(iterator);
1011 }
1012 }
1013
1015 {
1016 retainedStaticAssets_.clear();
1017 for (auto& [key, resource] : dynamicVertices_)
1018 {
1019 (void)key;
1020 release(resource);
1021 }
1022 dynamicVertices_.clear();
1023 for (auto& [id, resource] : resources_)
1024 {
1025 (void)id;
1026 release(resource);
1027 }
1028 resources_.clear();
1029 }
1030}
std::size_t count
decision::EconomicActionPlan plan
LegacyBitmapRGBA8 image
bool uploaded
Definition Engine.cpp:170
data::DataId id
std::uint32_t transferOffset
std::shared_ptr< const data::HmdTextureImage > source
SDL_GPUTexture * texture
std::uint32_t transferBytes
std::uint32_t rowPixels
std::uint64_t key
std::uint32_t height
std::uint32_t width
unsigned index
std::string error
std::uint16_t value
Definition Timers.cpp:16
bool retainStaticAssets(std::vector< std::shared_ptr< const data::MeshRuntimeAsset > > assets) noexcept
MeshGPUCache(SDL_GPUDevice *device) noexcept
std::size_t size() const noexcept
const MeshGPUDynamicVertexResource * findDynamic(std::uint64_t key) const noexcept
void erase(data::DataId id) noexcept
const MeshGPUResource * find(data::DataId id) const noexcept
std::expected< const MeshGPUDynamicVertexResource *, MeshGPUError > resolveDynamicVertices(std::uint64_t key, std::shared_ptr< const data::MeshRuntimeAsset > asset, std::shared_ptr< const data::MeshRenderData > renderData)
void prune(std::span< const data::DataId > activeIds) noexcept
void pruneAssets(std::span< const data::MeshRuntimeAsset *const > activeAssets) noexcept
void pruneDynamicVertices(std::span< const std::uint64_t > activeKeys) noexcept
std::expected< const MeshGPUResource *, MeshGPUError > resolveForScene(std::shared_ptr< const data::MeshRuntimeAsset > asset)
std::expected< const MeshGPUResource *, MeshGPUError > resolve(std::shared_ptr< const data::MeshRuntimeAsset > asset)
std::size_t dynamicSize() const noexcept
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr std::array< std::uint8_t, 4 > legacyShadowPixel(std::uint8_t red, std::uint8_t green, std::uint8_t blue) noexcept
constexpr bool isLegacyShadowMesh(DataId id) noexcept
std::expected< MeshGPUUploadPlan, MeshGPUError > makeMeshGPUUploadPlan(const data::MeshRenderData &renderData)
std::size_t size()
std::vector< MeshRenderBatch > batches
std::vector< std::uint32_t > indices
std::vector< MeshVertex > vertices
std::shared_ptr< const data::MeshRenderData > sourceRenderData
std::shared_ptr< const data::MeshRuntimeAsset > sourceAsset