[Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known

A per-draw CPU profile of a real Minecraft frame (perf on the render thread,
which sits at 100% of one core on both backends) said the deficit is translation
overhead, not the GPU, and named where it goes. This removes the largest items
it found, on both backends and in the shared frontend they both feed.

The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread
called eglGetCurrentContext on every invocation, and glvnd answers that with a
getpid() fork check - a real syscall. The predicate sits two and three deep in
every draw (the deferred-release drain, the global-UBO ring availability check,
and the ring allocation), so it accounted for 16.3% of the render thread. EGL is
still the ground truth, but re-verifying it once per thread per frame catches an
external migration at the next frame boundary rather than the next call, which
recovers the same bookkeeping.

Texture uploads now carry a dirty region instead of a per-level flag. Minecraft
animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas
and respecifies the lightmap every frame; a per-level flag turned each of those
into a full-level re-upload - about 3.6 MB a frame of texels nobody changed.
MipmapStorage accumulates the written box, Espryt uploads it with
UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box.
The box is a union, not a range list: repeated writes to one level widen it and
it degrades to exactly the old whole-level upload, which is the honest worst case.

glBufferData(NULL) is the orphaning idiom, and the backend was answering it by
uploading the stale CPU shadow - turning a rename the driver does for free into
a full synchronized upload. BufferObject now records that a NULL respecify leaves
the store undefined, and the upload is skipped until content is actually written.

The rest are smaller and of a kind: the deferred-release queue is probed without
taking its mutex, the UBO ring waits on the frame fence that frees the space it
needs instead of draining the whole pipeline with glFinish at the size cap, VAO
binds go through a shadow so a draw's second bind of the same object does not
reach the driver, the per-draw clean-texture probe short-circuits on the content
version before rebuilding shape info, glUniform drops byte-identical writes
(which otherwise dirty the whole UBO for the next draw), re-binding the texture
or VAO a slot already holds no longer bumps the generation counters a backend
fast path is keyed on, and the texture validators stopped taking shared_ptr by
value.

On Magma: descriptor-set reuse keeps four entries instead of one, because draws
alternating between two programs - the chunk/entity ping-pong - thrashed a single
slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose
contents survive two frame boundaries is promoted to resident storage instead of
being re-copied into the per-frame arena forever; and sampled-read barriers name
only the shader stages whose device feature is enabled, which also removes a
latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a
device need not have).

Measured with the Minecraft rig (render distance 32, p50 fps, same machine,
single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6;
26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma
(854 -> 766) with the native baseline itself moving 838 -> 1031 between the two
sessions, so treat that cell as unresolved rather than a regression measured.
Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are
the whole of the evidence, and a conformance regression would not have been
caught here.
This commit is contained in:
BZLZHH
2026-08-06 06:24:37 -04:00
parent 9c0144d24a
commit 57aeeec053
29 changed files with 605 additions and 89 deletions
@@ -202,9 +202,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
cacheEntryPair.second.cursor = 0;
}
// The frame's descriptor sets are recycled above, so last frame's reuse target
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
m_hasLastDescriptor = false;
// The frame's descriptor sets are recycled above, so last frame's reuse targets
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
@@ -241,8 +243,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (purgedSets > 0) {
// The per-draw reuse memo folds the layout handle into its signature; drop
// it so a recycled handle value cannot revive a purged set mid-frame.
m_hasLastDescriptor = false;
// every entry so a recycled handle value cannot revive a purged set mid-frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
@@ -1358,13 +1362,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Reuse the previous draw's descriptor set when the resolved content is
// Reuse a recent draw's descriptor set when the resolved content is
// byte-identical (only the bind-time dynamic offsets differ). The signature
// covers the descriptor-set layout + every write's binding/type/count + the
// pointed-to buffer/image/texel-buffer infos (all value-initialized, so no
// padding noise). Correctness: bindings are re-resolved every draw, so the
// signature always reflects the current state and reuse happens only on an
// exact match; the reused set is never re-acquired within a frame (the acquire
// exact match; a reused set is never re-acquired within a frame (the acquire
// cursor only advances), so its written contents survive; the layout is part of
// the signature so reuse never crosses programs. Sampler overrides (blits)
// bypass and invalidate the cache.
@@ -1396,8 +1400,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView));
}
if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) {
descriptorSet = m_lastBoundDescriptorSet;
VkDescriptorSet reusedSet = VK_NULL_HANDLE;
if (cacheable) {
for (const auto& entry : m_descriptorReuseMemo) {
if (entry.valid && entry.signature == signature) {
reusedSet = entry.set;
break;
}
}
}
if (reusedSet != VK_NULL_HANDLE) {
descriptorSet = reusedSet;
} else {
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
@@ -1411,9 +1424,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!writes.empty()) {
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
}
m_lastBoundDescriptorSet = descriptorSet;
m_lastDescriptorSignature = signature;
m_hasLastDescriptor = cacheable;
if (cacheable) {
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
DescriptorReuseEntry{signature, descriptorSet, true};
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
} else {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
}
}
// Skip the driver call when this exact binding is already live on the
@@ -179,14 +179,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to the previous
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
// frame in BeginFrame because the frame's descriptor sets are recycled there.
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to one memoized
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
// entries with round-robin replacement rather than one: draws alternating
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
// because the frame's descriptor sets are recycled there.
struct DescriptorReuseEntry {
Uint64 signature = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Bool valid = false;
};
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
@@ -591,6 +591,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// Idle-content promotion: see the field comments in VkBufferResource. The
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
// above swallows repeat draws), so a promotion needs the content stable
// for kStreamedPromotionStreak whole frames - one no-op frame does not
// trigger the resident round-trip, whose creation upload is itself a
// staged copy worth avoiding for content that is about to change again.
constexpr Uint32 kStreamedPromotionStreak = 2;
if (resource->promotedResident) {
if (resource->promotedChangeSerial == changeSerial &&
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
resource->promotedResident = false;
resource->unchangedStreak = 0;
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
resource->transientFrameSerial != 0) {
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
resource->promotedResident = true;
resource->promotedChangeSerial = changeSerial;
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
return true;
}
resource->promotedResident = false; // resident creation failed: stream as before
}
} else {
resource->unchangedStreak = 0;
}
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
outSlice)) {
return false;
@@ -62,6 +62,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
// Streaming re-copies the whole store into the per-frame arena on every
// frame, which is right for genuinely per-frame data but pure waste for a
// Dynamic-hinted buffer the app stopped touching. After the content
// survives kStreamedPromotionStreak frame boundaries unchanged it is
// promoted to resident storage (one final upload, then zero per-frame
// cost); the first content change demotes it back to streaming, and the
// streaming path's existing downgrade releases the resident store.
Uint32 unchangedStreak = 0;
Bool promotedResident = false;
Uint64 promotedChangeSerial = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
@@ -25,9 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
// compute stage in addition to the graphics stages.
static constexpr VkPipelineStageFlags kSampledReadStages =
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// compute stage in addition to the graphics stages. Set at Initialize from the renderer's
// device-feature-derived mask: geometry/tessellation stage bits are invalid in a barrier when
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
// would also serialize against non-shader stages. The default only matters before a device
// exists, when nothing records barriers.
static VkPipelineStageFlags s_sampledReadStages =
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(baseTexelSize.x(),
@@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outSrcStageMask = kSampledReadStages;
outSrcStageMask = s_sampledReadStages;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -241,7 +246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outDstStageMask = kSampledReadStages;
outDstStageMask = s_sampledReadStages;
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -599,6 +604,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
m_imageFormatListSupported = initInfo.imageFormatListSupported;
s_sampledReadStages = initInfo.sampledReadStageMask;
m_currentFrameIndex = 0;
m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount);
@@ -1226,7 +1232,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
kSampledReadStages, srcAccessMask,
s_sampledReadStages, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
@@ -2055,6 +2061,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const void* source = nullptr;
Vector<Uint8> expandedData;
VkDeviceSize offset = 0;
// Sub-region upload (a small sprite in a big atlas): only the dirty box
// is staged and copied. texelSize keeps the LEVEL extent - the staging
// row copy needs it for the shadow's stride. Plain color formats only;
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
// levels and stay full-size.
Bool subRegion = false;
IntVec3 regionLo = {0, 0, 0};
IntVec3 regionSize = {0, 0, 0};
SizeT texelBytes = 0;
};
Vector<UploadItem> uploadItems;
@@ -2101,6 +2116,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (!formatInfo.expandRgbToRgba &&
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0) {
uploadItem.subRegion = true;
uploadItem.regionLo = region.lo;
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
region.hi.z() - region.lo.z()};
uploadItem.texelBytes = byteSize / texelCount;
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
static_cast<SizeT>(uploadItem.regionSize.y()) *
static_cast<SizeT>(uploadItem.regionSize.z()) *
uploadItem.texelBytes;
}
}
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
@@ -2255,7 +2289,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* mapped = nullptr;
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
for (const auto& item : uploadItems) {
std::memcpy(static_cast<Uint8*>(mapped) + item.offset, item.source, item.uploadByteSize);
Uint8* dst = static_cast<Uint8*>(mapped) + item.offset;
if (!item.subRegion) {
std::memcpy(dst, item.source, item.uploadByteSize);
continue;
}
// Tight-pack the dirty box: the shadow keeps whole-level rows, the
// staging slice holds only the region (bufferRowLength stays 0).
const SizeT levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
const SizeT regionRowBytes = static_cast<SizeT>(item.regionSize.x()) * item.texelBytes;
const Uint8* src = static_cast<const Uint8*>(item.source);
for (Int z = 0; z < item.regionSize.z(); ++z) {
for (Int y = 0; y < item.regionSize.y(); ++y) {
const Uint8* srcRow = src +
static_cast<SizeT>(item.regionLo.z() + z) * levelSliceBytes +
static_cast<SizeT>(item.regionLo.y() + y) * levelRowBytes +
static_cast<SizeT>(item.regionLo.x()) * item.texelBytes;
std::memcpy(dst + (static_cast<SizeT>(z) * item.regionSize.y() + y) * regionRowBytes,
srcRow, regionRowBytes);
}
}
}
vmaUnmapMemory(m_allocator, stagingAllocation);
@@ -2306,6 +2360,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
depthSelectsArrayLayer ? 1u : depthOrLayers};
if (item.subRegion) {
const Uint32 regionDepth = static_cast<Uint32>(std::max(item.regionSize.z(), 1));
copy.imageOffset = {item.regionLo.x(), item.regionLo.y(),
depthSelectsArrayLayer ? 0 : item.regionLo.z()};
copy.imageExtent = {static_cast<Uint32>(item.regionSize.x()),
static_cast<Uint32>(item.regionSize.y()),
depthSelectsArrayLayer ? 1u : regionDepth};
if (depthSelectsArrayLayer) {
// The GL "depth" axis addresses array layers here, so a partial
// z-range narrows the layer span rather than the extent.
copy.imageSubresource.baseArrayLayer =
item.baseArrayLayer + static_cast<Uint32>(item.regionLo.z());
copy.imageSubresource.layerCount = regionDepth;
}
}
if (isCombinedDepthStencil) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
@@ -2330,7 +2399,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadLayout,
finalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT,
kSampledReadStages,
s_sampledReadStages,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
@@ -59,6 +59,12 @@ public:
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
// Union of shader stages sampled-read barriers may name on this device; the renderer
// builds it from the enabled features because geometry/tessellation stage bits are
// invalid in a barrier when their feature is off.
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
};
struct TextureResource {
@@ -2680,7 +2680,8 @@ void main() {
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
succeeded = m_textureManager->Initialize(
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled});
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled,
m_sampledReadStageMask});
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
m_clearManager = MakeUnique<VkClearManager>();
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
@@ -2935,6 +2936,8 @@ void main() {
dlclose(m_platformLibrary);
m_platformLibrary = nullptr;
}
#endif
if (m_debugMessenger != VK_NULL_HANDLE) {
DestroyDebugMessenger();
m_debugMessenger = VK_NULL_HANDLE;
@@ -10163,6 +10166,19 @@ void main() {
: supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
// Sampled-read barriers may only name the shader stages whose device feature is
// actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the
// mask is assembled here, next to the feature decision, and handed to consumers.
m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
if (deviceFeatures.geometryShader == VK_TRUE) {
m_sampledReadStageMask |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT;
}
if (deviceFeatures.tessellationShader == VK_TRUE) {
m_sampledReadStageMask |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT |
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT;
}
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
@@ -491,6 +491,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// Union of shader stages sampled-read barriers may name; built at device creation
// because geometry/tessellation stage bits are invalid in a barrier when their
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
// ALL_GRAPHICS would also serialize against non-shader stages.
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Cached at device creation from the graphics queue family properties
// and device limits; drives timer-query support.
Uint32 m_timestampValidBits = 0;