[Fix] (DirectVulkan): give an unbound multisample sampler a placeholder, clear a multisample texture through a load-op pass, and plumb glSampleMaski into the pipeline

This commit is contained in:
2026-08-27 12:52:19 -04:00
parent 28c5badf8f
commit 9dee53337f
6 changed files with 212 additions and 20 deletions
@@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY( XXHASH_VERIFY(
@@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the // Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by. // struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading; ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = &payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -44,6 +44,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784). // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false; Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f; Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask. One
// word is the whole mask: GL_MAX_SAMPLE_MASK_WORDS is 1 on both backends, and Vulkan
// reads ceil(rasterizationSamples / 32) words. Pipeline state like the two above -
// Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 - so it is
// hashed with them, and 0xffffffff (the GL default, and what a null pSampleMask
// means) has to keep producing the pipeline it always did.
Uint32 sampleMask = 0xffffffffu;
Uint32 subpass = 0; Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false; Bool primitiveRestartEnable = false;
@@ -37,6 +37,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the // glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
// id only has to stay clear of the application's, exactly like the sampled fallback's. // id only has to stay clear of the application's, exactly like the sampled fallback's.
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u; constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
// The multisample sampled fallbacks. Separate ids for the same reason as the two above:
// they must not collide with anything glGenTextures can hand out.
constexpr Uint kFallbackTexture2DMultisampleExternalIndex = 0xFFFFFF02u;
constexpr Uint kFallbackTexture2DMultisampleArrayExternalIndex = 0xFFFFFF03u;
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can // MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
// do reaches one, so anything keyed on the GL object an application bound - image-unit // do reaches one, so anything keyed on the GL object an application bound - image-unit
@@ -44,7 +48,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) { Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) return false; if (texture == nullptr) return false;
const Uint index = static_cast<Uint>(texture->GetExternalIndex()); const Uint index = static_cast<Uint>(texture->GetExternalIndex());
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex; return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
index == kFallbackTexture2DMultisampleExternalIndex ||
index == kFallbackTexture2DMultisampleArrayExternalIndex;
} }
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support // The R32 member of each numeric class. Every one of the three is a MANDATORY-support
@@ -369,6 +375,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_textureManager = nullptr; m_textureManager = nullptr;
m_samplerManager = nullptr; m_samplerManager = nullptr;
m_fallbackTexture2D.reset(); m_fallbackTexture2D.reset();
m_fallbackTexture2DMultisample.reset();
m_fallbackTexture2DMultisampleArray.reset();
} }
void UniformManager::BeginFrame(Uint32 frameIndex) { void UniformManager::BeginFrame(Uint32 frameIndex) {
@@ -1377,11 +1385,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const { SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that // A multisample sampler cannot be served by the single-sampled 2D image below - its
// would accept one. A multisample sampler in particular cannot: its descriptor demands a // descriptor demands a multisample view - so it gets its own placeholder rather than no
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture. // placeholder at all. Without one, ResolveSamplerDescriptor declined and
// Report that there is no fallback and let the caller decline the draw - aborting the // BindProgramUniformBuffers dropped the WHOLE draw, which is how every
// process over an unbound sampler is never the right answer. // sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
// incomplete texture is undefined, not fatal, so the draw has to happen.
if (target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray) {
return GetFallbackMultisampleTexture(target);
}
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) { if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d", MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target)); static_cast<Int>(target));
@@ -1405,6 +1420,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return m_fallbackTexture2D; return m_fallbackTexture2D;
} }
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
TextureTarget target) const {
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
auto& slot = arrayed ? m_fallbackTexture2DMultisampleArray : m_fallbackTexture2DMultisample;
if (slot != nullptr) {
return slot;
}
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
: TextureUploadTarget::Texture2DMultisample;
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
if (arrayed) {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(
kFallbackTexture2DMultisampleArrayExternalIndex);
} else {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(
kFallbackTexture2DMultisampleExternalIndex);
}
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
// that re-created it would be worse than none.
texture->SetSamples(2);
texture->SetFixedSampleLocations(true);
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
// texture that is not complete. The point of the placeholder is that the DRAW happens.
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
texture->TruncateMipmapLevels(uploadTarget, 1);
texture->MarkStorageDirty(uploadTarget, 0, false);
slot = texture;
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d",
static_cast<Int>(target));
return slot;
}
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat, VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
SamplerNumericDomain numericDomain, Bool storage) { SamplerNumericDomain numericDomain, Bool storage) {
MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null"); MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null");
@@ -1589,11 +1643,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ResolveSamplerDescriptor will substitute the fallback texture for this binding; // ResolveSamplerDescriptor will substitute the fallback texture for this binding;
// include it in the sampled set so the pre-render-pass sync/transition pass covers // include it in the sampled set so the pre-render-pass sync/transition pass covers
// its first use instead of leaving that work to happen inside an active pass. // its first use instead of leaving that work to happen inside an active pass.
if (preferredTarget != TextureTarget::Texture2D && // Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
preferredTarget != TextureTarget::TextureRectangle) { // a multisample arm and the two must not drift apart.
texture = GetFallbackTexture(preferredTarget).get();
if (texture == nullptr) {
return false; return false;
} }
texture = GetFallbackTexture(preferredTarget).get();
// The substitution changed the texture, so the "no override" arm of the effective // The substitution changed the texture, so the "no override" arm of the effective
// sampler has to follow it to the fallback's own. // sampler has to follow it to the fallback's own.
if (!samplerOverride) { if (!samplerOverride) {
@@ -180,6 +180,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const; SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
// The multisample arm of GetFallbackTexture. Separate object per target and no upload
// path: a multisample image cannot be written by a transfer, so its texels stay undefined
// - which is what GL promises for a texelFetch on an incomplete multisample texture.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(TextureTarget target) const;
// ---- placeholders for UNBOUND image-backed descriptors ------------------------- // ---- placeholders for UNBOUND image-backed descriptors -------------------------
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing // GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete // to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
@@ -293,6 +297,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr; VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr; VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D; mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2DMultisample;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2DMultisampleArray;
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily // See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn // populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled // down with the manager. The texel views are keyed by format AND by storage-vs-sampled
@@ -4767,9 +4767,9 @@ void main() {
// build in GetOrCreatePipeline - any new GL-state read there must be added here: // build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides // - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, // the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask // PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op, // - patch vertices, polygon mode, cull face mode, depth func, logic op,
// min sample shading // min sample shading, the glSampleMaski word
// - front/back stencil ops + compare funcs (ref/mask are dynamic state) // - front/back stencil ops + compare funcs (ref/mask are dynamic state)
// - per draw buffer up to the render pass's colour span: indexed blend enable, // - per draw buffer up to the render pass's colour span: indexed blend enable,
// blend factors/equations, indexed colour write mask (broadcast from index 0 // blend factors/equations, indexed colour write mask (broadcast from index 0
@@ -4795,6 +4795,7 @@ void main() {
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0; capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
capabilityBits |= p.DepthMask ? 1ull << 8 : 0; capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0; capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
capabilityBits |= p.SampleMaskEnabled ? 1ull << 10 : 0;
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits); Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than // glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
// versions, so an unhashed float would let a pipeline built at one rate be handed back // versions, so an unhashed float would let a pipeline built at one rate be handed back
@@ -4804,6 +4805,13 @@ void main() {
std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits)); std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits));
} }
// glSampleMaski's word, for the same reason glMinSampleShading's bits are hashed above:
// this memo compares hashes, not versions, so a mask that moved between two otherwise
// identical draws has to key a different pipeline. Hashed unconditionally rather than only
// while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and
// folding one more word costs nothing on a path that only recomputes when the
// pipeline-state version moved.
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.SampleMaskValue));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
// The default tessellation levels belong here for the same reason PatchVertices does: // The default tessellation levels belong here for the same reason PatchVertices does:
// when a program has an evaluation stage and no control stage, both are compiled into the // when a program has an evaluation stage and no control stage, both are compiled into the
@@ -5199,6 +5207,12 @@ void main() {
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled && .sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading), MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(), .minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
// GL_SAMPLE_MASK off means full coverage, which is what an all-ones mask says and what
// a null pSampleMask used to say by omission. One word: MaxSampleMaskWords is clamped
// to 1 on both backends, so glSampleMaski only ever writes index 0.
.sampleMask = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)
? MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue
: 0xffffffffu,
.subpass = 0, .subpass = 0,
.topology = vkTopology, .topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled, .primitiveRestartEnable = primitiveRestartEnabled,
@@ -7610,7 +7624,8 @@ void main() {
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue) { Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout) {
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false; if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false; if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
@@ -7623,7 +7638,11 @@ void main() {
VkAttachmentDescription colorAttachment{}; VkAttachmentDescription colorAttachment{};
colorAttachment.format = resource->format; colorAttachment.format = resource->format;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; // The image's own count, not a hardcoded one: a render-pass attachment must match the
// image it is given (VUID-VkFramebufferCreateInfo-pAttachments-00880), and this helper is
// now also the multisample path - a multisample image carries no TRANSFER_DST usage, so a
// load-op clear is the only legal way to clear it at all.
colorAttachment.samples = resource->sampleCount;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@@ -7631,7 +7650,7 @@ void main() {
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// Hand the slice back in the layout the caller already tracks for the whole image, so its // Hand the slice back in the layout the caller already tracks for the whole image, so its
// closing barrier stays truthful and resource->layout is never touched from in here. // closing barrier stays truthful and resource->layout is never touched from in here.
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; colorAttachment.finalLayout = finalLayout;
VkAttachmentReference colorRef{}; VkAttachmentReference colorRef{};
colorRef.attachment = 0; colorRef.attachment = 0;
@@ -7699,9 +7718,26 @@ void main() {
"MaterializePendingClearForTexture requires no active render pass on the target buffer"); "MaterializePendingClearForTexture requires no active render pass on the target buffer");
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(resource != nullptr, if (resource == nullptr) {
"MaterializePendingClearForTexture: SyncTextureAndGetDescriptor failed for textureId=%d", // Declined sync (an incomplete texture, say). Nothing to clear into, and every line
texture.GetExternalIndex()); // below dereferences this - the assert that used to stand here is compiled out of
// every build past DEBUG.
MGLOG_E_ONCE("MaterializePendingClearForTexture: no texture resource for textureId=%d; the queued clears "
"stay queued",
texture.GetExternalIndex());
return false;
}
// A multisample image is not a transfer target: SyncTextureResource deliberately withholds
// TRANSFER_DST/TRANSFER_SRC from every one of them, so the vkCmdClearColorImage below -
// and the TRANSFER_DST transition ahead of it - are invalid usage
// (VUID-vkCmdClearColorImage-image-00002) on exactly the shape a
// glClearBufferfv-then-sample sequence produces. Clear it the one way that is legal at
// any sample count instead: a throwaway render pass whose whole content is its load-op
// clear, which is also what the 3D-slice case below already does.
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) {
return MaterializeMultisamplePendingClear(commandBuffer, texture, *resource, pendingClears);
}
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0; VkAccessFlags srcAccessMask = 0;
@@ -7841,6 +7877,77 @@ void main() {
return true; return true;
} }
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears) {
// Colour only. GL can queue a depth/stencil clear on a multisample texture too, and the
// load-op idiom would serve it just as well, but this helper attaches its view as a
// COLOUR attachment; declining is honest and leaves the queue intact for a later path.
if ((resource.aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d is a multisample depth/stencil texture; "
"its queued clear cannot be materialised out of a render pass yet",
texture.GetExternalIndex());
return false;
}
Bool allCleared = true;
for (const auto& pendingClear : pendingClears) {
if (pendingClear.key.mipLevel >= resource.mipLevels) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d pending clear mip=%u out of range %u",
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource.mipLevels);
allCleared = false;
continue;
}
auto clearPayload = pendingClear.payload;
PreCompensateSrgbClearColor(clearPayload, resource.format);
VkClearValue clearValue{};
clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture));
// A multisample texture has exactly one level and, for the 2D target, one layer; the
// array target's layers are cleared one at a time, which is what this helper's
// per-layer view gives us.
const Uint32 firstLayer = pendingClear.key.baseArrayLayer;
const Uint32 layerCount = std::max(pendingClear.key.layerCount, 1u);
for (Uint32 layer = firstLayer; layer < firstLayer + layerCount; ++layer) {
if (layer >= resource.arrayLayers) break;
// COLOR_ATTACHMENT_OPTIMAL, not TRANSFER_DST: the image never has transfer usage,
// and a render target is where it came from and where it is going.
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, layer,
clearValue, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d layer %u could not be cleared",
texture.GetExternalIndex(), layer);
allCleared = false;
}
}
}
if (!allCleared) {
return false;
}
// The load-op clear left every touched layer in COLOR_ATTACHMENT_OPTIMAL (each pass's
// finalLayout), so that - not the tracked layout on entry - is what the closing barrier
// has to start from.
// TransitionImageLayout takes the tracked layout by reference and updates it, so seeding
// it is both how the barrier learns its source and how resource->layout ends up right.
resource.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
const Bool ok = VkTextureManager::TransitionImageLayout(
commandBuffer, resource.image, resource.layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
resource.aspect, 0, resource.mipLevels);
if (!ok) {
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: failed to transition textureId=%d to the sampled layout",
texture.GetExternalIndex());
return false;
}
m_clearManager->PopPendingClear(&texture);
MGLOG_D("MaterializeMultisamplePendingClear: textureId=%d pending clear materialised through a load-op pass",
texture.GetExternalIndex());
return true;
}
Bool VulkanRenderer::MaterializePendingClearForRenderbuffer( Bool VulkanRenderer::MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) { VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {
if (renderbuffer == nullptr) { if (renderbuffer == nullptr) {
@@ -1279,13 +1279,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter); GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in // Clears one layer of a colour image through a throwaway render pass whose entire content
// MaterializePendingClearForTexture for why a transfer clear cannot do this. // is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue); Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer( Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer); const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);