mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Perf] (DirectVulkan): drop per-draw weak-ptr locks, re-resolves and rebuilt masks from the sampled-texture and vertex paths
This commit is contained in:
@@ -209,6 +209,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||||
entry.layoutHash = XXH64_digest(m_hashState);
|
entry.layoutHash = XXH64_digest(m_hashState);
|
||||||
|
entry.attributeLocationMask = 0;
|
||||||
|
for (const auto& attribute : entry.attributes) {
|
||||||
|
if (attribute.location < 32u) {
|
||||||
|
entry.attributeLocationMask |= (1u << attribute.location);
|
||||||
|
}
|
||||||
|
}
|
||||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||||
Uint32 unsupportedAttribMask = 0;
|
Uint32 unsupportedAttribMask = 0;
|
||||||
|
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||||
|
// three times per draw, so it is baked once at build time.
|
||||||
|
Uint32 attributeLocationMask = 0;
|
||||||
VkPipelineVertexInputStateCreateInfo state{
|
VkPipelineVertexInputStateCreateInfo state{
|
||||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -966,6 +966,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
.target = TrackedAttachmentTarget::Texture,
|
.target = TrackedAttachmentTarget::Texture,
|
||||||
.texture = att.GetTexture(),
|
.texture = att.GetTexture(),
|
||||||
|
.textureRaw = att.GetTexture().get(),
|
||||||
.textureMipLevel = attachmentMipLevel,
|
.textureMipLevel = attachmentMipLevel,
|
||||||
.finalLayout = desc.finalLayout,
|
.finalLayout = desc.finalLayout,
|
||||||
});
|
});
|
||||||
@@ -1142,6 +1143,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
.target = TrackedAttachmentTarget::Texture,
|
.target = TrackedAttachmentTarget::Texture,
|
||||||
.texture = selectedDepthStencilAttachment->GetTexture(),
|
.texture = selectedDepthStencilAttachment->GetTexture(),
|
||||||
|
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
||||||
.textureMipLevel = attachmentMipLevel,
|
.textureMipLevel = attachmentMipLevel,
|
||||||
.finalLayout = depthAttachmentDescription.finalLayout,
|
.finalLayout = depthAttachmentDescription.finalLayout,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct TrackedAttachmentLayoutInfo {
|
struct TrackedAttachmentLayoutInfo {
|
||||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||||
|
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||||
|
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||||
|
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||||
|
// May dangle once the texture dies - compare only, never dereference.
|
||||||
|
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
Uint32 textureMipLevel = 0;
|
Uint32 textureMipLevel = 0;
|
||||||
Uint32 swapchainImageIndex = 0;
|
Uint32 swapchainImageIndex = 0;
|
||||||
|
|||||||
@@ -835,16 +835,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_assert(kMaxVertexAttribs <= ProgramFactory::VkProgramObject::kMaxVertexInputLocations,
|
static_assert(kMaxVertexAttribs <= ProgramFactory::VkProgramObject::kMaxVertexInputLocations,
|
||||||
"vertexInputTypes is indexed by vertex attribute location");
|
"vertexInputTypes is indexed by vertex attribute location");
|
||||||
|
|
||||||
static Uint32 BuildVertexInputAttributeMask(const Vector<VkVertexInputAttributeDescription>& attributes) {
|
|
||||||
Uint32 attributeMask = 0;
|
|
||||||
for (const auto& attribute : attributes) {
|
|
||||||
if (attribute.location < kMaxVertexAttribs) {
|
|
||||||
attributeMask |= (1u << attribute.location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attributeMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) {
|
static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) {
|
||||||
switch (glType) {
|
switch (glType) {
|
||||||
case GL_FLOAT:
|
case GL_FLOAT:
|
||||||
@@ -995,8 +985,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (trackedAttachment.target != TrackedAttachmentTarget::Texture) {
|
if (trackedAttachment.target != TrackedAttachmentTarget::Texture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto trackedTexture = trackedAttachment.texture.lock();
|
// Raw identity compare (see textureRaw): the caller's texture is
|
||||||
if (trackedTexture && trackedTexture.get() == &texture) {
|
// live, so a dangling tracked pointer can never equal its address
|
||||||
|
// unless the allocator reused it - and that false positive merely
|
||||||
|
// ends the render pass early, never misses a genuine use.
|
||||||
|
if (trackedAttachment.textureRaw == &texture) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2931,7 +2924,7 @@ void main() {
|
|||||||
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
|
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
|
||||||
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
||||||
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
||||||
const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vertexInputState.attributes);
|
const Uint32 vertexInputAttribMask = vertexInputState.attributeLocationMask;
|
||||||
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
||||||
|
|
||||||
const auto bindingCount = vertexInputState.bindings.size() + static_cast<SizeT>(std::popcount(missingAttribMask));
|
const auto bindingCount = vertexInputState.bindings.size() + static_cast<SizeT>(std::popcount(missingAttribMask));
|
||||||
@@ -3923,7 +3916,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes);
|
const Uint32 vertexInputAttribMask = vis.attributeLocationMask;
|
||||||
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
||||||
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
||||||
auto& patchedAttributes = m_patchedAttributesScratch;
|
auto& patchedAttributes = m_patchedAttributesScratch;
|
||||||
@@ -4468,16 +4461,52 @@ void main() {
|
|||||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||||
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
|
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
|
||||||
const auto* programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
|
||||||
// Sampling a colour render target through the driver's implicit-LOD path faults the GPU on
|
// Sampling a colour render target through the driver's implicit-LOD path faults the GPU on
|
||||||
// Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing
|
// Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing
|
||||||
// so cannot change a texel, i.e. when every sampler this program reads is pinned to a
|
// so cannot change a texel, i.e. when every sampler this program reads is pinned to a
|
||||||
// single mip level.
|
// single mip level. The probe walks every sampler binding, so its verdict is memoized
|
||||||
if (UniformManager::ProgramSamplesOnlySingleLevelTextures(program, *programObjPtr)) {
|
// under the sampled-set memo's key plus the sampled textures' params-version sum (level
|
||||||
transformFlags |= ProgramFactory::CompileOptionBit::ExplicitLod0Sampling;
|
// range and filter changes live there); the previous draw's texture list is valid for the
|
||||||
programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
// sum exactly when that key matches (same program, same binds).
|
||||||
|
{
|
||||||
|
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
|
||||||
|
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
|
||||||
|
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||||
|
Bool lodMemoHit = false;
|
||||||
|
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
|
||||||
|
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
|
||||||
|
m_lastLodProgramVersion == lodProgramVersion &&
|
||||||
|
m_lastLodBindGeneration == lodBindGeneration && m_lastLodBaseFlags == transformFlags &&
|
||||||
|
m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId &&
|
||||||
|
m_lastSampledSetProgramVersion == lodProgramVersion &&
|
||||||
|
m_lastSampledSetBindGeneration == lodBindGeneration) {
|
||||||
|
Uint64 paramsSum = 0;
|
||||||
|
for (const auto* sampledTexture : m_sampledTexturesScratch) {
|
||||||
|
if (sampledTexture != nullptr) {
|
||||||
|
paramsSum += sampledTexture->GetTextureParamsVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paramsSum == m_lastLodParamsSum) {
|
||||||
|
transformFlags = m_lastLodResultFlags;
|
||||||
|
lodMemoHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lodMemoHit) {
|
||||||
|
const ProgramFactory::CompileOptionFlags baseFlags = transformFlags;
|
||||||
|
const auto& baseProgramObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||||
|
if (UniformManager::ProgramSamplesOnlySingleLevelTextures(program, baseProgramObj)) {
|
||||||
|
transformFlags |= ProgramFactory::CompileOptionBit::ExplicitLod0Sampling;
|
||||||
|
}
|
||||||
|
m_lastLodDecisionValid = true;
|
||||||
|
m_lastLodProgramLifetimeId = lodProgramLifetimeId;
|
||||||
|
m_lastLodProgramVersion = lodProgramVersion;
|
||||||
|
m_lastLodBindGeneration = lodBindGeneration;
|
||||||
|
m_lastLodBaseFlags = baseFlags;
|
||||||
|
m_lastLodResultFlags = transformFlags;
|
||||||
|
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const auto& programObj = *programObjPtr;
|
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||||
|
|
||||||
// Begin command recording if not yet
|
// Begin command recording if not yet
|
||||||
if (!frame.isCommandRecording) {
|
if (!frame.isCommandRecording) {
|
||||||
@@ -4522,6 +4551,18 @@ void main() {
|
|||||||
m_lastSampledSetTransformFlags = transformFlags;
|
m_lastSampledSetTransformFlags = transformFlags;
|
||||||
m_lastSampledSetBindGeneration = bindGeneration;
|
m_lastSampledSetBindGeneration = bindGeneration;
|
||||||
}
|
}
|
||||||
|
// Complete a freshly-made LOD decision (see above): its params sum
|
||||||
|
// can only be taken once the sampled set is known. A genuine
|
||||||
|
// all-zero sum merely re-probes next draw.
|
||||||
|
if (m_lastLodDecisionValid && m_lastLodParamsSum == 0) {
|
||||||
|
Uint64 paramsSum = 0;
|
||||||
|
for (const auto* sampledTexture : sampledTextures) {
|
||||||
|
if (sampledTexture != nullptr) {
|
||||||
|
paramsSum += sampledTexture->GetTextureParamsVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_lastLodParamsSum = paramsSum;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s",
|
MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s",
|
||||||
program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(),
|
program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(),
|
||||||
@@ -4545,7 +4586,10 @@ void main() {
|
|||||||
activeRenderPass = nullptr;
|
activeRenderPass = nullptr;
|
||||||
}
|
}
|
||||||
Bool needSampledTextureTransitions = false;
|
Bool needSampledTextureTransitions = false;
|
||||||
for (auto* sampledTexture : sampledTextures) {
|
auto& sampledResources = m_sampledResourcesScratch;
|
||||||
|
sampledResources.assign(sampledTextures.size(), nullptr);
|
||||||
|
for (SizeT sampledIndex = 0; sampledIndex < sampledTextures.size(); ++sampledIndex) {
|
||||||
|
auto* sampledTexture = sampledTextures[sampledIndex];
|
||||||
if (!sampledTexture) {
|
if (!sampledTexture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4554,6 +4598,7 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(textureResource != nullptr,
|
MOBILEGL_ASSERT(textureResource != nullptr,
|
||||||
"%s: SyncTextureAndGetDescriptor failed for textureId=%d",
|
"%s: SyncTextureAndGetDescriptor failed for textureId=%d",
|
||||||
__func__, sampledTexture->GetExternalIndex());
|
__func__, sampledTexture->GetExternalIndex());
|
||||||
|
sampledResources[sampledIndex] = textureResource;
|
||||||
MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)",
|
MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)",
|
||||||
sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout),
|
sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout),
|
||||||
static_cast<Int>(textureResource->layout));
|
static_cast<Int>(textureResource->layout));
|
||||||
@@ -4592,10 +4637,23 @@ void main() {
|
|||||||
activeRenderPass = nullptr;
|
activeRenderPass = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto* sampledTexture : sampledTextures) {
|
for (SizeT sampledIndex = 0; sampledIndex < sampledTextures.size(); ++sampledIndex) {
|
||||||
|
auto* sampledTexture = sampledTextures[sampledIndex];
|
||||||
if (!sampledTexture) {
|
if (!sampledTexture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Fast path: the first loop already resolved this texture, nothing
|
||||||
|
// is pending against it, and its layout is still sampleable (the
|
||||||
|
// layout re-check covers an EndRenderPass between the loops having
|
||||||
|
// rewritten an attachment's layout). Skipping the materialize +
|
||||||
|
// transition + re-resolve chain here is the difference between one
|
||||||
|
// pointer read and three calls per sampled texture per draw.
|
||||||
|
if (auto* fastResource = sampledResources[sampledIndex];
|
||||||
|
fastResource != nullptr && !m_clearManager->HasPendingClear(sampledTexture) &&
|
||||||
|
IsValidSampledImageLayout(fastResource->layout)) {
|
||||||
|
m_textureManager->StampResourceRecordingUse(*fastResource);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sampledTexture);
|
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sampledTexture);
|
||||||
MOBILEGL_ASSERT(clearReady, "%s: MaterializePendingClearForTexture failed for textureId=%d",
|
MOBILEGL_ASSERT(clearReady, "%s: MaterializePendingClearForTexture failed for textureId=%d",
|
||||||
__func__, sampledTexture->GetExternalIndex());
|
__func__, sampledTexture->GetExternalIndex());
|
||||||
@@ -4659,7 +4717,7 @@ void main() {
|
|||||||
// Every genuinely disabled attribute the shader reads must have a current-value type we can
|
// Every genuinely disabled attribute the shader reads must have a current-value type we can
|
||||||
// synthesize a binding for; otherwise the upload below would push a null payload.
|
// synthesize a binding for; otherwise the upload below would push a null payload.
|
||||||
const Uint32 missingAttribMask =
|
const Uint32 missingAttribMask =
|
||||||
activeAttribMask & ~BuildVertexInputAttributeMask(vertexInputState.attributes);
|
activeAttribMask & ~vertexInputState.attributeLocationMask;
|
||||||
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
|
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
|
||||||
if ((missingAttribMask & (1u << location)) == 0) continue;
|
if ((missingAttribMask & (1u << location)) == 0) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -507,9 +507,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||||
|
|
||||||
|
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||||
|
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||||
|
// sampled-set memo, plus the sampled textures' params-version sum so a
|
||||||
|
// level-range or filter change re-probes. On a hit the resolved
|
||||||
|
// transform flags are reused, which also collapses the two
|
||||||
|
// GetOrCreateProgram lookups into one.
|
||||||
|
Bool m_lastLodDecisionValid = false;
|
||||||
|
Uint64 m_lastLodProgramLifetimeId = 0;
|
||||||
|
Uint32 m_lastLodProgramVersion = 0;
|
||||||
|
Uint64 m_lastLodBindGeneration = 0;
|
||||||
|
Uint64 m_lastLodParamsSum = 0;
|
||||||
|
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||||
|
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||||
|
|
||||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||||
// draw call and must not allocate.
|
// draw call and must not allocate.
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||||
|
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||||
|
// first sampled-texture loop: the resolved backend resources, so the
|
||||||
|
// post-transition loop can skip re-resolving textures whose layout is
|
||||||
|
// already sampleable.
|
||||||
|
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
|
|||||||
Reference in New Issue
Block a user