Skip to content

Commit 4d5097b

Browse files
author
devsh
committed
finish the Acceleration Structure CAssetConverter::reserve
1 parent 99e473d commit 4d5097b

File tree

2 files changed

+84
-86
lines changed

2 files changed

+84
-86
lines changed

include/nbl/video/utilities/CAssetConverter.h

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,32 +1100,30 @@ class CAssetConverter : public core::IReferenceCounted
11001100
inline build_f getBuildFlags() const {return static_cast<build_f>(buildFlags);}
11011101

11021102
core::smart_refctd_ptr<const CPUAccelerationStructure> canonical = nullptr;
1103-
uint64_t scratchSize : 45;
1104-
uint64_t compact : 1;
1103+
uint64_t scratchSize : 47 = 0;
11051104
uint64_t buildFlags : 16 = 0;
1105+
uint64_t compact : 1;
11061106
// scratch + input size also accounting for worst case padding due to alignment
11071107
uint64_t buildSize;
11081108
};
1109-
template<typename CPUAccelerationStructure>
1110-
using SConvReqAccelerationStructureMap = core::unordered_map<typename asset_traits<CPUAccelerationStructure>::video_t*,SConvReqAccelerationStructure<CPUAccelerationStructure>>;
1111-
SConvReqAccelerationStructureMap<asset::ICPUBottomLevelAccelerationStructure> m_blasConversions[2];
1112-
SConvReqAccelerationStructureMap<asset::ICPUTopLevelAccelerationStructure> m_tlasConversions[2];
1109+
using SConvReqBLASMap = core::unordered_map<IGPUBottomLevelAccelerationStructure*,SConvReqAccelerationStructure<asset::ICPUBottomLevelAccelerationStructure>>;
1110+
SConvReqBLASMap m_blasConversions[2];
1111+
struct SConvReqTLAS : SConvReqAccelerationStructure<asset::ICPUTopLevelAccelerationStructure>
1112+
{
1113+
// This tracks non-root BLASes which are needed for a subsequent TLAS build.
1114+
// Because the copy group ID of the BLAS can only depend on the copy group and pointer of the TLAS and BLAS,
1115+
// we can be sure that all instances of the same BLAS within a TLAS will have the same copy group ID and use a map instead of a vector for storage
1116+
// Note that even things which are NOT in the staging cache are tracked here to make sure they don't finish their lifetimes prematurely.
1117+
using cpu_to_gpu_blas_map_t = core::unordered_map<const asset::ICPUBottomLevelAccelerationStructure*,core::smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure>>;
1118+
cpu_to_gpu_blas_map_t instanceMap;
1119+
};
1120+
using SConvReqTLASMap = core::unordered_map<IGPUTopLevelAccelerationStructure*,SConvReqTLAS>;
1121+
SConvReqTLASMap m_tlasConversions[2];
11131122

11141123
// array index 0 for device builds, 1 for host builds
11151124
uint64_t m_minASBuildScratchSize[2] = {0,0};
11161125
uint64_t m_maxASBuildScratchSize[2] = {0,0};
11171126
uint64_t m_compactedASMaxMemory = 0;
1118-
// This tracks non-root BLASes which are needed for a subsequent TLAS build. Note that even things which are NOT in the staging cache are tracked here to make sure they don't finish their lifetimes early.
1119-
struct BLASUsedInTLASBuild
1120-
{
1121-
// This is the BLAS meant to be used for the instance, note that compaction of a BLAS overwrites the initial values at the end of `reserve`
1122-
core::smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure> gpuBLAS;
1123-
uint64_t buildDuringConvertCall : 1 = false;
1124-
// internal micro-refcount which lets us know when we should remove the entry from the map below
1125-
uint64_t remainingUsages : 63 = 0;
1126-
};
1127-
using cpu_to_gpu_blas_map_t = core::unordered_map<const asset::ICPUBottomLevelAccelerationStructure*,BLASUsedInTLASBuild>;
1128-
cpu_to_gpu_blas_map_t m_blasBuildMap;
11291127
//
11301128
struct SDeferredTLASWrite
11311129
{

src/nbl/video/utilities/CAssetConverter.cpp

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ class AssetVisitor : public CRTP
445445
}
446446

447447
private:
448-
// there is no `impl()` overload taking `ICPUTopLevelAccelerationStructure` same as there is no `ICPUmage`
448+
// there is no `impl()` overload taking `ICPUBottomLevelAccelerationStructure` same as there is no `ICPUmage`
449449
inline bool impl(const instance_t<ICPUTopLevelAccelerationStructure>& instance, const CAssetConverter::patch_t<ICPUTopLevelAccelerationStructure>& userPatch)
450450
{
451451
const auto blasInstances = instance.asset->getInstances();
@@ -1656,6 +1656,9 @@ class GetDependantVisit;
16561656
template<>
16571657
class GetDependantVisit<ICPUTopLevelAccelerationStructure> : public GetDependantVisitBase<ICPUTopLevelAccelerationStructure>
16581658
{
1659+
public:
1660+
CAssetConverter::SReserveResult::SConvReqTLAS::cpu_to_gpu_blas_map_t* instanceMap;
1661+
16591662
protected:
16601663
bool descend_impl(
16611664
const instance_t<AssetType>& user, const CAssetConverter::patch_t<AssetType>& userPatch,
@@ -1666,6 +1669,7 @@ class GetDependantVisit<ICPUTopLevelAccelerationStructure> : public GetDependant
16661669
auto depObj = getDependant<ICPUBottomLevelAccelerationStructure>(dep,soloPatch);
16671670
if (!depObj)
16681671
return false;
1672+
instanceMap->operator[](dep.asset) = std::move(depObj);
16691673
return true;
16701674
}
16711675
};
@@ -3397,9 +3401,7 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
33973401
// now allocate the memory for buffers and images
33983402
deferredAllocator.finalize();
33993403

3400-
// TODO: everything below is slightly wrong due to not having a final top-down dependency checking pass throwing away useless non-root GPU subtrees
3401-
3402-
// find out which buffers need to be uploaded via a staging buffer
3404+
// enqueue successfully created buffers for conversion
34033405
for (auto& entry : bufferConversions.contentHashToCanonical)
34043406
for (auto i=0ull; i<entry.second.copyCount; i++)
34053407
if (auto& gpuBuff=bufferConversions.gpuObjects[i+entry.second.firstCopyIx].value; gpuBuff)
@@ -3414,7 +3416,7 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
34143416
{
34153417
constexpr bool IsTLAS = std::is_same_v<AccelerationStructure,ICPUTopLevelAccelerationStructure>;
34163418
//
3417-
SReserveResult::SConvReqAccelerationStructureMap<AccelerationStructure>* pConversions;
3419+
std::conditional_t<IsTLAS,SReserveResult::SConvReqTLASMap,SReserveResult::SConvReqBLASMap>* pConversions;
34183420
if constexpr (IsTLAS)
34193421
pConversions = retval.m_tlasConversions;
34203422
else
@@ -3437,11 +3439,12 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
34373439
};
34383440
}
34393441
smart_refctd_ptr<typename asset_traits<AccelerationStructure>::video_t> as;
3442+
CAssetConverter::SReserveResult::SConvReqTLAS::cpu_to_gpu_blas_map_t blasInstanceMap;
34403443
if constexpr (IsTLAS)
34413444
{
34423445
// check if the BLASes we want to use for the instances were successfully allocated and created
34433446
AssetVisitor<GetDependantVisit<ICPUTopLevelAccelerationStructure>> visitor = {
3444-
{inputs,dfsCaches},
3447+
{inputs,dfsCaches,&blasInstanceMap},
34453448
{canonical,deferredParams.uniqueCopyGroupID},
34463449
patch
34473450
};
@@ -3469,14 +3472,16 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
34693472
request.compact = patch.compactAfterBuild;
34703473
request.buildFlags = static_cast<uint16_t>(patch.getBuildFlags(canonical).value);
34713474
request.buildSize = deferredParams.buildSize;
3475+
if constexpr (IsTLAS)
3476+
request.instanceMap = std::move(blasInstanceMap);
34723477
}
34733478
};
34743479
createAccelerationStructures.template operator()<ICPUBottomLevelAccelerationStructure>();
34753480
blasConversions.propagateToCaches(std::get<dfs_cache<ICPUBottomLevelAccelerationStructure>>(dfsCaches),std::get<SReserveResult::staging_cache_t<ICPUBottomLevelAccelerationStructure>>(retval.m_stagingCaches));
34763481
createAccelerationStructures.template operator()<ICPUTopLevelAccelerationStructure>();
34773482
tlasConversions.propagateToCaches(std::get<dfs_cache<ICPUTopLevelAccelerationStructure>>(dfsCaches),std::get<SReserveResult::staging_cache_t<ICPUTopLevelAccelerationStructure>>(retval.m_stagingCaches));
34783483
}
3479-
// find out which images need what caps for the transfer and mipmapping
3484+
// enqueue successfully created images with data to upload for conversion
34803485
auto& dfsCacheImages = std::get<dfs_cache<ICPUImage>>(dfsCaches);
34813486
for (auto& entry : imageConversions.contentHashToCanonical)
34823487
for (auto i=0ull; i<entry.second.copyCount; i++)
@@ -3584,26 +3589,6 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
35843589
pruneStaging.template operator()<ICPUBufferView>();
35853590
pruneStaging.template operator()<ICPUImage>();
35863591
pruneStaging.template operator()<ICPUTopLevelAccelerationStructure>();
3587-
// go over future TLAS builds to gather used BLASes
3588-
for (auto i=0; i<2; i++)
3589-
for (const auto& req : retval.m_tlasConversions[i])
3590-
{
3591-
auto* const cpuTLAS = req.second.canonical.get();
3592-
assert(cpuTLAS);
3593-
for (const auto& instance : cpuTLAS->getInstances())
3594-
{
3595-
auto* const cpuBLAS = instance.getBase().blas.get();
3596-
auto foundBLAS = retval.m_blasBuildMap.find(cpuBLAS);
3597-
if (foundBLAS!=retval.m_blasBuildMap.end())
3598-
foundBLAS->second.remainingUsages++;
3599-
else
3600-
{
3601-
smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure> gpuBLAS;
3602-
// TODO: figure out the BLAS that will be used, (this requires UUID)
3603-
retval.m_blasBuildMap.insert(foundBLAS,{cpuBLAS,{std::move(gpuBLAS),1,1}});
3604-
}
3605-
}
3606-
}
36073592
pruneStaging.template operator()<ICPUBottomLevelAccelerationStructure>();
36083593
pruneStaging.template operator()<ICPUBuffer>();
36093594
}
@@ -3620,7 +3605,7 @@ auto CAssetConverter::reserve(const SInputs& inputs) -> SReserveResult
36203605
// index 0 is device build, 1 is host build
36213606
size_t scratchSizeFullParallelBuild[2] = {0,0};
36223607
//
3623-
const SReserveResult::SConvReqAccelerationStructureMap<AccelerationStructure>* pConversions;
3608+
const std::conditional_t<IsTLAS,SReserveResult::SConvReqTLASMap,SReserveResult::SConvReqBLASMap>* pConversions;
36243609
if constexpr (IsTLAS)
36253610
pConversions = retval.m_tlasConversions;
36263611
else
@@ -3755,7 +3740,25 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
37553740
}
37563741
};
37573742

3758-
// compacted TLASes need to be substituted in cache and Descriptor Sets
3743+
// want to check if deps successfully exist
3744+
struct SMissingDependent
3745+
{
3746+
// This only checks if whether we had to convert and failed, but the dependent might be in readCache of one or more converters, so if in doubt assume its okay
3747+
inline operator bool() const {return wasInStaging && gotWiped;}
3748+
3749+
bool wasInStaging;
3750+
bool gotWiped;
3751+
};
3752+
auto missingDependent = [&reservations]<Asset AssetType>(const typename asset_traits<AssetType>::video_t* dep)->SMissingDependent
3753+
{
3754+
auto& stagingCache = std::get<SReserveResult::staging_cache_t<AssetType>>(reservations.m_stagingCaches);
3755+
auto found = stagingCache.find(const_cast<typename asset_traits<AssetType>::video_t*>(dep));
3756+
SMissingDependent retval = {.wasInStaging=found!=stagingCache.end()};
3757+
retval.gotWiped = retval.wasInStaging && found->second.value==CHashCache::NoContentHash;
3758+
return retval;
3759+
};
3760+
3761+
// Descriptor Sets need their TLAS descriptors substituted if they've been compacted
37593762
core::unordered_map<const IGPUTopLevelAccelerationStructure*,smart_refctd_ptr<IGPUTopLevelAccelerationStructure>> compactedTLASMap;
37603763
// Anything to do?
37613764
auto reqQueueFlags = reservations.m_queueFlags;
@@ -4672,6 +4675,9 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
46724675
.dstAccessMask = ACCESS_FLAGS::ACCELERATION_STRUCTURE_READ_BIT
46734676
};
46744677

4678+
// compacted BLASes need to be substituted in cache and TLAS Build Inputs
4679+
using compacted_blas_map_t = core::unordered_map<const IGPUBottomLevelAccelerationStructure*,smart_refctd_ptr<IGPUBottomLevelAccelerationStructure>>;
4680+
compacted_blas_map_t compactedBLASMap;
46754681
// Device BLAS builds
46764682
if (blasCount)
46774683
{
@@ -4749,7 +4755,7 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
47494755
computeCmdBuf->cmdbuf->endDebugMarker();
47504756
{
47514757
// the already compacted BLASes need to be written into the TLASes using them, want to swap them out ASAP
4752-
//reservations.m_blasBuildMap[canonical].gpuBLAS = compacted;
4758+
//compactedBLASMap[as] = compacted;
47534759
}
47544760
computeCmdBuf->cmdbuf->beginDebugMarker("Asset Converter Compact BLASes END");
47554761
computeCmdBuf->cmdbuf->endDebugMarker();
@@ -4801,11 +4807,8 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
48014807
using scratch_allocator_t = std::remove_reference_t<decltype(*params.scratchForDeviceASBuild)>;
48024808
using addr_t = typename scratch_allocator_t::size_type;
48034809
const auto& limits = physDev->getLimits();
4804-
core::unordered_set<smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure>> dedupBLASesUsed;
4805-
dedupBLASesUsed.reserve(reservations.m_blasBuildMap.size());
48064810
for (auto& tlasToBuild : tlasesToBuild)
48074811
{
4808-
dedupBLASesUsed.clear();
48094812
auto& canonical = tlasToBuild.second.canonical;
48104813
const auto as = tlasToBuild.first;
48114814
const auto pFoundHash = findInStaging.template operator()<ICPUTopLevelAccelerationStructure>(as);
@@ -4819,19 +4822,30 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
48194822
}
48204823
const auto instances = canonical->getInstances();
48214824
const auto instanceCount = static_cast<uint32_t>(instances.size());
4825+
const auto& instanceMap = tlasToBuild.second.instanceMap;
48224826
size_t instanceDataSize = 0;
48234827
// gather total input size and check dependants exist
4828+
bool dependsOnBLASBuilds = false;
48244829
for (const auto& instance : instances)
48254830
{
4826-
// failed BLAS builds erase themselves from this map, so this checks if some BLAS used but which had to be built failed the build
4827-
const auto found = reservations.m_blasBuildMap.find(instance.getBase().blas.get());
4828-
if (found==reservations.m_blasBuildMap.end() || failedBLASBarrier && found->second.buildDuringConvertCall)
4831+
auto found = instanceMap.find(instance.getBase().blas.get());
4832+
assert(instanceMap.end()!=found);
4833+
const auto depInfo = missingDependent.template operator()<ICPUBottomLevelAccelerationStructure>(found->second.get());
4834+
if (depInfo)
48294835
{
48304836
instanceDataSize = 0;
48314837
break;
48324838
}
4839+
if (depInfo.wasInStaging)
4840+
dependsOnBLASBuilds;
48334841
instanceDataSize += ITopLevelAccelerationStructure::getInstanceSize(instance.getType());
48344842
}
4843+
// problem with building some Dependent BLASes
4844+
if (failedBLASBarrier && dependsOnBLASBuilds)
4845+
{
4846+
markFailureInStaging("building BLASes which current TLAS build wants to instance",canonical,as,pFoundHash);
4847+
continue;
4848+
}
48354849
// problem with finding the dependents (BLASes)
48364850
if (instanceDataSize==0)
48374851
{
@@ -4862,6 +4876,7 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
48624876
params.scratchForDeviceASBuild->multi_deallocate(AllocCount,&offsets[0],&sizes[0],params.compute->getFutureScratchSemaphore());
48634877
}
48644878
// stream the instance/geometry input in
4879+
const size_t trackedBLASesOffset = trackedBLASes.size();
48654880
{
48664881
bool success = true;
48674882
{
@@ -4881,27 +4896,30 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
48814896
const auto newWritten = bytesWritten+size;
48824897
if (newWritten>=blockSize)
48834898
return bytesWritten;
4884-
auto found = blasBuildMap->find(instance.getBase().blas.get());
4885-
assert(found!=blasBuildMap->end());
4886-
const auto& blas = found->second.gpuBLAS;
4887-
dst = IGPUTopLevelAccelerationStructure::writeInstance(dst,instance,blas.get()->getReferenceForDeviceOperations());
4888-
dedupBLASesUsed->emplace(blas);
4889-
if (--found->second.remainingUsages == 0)
4890-
blasBuildMap->erase(found);
4899+
auto found = instanceMap->find(instance.getBase().blas.get());
4900+
auto blas = found->second.get();
4901+
if (auto found=compactedBLASMap->find(blas); found!=compactedBLASMap->end())
4902+
blas = found->second.get();
4903+
trackedBLASes->emplace_back(blas);
4904+
dst = IGPUTopLevelAccelerationStructure::writeInstance(dst,instance,blas->getReferenceForDeviceOperations());
48914905
bytesWritten = newWritten;
48924906
}
48934907
}
48944908

4895-
SReserveResult::cpu_to_gpu_blas_map_t* blasBuildMap;
4896-
core::unordered_set<smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure>>* dedupBLASesUsed;
4909+
const compacted_blas_map_t* compactedBLASMap;
4910+
core::vector<smart_refctd_ptr<const IGPUBottomLevelAccelerationStructure>>* trackedBLASes;
4911+
SReserveResult::SConvReqTLAS::cpu_to_gpu_blas_map_t* instanceMap;
48974912
std::span<const ICPUTopLevelAccelerationStructure::PolymorphicInstance> instances;
48984913
uint32_t instanceIndex = 0;
48994914
};
49004915
FillInstances fillInstances;
4901-
fillInstances.blasBuildMap = &reservations.m_blasBuildMap;
4902-
fillInstances.dedupBLASesUsed = &dedupBLASesUsed;
4916+
fillInstances.compactedBLASMap = &compactedBLASMap;
4917+
fillInstances.trackedBLASes = &trackedBLASes;
4918+
fillInstances.instanceMap = &tlasToBuild.second.instanceMap;
49034919
fillInstances.instances = instances;
49044920
success = streamDataToScratch(offsets[1],sizes[1],fillInstances);
4921+
// provoke refcounting bugs right away
4922+
tlasToBuild.second.instanceMap.clear();
49054923
}
49064924
if (success && as->usesMotion())
49074925
{
@@ -4935,6 +4953,7 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
49354953
xferCmdBuf = params.transfer->getCommandBufferForRecording();
49364954
if (!success)
49374955
{
4956+
trackedBLASes.resize(trackedBLASesOffset);
49384957
markFailureInStaging("Uploading Instance Data for TLAS build failed",canonical,as,pFoundHash);
49394958
continue;
49404959
}
@@ -4950,14 +4969,8 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
49504969
// note we don't build directly from staging, because only very small inputs could come from there and they'd impede the transfer efficiency of the larger ones
49514970
buildInfo.instanceData = {.offset=offsets[as->usesMotion() ? 2:1],.buffer=smart_refctd_ptr<IGPUBuffer>(scratchBuffer)};
49524971
// be based cause vectors can grow
4953-
{
4954-
const auto offset = trackedBLASes.size();
4955-
using p_p_BLAS_t = const IGPUBottomLevelAccelerationStructure**;
4956-
buildInfo.trackedBLASes = {reinterpret_cast<const p_p_BLAS_t&>(offset),dedupBLASesUsed.size()};
4957-
for (auto& blas : dedupBLASesUsed)
4958-
trackedBLASes.emplace_back(std::move(blas));
4959-
4960-
}
4972+
using p_p_BLAS_t = const IGPUBottomLevelAccelerationStructure**;
4973+
buildInfo.trackedBLASes = {reinterpret_cast<const p_p_BLAS_t&>(trackedBLASesOffset),trackedBLASes.size()-trackedBLASesOffset};
49614974
// no special extra byte offset into the instance buffer
49624975
rangeInfos.emplace_back(instanceCount,0u);
49634976
//
@@ -4984,7 +4997,6 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
49844997
else
49854998
compactedOwnershipReleaseIndices.push_back(~0u);
49864999
}
4987-
reservations.m_blasBuildMap.clear();
49885000
// finish the last batch
49895001
recordBuildCommands();
49905002
if (!flushRanges.empty())
@@ -5154,18 +5166,6 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
51545166
// finish host tasks if not done yet
51555167
hostUploadBuffers([]()->bool{return true;});
51565168

5157-
// Descriptor Sets need their TLAS descriptors substituted if they've been compacted
5158-
// want to check if deps successfully exist
5159-
auto missingDependent = [&reservations]<Asset AssetType>(const typename asset_traits<AssetType>::video_t* dep)->bool
5160-
{
5161-
auto& stagingCache = std::get<SReserveResult::staging_cache_t<AssetType>>(reservations.m_stagingCaches);
5162-
auto found = stagingCache.find(const_cast<typename asset_traits<AssetType>::video_t*>(dep));
5163-
// this only checks if whether we had to convert and failed
5164-
if (found!=stagingCache.end() && found->second.value==CHashCache::NoContentHash)
5165-
return true;
5166-
// but the dependent might be in readCache of one or more converters, so if in doubt assume its okay
5167-
return false;
5168-
};
51695169
// insert items into cache if overflows handled fine and commandbuffers ready to be recorded
51705170
auto mergeCache = [&]<Asset AssetType>()->void
51715171
{
@@ -5277,7 +5277,7 @@ ISemaphore::future_t<IQueue::RESULT> CAssetConverter::convert_impl(SReserveResul
52775277
mergeCache.template operator()<ICPUComputePipeline>();
52785278
mergeCache.template operator()<ICPURenderpass>();
52795279
mergeCache.template operator()<ICPUGraphicsPipeline>();
5280-
// write the TLASes into Descriptor Set finally
5280+
// overwrite the compacted TLASes in Descriptor Sets
52815281
if (auto& tlasRewriteSet=reservations.m_potentialTLASRewrites; !tlasRewriteSet.empty())
52825282
{
52835283
core::vector<IGPUDescriptorSet::SWriteDescriptorSet> writes;

0 commit comments

Comments
 (0)