12#include <unordered_map>
13#include <unordered_set>
28 if (detail !=
nullptr) result.detail = detail;
33 std::expected<std::uint32_t, MeshGPUError>
byteSize(
38 "GPU geometry buffers must be non-empty"));
41 "GPU buffer byte size exceeds SDL Uint32 range"));
45 std::uint64_t
alignUp(std::uint64_t
value, std::uint64_t alignment)
noexcept
47 return (
value + alignment - 1U) / alignment * alignment;
53 std::shared_ptr<const data::HmdTextureImage>
source;
62 std::expected<std::vector<TextureUpload>, MeshGPUError>
textureUploads(
63 const data::MeshRenderData& renderData, std::uint32_t
geometryBytes,
66 std::vector<TextureUpload> result;
67 std::unordered_map<std::uint64_t, std::size_t>
keys;
70 for (
const auto&
batch : renderData.batches)
72 if (!
batch.texture)
continue;
76 "textured mesh batch has no immutable HMD RGBA source image"));
83 "HMD texture region dimensions do not match its decoded image"));
89 "decoded HMD texture does not contain width*height RGBA8 bytes"));
97 "one mesh texture key refers to conflicting immutable images"));
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() ||
113 "aligned HMD texture upload exceeds SDL Uint32 transfer range"));
115 TextureUpload upload;
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));
128 if (
cursor > std::numeric_limits<std::uint32_t>::max())
130 "combined mesh and texture transfer exceeds SDL Uint32 range"));
152 return filter == Filter::Nearest ||
filter == Filter::NearestMipmapNearest ||
160 "modern texture has no immutable RGBA source image"));
167 "modern texture contains an invalid sampler or color-space enum"));
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))
174 "modern RGBA image exceeds Uint32 transfer range"));
175 const std::uint64_t
rawBytes =
static_cast<std::uint64_t
>(
image.width) *
image.height * 4U;
178 "modern texture requires valid width*height RGBA8 and TEXCOORD_0"));
182 "modern magnification filter must be nearest or linear"));
185 if (
image.height > std::numeric_limits<std::uint32_t>::max() /
rowBytes)
187 "aligned modern texture upload exceeds Uint32 transfer range"));
189 if (
transferBytes > std::numeric_limits<std::uint32_t>::max())
191 "modern texture upload exceeds Uint32 transfer range"));
192 MeshGPUModernTextureResource result;
207 "device does not support modern texture color space/mipmap render usage"));
226 binding.sampler.minFilter == Filter::LinearMipmapLinear
232 samplerInfo.max_lod =
static_cast<float>(result.mipLevels - 1U);
241 if (!result.sampler &&
samplerInfo.enable_anisotropy)
276 image.rgba.data() +
static_cast<std::size_t
>(
row) *
image.width * 4U,
277 static_cast<std::size_t
>(
image.width) * 4U);
321 for (
const auto&
batch : renderData.batches)
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());
331 "modern texture ownership storage exceeds vector range"));
338 catch (
const std::bad_alloc&)
341 "modern texture ownership allocation failed"));
343 catch (
const std::length_error&)
346 "modern texture ownership allocation exceeds vector range"));
348 for (
const auto&
batch : renderData.batches)
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)
357 if (!*map ||
resource.modernTexture(**map))
continue;
368 std::span<const MeshGPUVertex> vertices, std::uint32_t vertexBytes,
377 return std::unexpected(
error(
386 std::memcpy(
mapped, vertices.data(), vertexBytes);
390 if (command ==
nullptr)
393 return std::unexpected(
error(
417 std::expected<MeshGPUUploadPlan, MeshGPUError>
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)
426 "combined GPU geometry transfer exceeds SDL Uint32 range"));
431 "mesh render index references a missing vertex"));
437 "mesh render batch exceeds the index array"));
442 plan.indexBytes = *indexBytes;
443 plan.transferBytes = *vertexBytes + *indexBytes;
445 for (
const auto& vertex : renderData.
vertices)
448 for (std::size_t
i = 0;
i < 3; ++
i)
451 packed.normal[
i] = vertex.normal[
i];
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];
467 if (device_ !=
nullptr &&
resource.vertexBuffer !=
nullptr)
469 if (device_ !=
nullptr &&
resource.indexBuffer !=
nullptr)
471 if (device_ !=
nullptr)
475 if (
texture.texture !=
nullptr)
492 void MeshGPUCache::release(MeshGPUDynamicVertexResource&
resource)
noexcept
494 if (device_ !=
nullptr &&
resource.vertexBuffer !=
nullptr)
501 void MeshGPUCache::eraseDynamicForDataId(
data::DataId id)
noexcept
503 for (
auto iterator = dynamicVertices_.begin();
504 iterator != dynamicVertices_.end();)
516 std::expected<const MeshGPUResource*, MeshGPUError>
519 return resolveImpl(std::move(asset),
false);
522 std::expected<const MeshGPUResource*, MeshGPUError>
525 return resolveImpl(std::move(asset),
true);
528 std::expected<const MeshGPUResource*, MeshGPUError>
529 MeshGPUCache::resolveImpl(std::shared_ptr<const data::MeshRuntimeAsset> asset,
bool coexist)
531 if (device_ ==
nullptr)
533 "GPU mesh cache has no SDL_GPUDevice"));
534 if (!asset || !asset->renderData)
536 "GPU mesh upload requires immutable CPU render data"));
538 if (
const auto found = resources_.find(asset.get());
found != resources_.end())
540 if (!
coexist) eraseOtherAssets(asset->dataId, asset.get());
541 found->second.lastResolvedOrder = ++resolvedOrder_;
542 return &
found->second;
546 if (!
plan)
return std::unexpected(
plan.error());
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;
564 if (replacement.vertexBuffer ==
nullptr)
571 if (replacement.indexBuffer ==
nullptr)
589 if (upload.texture ==
nullptr)
595 pending.texture =
nullptr;
626 std::memcpy(
static_cast<std::byte*
>(
mapped) +
plan->vertexBytes,
627 plan->indices.data(),
plan->indexBytes);
631 const auto sourceRowBytes =
static_cast<std::size_t
>(upload.width) * 4U;
634 for (std::uint32_t
row = 0;
row < upload.height; ++
row)
636 const auto*
source = upload.source->rgba.data() +
661 if (command ==
nullptr)
682 replacement.vertexBuffer, 0,
plan->vertexBytes};
686 replacement.indexBuffer, 0,
plan->indexBytes};
693 source.offset = upload.transferOffset;
694 source.pixels_per_row = upload.rowPixels;
695 source.rows_per_layer = upload.height;
719 replacement.textures.emplace(upload.key, MeshGPUTextureResource{
720 upload.key, upload.texture, upload.width, upload.height, upload.source});
721 upload.texture =
nullptr;
734 static_assert(
noexcept(std::declval<MeshGPUResource&>() =
735 std::declval<MeshGPUResource&&>()));
738 const auto* identity = replacement.source.get();
742 eraseDynamicForAsset(identity);
745 inserted->second = std::move(replacement);
746 inserted->second.lastResolvedOrder = ++resolvedOrder_;
750 catch (
const std::bad_alloc&)
754 "GPU mesh cache ownership allocation failed"));
756 catch (
const std::length_error&)
760 "GPU mesh cache ownership exceeds container range"));
764 std::expected<const MeshGPUDynamicVertexResource*, MeshGPUError>
766 std::shared_ptr<const data::MeshRuntimeAsset> asset,
767 std::shared_ptr<const data::MeshRenderData> renderData)
769 if (device_ ==
nullptr)
771 "dynamic GPU mesh cache has no SDL_GPUDevice"));
772 if (!asset || !asset->renderData || !renderData)
774 "dynamic vertex upload requires static asset and evaluated render data"));
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)
790 const auto&
base = asset->renderData->batches[
index];
792 ? std::optional<std::uint64_t>{
animated.texture->key} : std::nullopt;
794 ? std::optional<std::uint64_t>{
base.texture->key} : std::nullopt;
798 return std::unexpected(
error(
800 "animated MESHX batch topology or texture identity changed"));
803 if (
auto found = dynamicVertices_.find(
key);
found != dynamicVertices_.end())
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())
812 found->second.vertexBuffer,
plan->vertices,
813 plan->vertexBytes,
true);
815 return std::unexpected(
uploaded.error());
816 found->second.dataId = asset->dataId;
817 found->second.sourceRenderData = std::move(renderData);
818 return &
found->second;
824 replacement.
dataId = asset->dataId;
825 replacement.
vertexCount =
static_cast<std::uint32_t
>(
plan->vertices.size());
833 return std::unexpected(
error(
836 plan->vertices,
plan->vertexBytes,
false);
839 release(replacement);
840 return std::unexpected(
uploaded.error());
843 "Monopoly animated mesh vertices");
845 if (
auto found = dynamicVertices_.find(
key);
found != dynamicVertices_.end())
847 release(
found->second);
848 found->second = std::move(replacement);
849 return &
found->second;
852 std::move(replacement));
858 std::uint64_t
key)
const noexcept
860 const auto found = dynamicVertices_.find(
key);
865 {
return dynamicVertices_.size(); }
868 std::span<const std::uint64_t>
activeKeys)
noexcept
870 const std::unordered_set<std::uint64_t> active(
872 for (
auto iterator = dynamicVertices_.begin();
873 iterator != dynamicVertices_.end();)
875 if (active.contains(
iterator->first))
894 if (retainsStaticAsset(
iterator->first))
896 eraseDynamicForAsset(
iterator->first);
900 eraseDynamicForAsset(
iterator->first);
911 for (
const auto& [identity,
resource] : resources_)
922 {
return resources_.size(); }
926 std::erase_if(retainedStaticAssets_, [
id](
const auto& asset) {
return asset->dataId ==
id; });
927 eraseDynamicForDataId(
id);
942 for (
auto iterator = dynamicVertices_.begin();
iterator != dynamicVertices_.end();)
944 if (
iterator->second.sourceAsset.get() != asset)
955 const data::MeshRuntimeAsset*
retained)
noexcept
957 std::erase_if(retainedStaticAssets_, [
id,
retained](
const auto& asset)
958 {
return asset->dataId ==
id && asset.get() !=
retained; });
966 eraseDynamicForAsset(
iterator->first);
973 std::vector<std::shared_ptr<const data::MeshRuntimeAsset>>
assets)
noexcept
975 if (
assets.size() > MaximumRetainedStaticAssets ||
976 std::any_of(
assets.begin(),
assets.end(), [](
const auto& asset)
977 { return !asset || !asset->renderData; }))
979 retainedStaticAssets_.clear();
982 retainedStaticAssets_ = std::move(
assets);
988 return std::any_of(retainedStaticAssets_.begin(), retainedStaticAssets_.end(),
989 [asset](
const auto&
retained) { return retained.get() == asset; });
993 std::span<const data::MeshRuntimeAsset* const>
activeAssets)
noexcept
1002 if (retainsStaticAsset(
iterator->first))
1004 eraseDynamicForAsset(
iterator->first);
1008 eraseDynamicForAsset(
iterator->first);
1016 retainedStaticAssets_.clear();
1022 dynamicVertices_.clear();
1023 for (
auto& [
id,
resource] : resources_)
decision::EconomicActionPlan plan
std::uint32_t transferOffset
std::shared_ptr< const data::HmdTextureImage > source
std::uint32_t transferBytes
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
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)
@ CommandBufferCreationFailed
@ DynamicTopologyMismatch
@ VertexBufferCreationFailed
@ TransferBufferCreationFailed
@ UnsupportedTextureFormat
@ IndexBufferCreationFailed
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
std::uint32_t vertexCount
SDL_GPUBuffer * vertexBuffer
std::uint32_t vertexBytes