[MG_Backend, MG_IntegrationTest, MG_Test] (P7): a multisample depth or stencil buffer resolves on a device with no resolve extension, and the sample-count shapes with no arm decline

CONTRACT-P7 §3.2's `multisample-blit-aspect@P7`, the last `@P7` refusal in the tree.
Two halves again, and neither is the other's fallback.

THE HALF WITH AN ARM. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan
resolves a depth or stencil aspect without a shader is a render pass with a
depth/stencil resolve attachment, which needs VK_KHR_create_renderpass2 and
VK_KHR_depth_stencil_resolve - and a device with neither ended the session at
`Magma:multisample-depth-resolve-capability` on the first
glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) out of a multisampled read framebuffer,
which is what every multisample depth resolve in GL is. §3.2 names the second arm
and this is it: WireMultisampleResolve.inc, three baked modules (one vertex stage,
one fragment stage per aspect), fetching SAMPLE ZERO - the same sample
VK_RESOLVE_MODE_SAMPLE_ZERO_BIT picks, so the two arms answer identically and the
lane can assert that.

The pass carries nothing: the scratch has the source's extent and coordinate
space, so the fragment stage indexes by gl_FragCoord and the rectangle is the
scissor. No push constants, no varyings, no frontend object. The stencil half
needs VK_EXT_shader_stencil_export to exist at all (a fragment shader cannot write
the stencil aspect otherwise); where it is absent the arm declines stencil rather
than leaving the aspect undefined, and the module is not even created, because
handing vkCreateShaderModule SPIR-V with an unsupported capability is undefined
rather than an error.

The two capability tests are deliberately not the same test: the subpass resolves
every aspect the FORMAT has and needs a mode for each, while the pass writes only
the aspect this blit asked for - which is safe because the copy-out reads exactly
that aspect.

THE HALF WITH NO ARM. Sample counts that differ with the DESTINATION multisampled:
one sample fanned out to N, or N reduced to M. vkCmdResolveImage only ever writes
a single-sample destination, vkCmdBlitImage refuses a multisampled side, and
vkCmdCopyImage needs the counts equal. Rule I (a): name both counts, record
INVALID_OPERATION, leave the destination alone.

Lanes. DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepth… is PROMOTED
from the informational tier into the three gated Magma arms - it already owns the
observable (clear a 4x DEPTH24_STENCIL8, resolve with glBlitFramebuffer, read both
aspects back, check that a depth-only resolve left the destination's stencil alone
and vice versa), it just had no gated entry. Two entries over it per arm: plain,
and under MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE, which is the only way the new
arm is reachable here at all - lavapipe carries BOTH resolve extensions, so the
extension arm answers every resolve on this host. The new F1 case is the decline.
It does NOT skip on this host: the shader arm resolves depth and stencil, because
lavapipe also carries VK_EXT_shader_stencil_export.

Three rows join MOBILEGL_BAKED_INTERNAL_SHADERS (8 -> 11).

Red once (R-16, executed and reverted), three times:
  * restore MagmaWireFatal("multisample-depth-resolve-capability") for the
    non-extension arm -> DirectVulkan.Split.MsResolve1.… dies by that name while
    MsResolve0 stays green, which is also the proof that the knob routes;
  * restore MagmaWireFatal("multisample-blit-aspect@P7") ->
    DirectVulkan.Split.MsBlit.…FromASingleSampleSourceDeclines… dies by that name;
  * fetch sample 1 instead of 0 in WireMultisampleStencilResolve.frag (no rebuild -
    the gate reads the source at run time) -> WireMultisampleStencilResolveFragment
    reds at word 158, naming the symbol, the header and the regeneration command.
Reverted, 15/15 green on the three arms and 11/11 on the freshness gate.

`grep -rn "@P7" MobileGL/ --include=*.cpp --include=*.inc --include=*.h` now
returns COMMENTS ONLY - 19 lines, every one of them prose about a retirement or a
red-once. No refusal in the tree carries the mark any more.

Gates: integration-magma-split 93 (+3), -spawn 72 (+3), -tcp 74 (+3),
integration-magma-full-split 524 (+1), ctest -L unit 2423 (+3), all green;
bake_internal_shaders.py --check reports all five headers fresh; fatal_census 79
abort sites / 44 family words / 0 unmarked; link_ratchet --assert-monotone
unchanged at 186; G1 pull .text 0xa52203, nm --defined-only identical to
p7-before/pull-syms.txt (30570 lines, 0 diffs) - every line lives under
`#if MOBILEGL_BUILD_DISAGGREGATED` or in the test tree.

Device debt: the shader arm is the arm the PHONE takes wherever
VK_KHR_depth_stencil_resolve is missing, and this host can only reach it through
the knob. Its geometry needs one look on the Redmi.
This commit is contained in:
2026-09-22 18:29:22 -04:00
parent 452d33b578
commit 51fa513d73
11 changed files with 739 additions and 14 deletions
@@ -3446,6 +3446,7 @@ void main() {
ClearAllWireDrawPassCaches();
DestroyWireColorBlitResources();
DestroyWireDepthMipmapResources();
DestroyWireMultisampleResolveResources();
#endif
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
@@ -7913,6 +7914,9 @@ void main() {
// AFTER WireColorBlit.inc: the depth mip reuses that file's window-Y convention and is
// written to be read beside it.
#include "WireDepthMipmap.inc"
// AFTER WireDepthMipmap.inc for the same reason: the multisample depth/stencil resolve is
// that file's pass with the box filter taken out and a sample index put in.
#include "WireMultisampleResolve.inc"
#include "WireDraw.inc"
void VulkanRenderer::Clear(GLbitfield mask) {
@@ -14777,6 +14781,16 @@ void main() {
m_wireDepthResolveModes = resolveProperties.supportedDepthResolveModes;
m_wireStencilResolveModes = resolveProperties.supportedStencilResolveModes;
}
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the shader resolve's
// stencil half needs SPV_EXT_shader_stencil_export, because a fragment shader cannot
// write the stencil aspect without it. Enabled on the same terms as the resolve
// extensions above - wire arms only - and the arm declines stencil where it is absent
// rather than producing an undefined aspect.
m_wireShaderStencilExport = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
IsExtensionSupported(availableExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
if (m_wireShaderStencilExport)
EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions,
VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
#endif
if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr &&
getPhysicalDeviceProperties2 != nullptr) {
@@ -467,6 +467,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout* trackedLayout = nullptr;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
VkImageAspectFlags aspect = 0;
// Zero for the default framebuffer, which no arm that reads this ever reaches.
VkImageUsageFlags usageFlags = 0;
Uint32 level = 0, layer = 0, layers = 1, levels = 1;
Bool isDefault = false;
// Set when isDefault: the swapchain image index this role resolved to.
@@ -491,6 +493,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
PFN_vkCreateRenderPass2 m_wireCreateRenderPass2 = nullptr;
VkResolveModeFlags m_wireDepthResolveModes = 0;
VkResolveModeFlags m_wireStencilResolveModes = 0;
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the SECOND arm of the
// multisample depth/stencil resolve, for the device that does not carry
// VK_KHR_depth_stencil_resolve. One baked pass per aspect, sample zero, no frontend
// object - see WireMultisampleResolve.inc. `resolved` must be a single-sample image of
// the source's format and extent; the rect is expressed by the scissor.
Bool ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
VkImageAspectFlags aspect, GLint sx0, GLint sy0,
Uint32 width, Uint32 height);
struct WireMultisampleResolveResources;
WireMultisampleResolveResources* m_wireMultisampleResolveResources = nullptr;
void DestroyWireMultisampleResolveResources();
Bool m_wireShaderStencilExport = false;
Bool BlitWireColorToDefault(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter);
@@ -101,6 +101,21 @@
return forced;
}
// AND THE SAME PROBLEM AGAIN FOR THE SHADER MULTISAMPLE RESOLVE (P7 wave 2-B2). lavapipe
// carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the extension arm
// is taken for every depth/stencil resolve this host can run and the shader arm - the one
// that exists for the device WITHOUT them, which is where the session used to die at
// `Magma:multisample-depth-resolve-capability` - would never execute under any lane. The
// knob drops the extension arm. It does not pretend the extension is missing anywhere else:
// nothing but the arm choice reads it.
static Bool MagmaWireForcedShaderDepthResolve() {
static const Bool forced = [] {
const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE");
return value && *value && value[0] != '0';
}();
return forced;
}
VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage(
const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface,
VkImageAspectFlags aspect, Bool isWriteTarget) {
@@ -145,6 +160,11 @@
out.layout = resource->layout;
out.trackedLayout = &resource->layout;
out.samples = resource->sampleCount;
// P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question
// about the image and not about the format - VkTextureManager asks for SAMPLED on every
// attachment it creates, and a device that declined it would make the descriptor write
// undefined rather than fail. Carried here so the arm can decline instead.
out.usageFlags = resource->usageFlags;
out.level = level;
out.levels = resource->mipLevels;
out.layer = layer;
@@ -428,10 +448,25 @@
else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT ||
source.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
if (!m_wireCreateRenderPass2 ||
((formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) && !(m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) ||
((formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) && !(m_wireStencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)))
MagmaWireFatal("multisample-depth-resolve-capability");
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): TWO ARMS, and which one
// runs is a fact about the DEVICE, not about the shape. VK_KHR_depth_stencil_resolve is
// the direct route and is taken wherever it exists; where it does not - which is where
// the session used to end at `Magma:multisample-depth-resolve-capability`, on the first
// glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) out of a multisampled read framebuffer - a
// baked pass fetches sample ZERO and writes gl_FragDepth (or, with
// VK_EXT_shader_stencil_export, gl_FragStencilRefARB). Sample zero in both arms, because
// VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is what the subpass below asks for.
//
// THE TWO CAPABILITY TESTS ARE NOT THE SAME TEST, and the difference is structural: the
// subpass resolves every aspect the FORMAT has, so it needs a mode for each of them,
// while the shader pass writes only the one aspect this blit asked for and leaves the
// rest of the scratch undefined - which is safe because the copy below reads exactly
// that aspect.
const Bool extensionResolve = !MagmaWireForcedShaderDepthResolve() && m_wireCreateRenderPass2 &&
(!(formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ||
(m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) &&
(!(formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ||
(m_wireStencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT));
// vkCmdResolveImage is color-only. A depth/stencil resolve subpass must
// write at the source coordinates and may require both aspects to use
@@ -461,10 +496,14 @@
resolved.extent = scratch->extent;
resolved.aspect = formatAspects;
resolved.trackedLayout = &scratch->layout;
VkImageView views[2]{};
VkRenderPass pass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
if (extensionResolve) {
TransitionWireImage(source, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
VkImageView views[2]{};
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = source.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
@@ -503,7 +542,6 @@
passInfo.pAttachments = attachments;
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
VkRenderPass pass = VK_NULL_HANDLE;
VK_VERIFY(m_wireCreateRenderPass2(m_device, &passInfo, nullptr, &pass));
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = pass;
@@ -512,15 +550,38 @@
framebufferInfo.width = source.extent.width;
framebufferInfo.height = source.extent.height;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = pass;
begin.framebuffer = framebuffer;
begin.renderArea = {{sx0, sy0}, {static_cast<Uint32>(width), static_cast<Uint32>(height)}};
vkCmdBeginRenderPass(m_frameContext.GetCurrent().commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdEndRenderPass(m_frameContext.GetCurrent().commandBuffer);
} else if (!ResolveWireDepthStencilWithShader(source, resolved, source.aspect, sx0, sy0,
static_cast<Uint32>(width), static_cast<Uint32>(height))) {
// NEITHER ARM, and rule I (a) says what that is: this device has no
// VK_KHR_depth_stencil_resolve and the pass cannot stand in for it either - the
// commonest reason being a stencil-only resolve without VK_EXT_shader_stencil_export,
// where a fragment shader cannot put a value in the stencil aspect at all. A decline
// leaves the destination as the application last wrote it, which is what
// glBlitFramebuffer would do if the driver refused the resolve.
MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: resolving aspect 0x%x of a %u-sample "
"format %d needs either VK_KHR_depth_stencil_resolve (renderpass2=%d, "
"depth modes 0x%x, stencil modes 0x%x) or a shader pass "
"(stencil export=%d, source usage 0x%x); declining the blit",
static_cast<Uint32>(source.aspect), static_cast<Uint32>(source.samples),
static_cast<Int>(source.format), m_wireCreateRenderPass2 ? 1 : 0,
static_cast<Uint32>(m_wireDepthResolveModes),
static_cast<Uint32>(m_wireStencilResolveModes),
m_wireShaderStencilExport ? 1 : 0, static_cast<Uint32>(source.usageFlags));
RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation,
"This device cannot resolve that multisample depth/stencil aspect.");
WireRetiredObjects declined{};
declined.textures.push_back(Move(scratch));
RetireWireObjects(Move(declined));
return;
}
auto& frame = m_frameContext.GetCurrent();
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdEndRenderPass(frame.commandBuffer);
// Depth/stencil resolve writes execute in COLOR_ATTACHMENT_OUTPUT,
// unlike ordinary depth attachment writes. Include both when exposing
// the resolved image to a transfer; layout-based inference only covers
@@ -579,9 +640,11 @@
MagmaWireFatal("multisample-depth-resolve-conversion");
}
WireRetiredObjects retired{};
retired.framebuffers.push_back(framebuffer);
retired.renderPasses.push_back(pass);
retired.imageViews = {views[0], views[1]};
// The shader arm owns and retires its own pass, framebuffer and views, so these four
// are null on that path - pushed only when the extension arm made them.
if (framebuffer != VK_NULL_HANDLE) retired.framebuffers.push_back(framebuffer);
if (pass != VK_NULL_HANDLE) retired.renderPasses.push_back(pass);
if (views[0] != VK_NULL_HANDLE) retired.imageViews = {views[0], views[1]};
retired.textures.push_back(Move(scratch));
if (aspectBuffer) retired.buffers.push_back(Move(aspectBuffer));
RetireWireObjects(Move(retired));
@@ -883,7 +946,35 @@
VkImageCopy copy{region.srcSubresource, region.srcOffsets[0], region.dstSubresource,
region.dstOffsets[0], {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}};
vkCmdCopyImage(command, source.image, srcLayout, destination.image, dstLayout, 1, &copy);
} else MagmaWireFatal("multisample-blit-aspect@P7");
} else {
// `multisample-blit-aspect@P7` RETIRES. The half with an arm is
// ResolveWireDepthStencil's: a multisample depth or stencil SOURCE now
// reaches a single-sample destination on every device, through
// VK_KHR_depth_stencil_resolve where it exists and through the baked pass in
// WireMultisampleResolve.inc where it does not. This is the half with none -
// the sample counts differ and the DESTINATION is the multisampled side, so
// it is either one sample fanned out to N or N reduced to M.
//
// Vulkan has no command for either: vkCmdResolveImage only ever writes a
// single-sample destination, vkCmdBlitImage refuses both sides, and
// vkCmdCopyImage requires the counts to match. Reaching it through a draw
// would need a pipeline with the destination's sample count and a
// multisample source view, i.e. a second copy of the resolve machinery
// pointed the other way, for a shape GL 4.6 core 18.3.1 calls
// INVALID_OPERATION whenever the two rectangles are not identical and
// describes as sample replication when they are. Rule I (a): name both
// counts, record the error, leave the destination alone.
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: aspect 0x%x cannot go from a "
"%u-sample source to a %u-sample destination - a resolve only ever "
"writes one sample and a copy needs the counts to match; declining "
"the blit",
static_cast<Uint32>(aspect), static_cast<Uint32>(source.samples),
static_cast<Uint32>(destination.samples));
RecordTextureCopyError("BlitWireFramebuffers", ErrorCode::InvalidOperation,
"glBlitFramebuffer cannot change the sample count of an "
"attachment in that direction.");
return;
}
} else {
vkCmdBlitImage(command,source.image,srcLayout,destination.image,dstLayout,1,&region,
filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
@@ -0,0 +1,16 @@
#version 450
// SAMPLE ZERO, and it is not an approximation: VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is the mode the
// VK_KHR_depth_stencil_resolve arm asks for, and GL 4.6 core 18.3.1 says a depth resolve picks
// a value "in an implementation-dependent manner where the result will be between the minimum
// and maximum depth values in the pixel" - sample 0 is one of the samples, so it qualifies, and
// taking the same sample as the extension arm is what makes the two arms interchangeable on one
// device.
//
// AVERAGING WOULD NOT BE: a mean of four depths is a surface that exists nowhere, and GL's
// bound is on the per-pixel min/max, which a mean satisfies but a shadow comparison against it
// does not survive.
layout(set = 0, binding = 0) uniform sampler2DMS sourceImage;
void main() {
gl_FragDepth = texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r;
}
@@ -0,0 +1,319 @@
// Included by VulkanRenderer.cpp inside the backend namespace, after WireDepthMipmap.inc.
// The native multisample depth/stencil resolve owns no frontend object and never invokes the
// GL shader compiler.
//
// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth
// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment,
// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this
// package a device without either killed the session at
// `Magma:multisample-depth-resolve-capability` the first time an application resolved a
// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a
// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both
// arms; this is the second one.
//
// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
// requires a SELECTED sample for stencil anyway.
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireMultisampleResolveSpirv.h"
struct VulkanRenderer::WireMultisampleResolveResources {
struct Target {
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = 0; // the ONE aspect this pipeline writes
VkRenderPass renderPass = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
};
VkShaderModule vertex = VK_NULL_HANDLE;
VkShaderModule depthFragment = VK_NULL_HANDLE, stencilFragment = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkSampler nearest = VK_NULL_HANDLE;
Vector<Target> targets;
};
void VulkanRenderer::DestroyWireMultisampleResolveResources() {
if (!m_wireMultisampleResolveResources) return;
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit descriptor
// pools/framebuffers/views have been collected.
auto& cache = *m_wireMultisampleResolveResources;
for (const auto& target : cache.targets) {
vkDestroyPipeline(m_device, target.pipeline, nullptr);
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
}
vkDestroySampler(m_device, cache.nearest, nullptr);
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
vkDestroyShaderModule(m_device, cache.depthFragment, nullptr);
vkDestroyShaderModule(m_device, cache.stencilFragment, nullptr);
delete m_wireMultisampleResolveResources;
m_wireMultisampleResolveResources = nullptr;
}
// ONE ASPECT, source -> resolved, both 2D images of the same format and extent, the source
// multisampled and the destination not. Returns false for every shape it cannot express,
// which the caller turns into a decline; it never ends the session.
Bool VulkanRenderer::ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
VkImageAspectFlags aspect, GLint sx0, GLint sy0, Uint32 width, Uint32 height) {
if (!source.image || !resolved.image || !width || !height) return false;
if (aspect != VK_IMAGE_ASPECT_DEPTH_BIT && aspect != VK_IMAGE_ASPECT_STENCIL_BIT) return false;
// A fragment shader cannot write the stencil aspect at all without
// VK_EXT_shader_stencil_export. §3.2: decline stencil-only rather than produce a level
// whose stencil is whatever the DONT_CARE left there.
if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && !m_wireShaderStencilExport) return false;
if (source.samples == VK_SAMPLE_COUNT_1_BIT) return false;
if (source.format != resolved.format) return false;
// SAMPLED is asked for by every attachment VkTextureManager creates; a device that
// declined it would make the descriptor write below undefined, not failed.
if (!(source.usageFlags & VK_IMAGE_USAGE_SAMPLED_BIT)) return false;
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties);
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
if ((properties.optimalTilingFeatures & required) != required) return false;
VkImageAspectFlags formatAspects = VK_IMAGE_ASPECT_DEPTH_BIT;
if (source.format == VK_FORMAT_S8_UINT) formatAspects = VK_IMAGE_ASPECT_STENCIL_BIT;
else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT ||
source.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
if (!(formatAspects & aspect)) return false;
if (!m_wireMultisampleResolveResources) {
m_wireMultisampleResolveResources = new WireMultisampleResolveResources;
auto& cache = *m_wireMultisampleResolveResources;
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = bytes;
info.pCode = code;
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
};
shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), cache.vertex);
shader(kWireMultisampleDepthResolveFragmentSpirv,
sizeof(kWireMultisampleDepthResolveFragmentSpirv), cache.depthFragment);
// THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the
// StencilExportEXT capability, and handing a module with an unsupported capability
// to vkCreateShaderModule is undefined behaviour - not an error this could catch.
if (m_wireShaderStencilExport)
shader(kWireMultisampleStencilResolveFragmentSpirv,
sizeof(kWireMultisampleStencilResolveFragmentSpirv), cache.stencilFragment);
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout.bindingCount = 1;
layout.pBindings = &binding;
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
// NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and
// its rectangle from the scissor, so neither stage takes an input.
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayout.setLayoutCount = 1;
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
// NEAREST: texelFetch ignores the sampler's filter, and most devices do not
// advertise linear filtering for a depth format anyway. The sampler exists only
// because GLSL's sampler2DMS is a COMBINED image sampler.
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW =
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
sampler.maxLod = 0.0f;
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
}
auto& cache = *m_wireMultisampleResolveResources;
const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.stencilFragment
: cache.depthFragment;
if (fragment == VK_NULL_HANDLE) return false;
WireMultisampleResolveResources::Target* target = nullptr;
for (auto& entry : cache.targets)
if (entry.format == resolved.format && entry.aspect == aspect) { target = &entry; break; }
if (!target) {
cache.targets.emplace_back();
target = &cache.targets.back();
target->format = resolved.format;
target->aspect = aspect;
VkAttachmentDescription attachment{};
attachment.format = resolved.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
// DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for
// the aspect it owns, and the caller copies only that aspect out. The other aspect
// of a packed format is left undefined on purpose - it is never read, and LOADing
// it would order this pass behind a write nobody is waiting for.
attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.pDepthStencilAttachment = &depthStencil;
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
pass.attachmentCount = 1;
pass.pAttachments = &attachment;
pass.subpassCount = 1;
pass.pSubpasses = &subpass;
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
VkPipelineShaderStageCreateInfo stages[2]{};
for (auto& stage : stages) {
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage.pName = "main";
}
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = cache.vertex;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = fragment;
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewport.viewportCount = viewport.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineDepthStencilStateCreateInfo depthStencilState{
VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencilState.minDepthBounds = 0.0f;
depthStencilState.maxDepthBounds = 1.0f;
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
// ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever
// DONT_CARE left behind would discard some of it.
depthStencilState.depthTestEnable = VK_TRUE;
depthStencilState.depthWriteEnable = VK_TRUE;
depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS;
} else {
// REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes
// the shader-exported reference the value that lands. The static `reference` is
// overridden by gl_FragStencilRefARB, which is the whole point of the extension.
depthStencilState.stencilTestEnable = VK_TRUE;
VkStencilOpState op{};
op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE;
op.compareOp = VK_COMPARE_OP_ALWAYS;
op.compareMask = op.writeMask = 0xFFu;
op.reference = 0;
depthStencilState.front = depthStencilState.back = op;
}
// No colour attachment in this render pass, so no attachment states either.
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamic.dynamicStateCount = 2;
dynamic.pDynamicStates = dynamicStates;
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipeline.stageCount = 2;
pipeline.pStages = stages;
pipeline.pVertexInputState = &vertex;
pipeline.pInputAssemblyState = &assembly;
pipeline.pViewportState = &viewport;
pipeline.pRasterizationState = &raster;
pipeline.pMultisampleState = &samples;
pipeline.pDepthStencilState = &depthStencilState;
pipeline.pColorBlendState = &blend;
pipeline.pDynamicState = &dynamic;
pipeline.layout = cache.pipelineLayout;
pipeline.renderPass = target->renderPass;
if (vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr,
&target->pipeline) != VK_SUCCESS) {
vkDestroyRenderPass(m_device, target->renderPass, nullptr);
cache.targets.pop_back();
return false;
}
}
m_textureManager->FlushPendingUploads();
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = source.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = source.format;
// ONE ASPECT on the sampled view: a depth/stencil image can only be SAMPLED through a
// view that names exactly one of its aspects, which is also what makes the stencil half
// a usampler2DMS and the depth half a sampler2DMS.
viewInfo.subresourceRange = {aspect, source.level, 1, source.layer, 1};
VkImageView sourceView = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &sourceView));
viewInfo.image = resolved.image;
// BOTH aspects on the attachment view: a depth/stencil ATTACHMENT names every aspect
// its format has, whichever one the pipeline writes.
viewInfo.subresourceRange = {formatAspects, 0, 1, 0, 1};
VkImageView targetView = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &targetView));
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = target->renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &targetView;
framebufferInfo.width = resolved.extent.width;
framebufferInfo.height = resolved.extent.height;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
// Each in-flight pass owns its descriptor set, for the colour blit's reason: updating a
// cached set would rewrite the source view of an older queued pass.
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = descriptorPool;
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
const VkDescriptorImageInfo sampled{cache.nearest, sourceView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = descriptorSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &sampled;
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
auto& frame = m_frameContext.GetCurrent();
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = target->renderPass;
begin.framebuffer = framebuffer;
// The RECTANGLE is the render area, exactly as the extension arm's renderArea is - the
// scratch outside it is never read.
begin.renderArea = {{sx0, sy0}, {width, height}};
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
// THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not
// interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the
// clip-space-to-attachment mapping has to be the identity for every rectangle. A
// viewport shrunk onto the rect would slide the whole image instead of moving a window
// over it.
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(resolved.extent.width),
static_cast<float>(resolved.extent.height), 0.0f, 1.0f};
const VkRect2D scissor{{sx0, sy0}, {width, height}};
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(frame.commandBuffer);
ResetDynamicStateShadow();
InvalidatePipelineMemo();
// This native pass binds its own set/layout without going through the application's
// descriptor binder. Its next fast path must rebind.
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
WireRetiredObjects retired{};
retired.framebuffers.push_back(framebuffer);
retired.imageViews = {sourceView, targetView};
retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired));
return true;
}
#endif
@@ -0,0 +1,18 @@
#version 450
// Native Vulkan multisample depth/stencil resolve: no GL program, registry entry, or runtime
// transpilation.
//
// THIS STAGE CARRIES NOTHING, and that is the whole design. The scratch this pass writes has
// the SAME extent and the SAME coordinate space as the multisample source it reads - both are
// native images, neither is flipped against the other - so the fragment stage can take its
// coordinate straight out of gl_FragCoord and needs no varying, no push constant and no rect.
// The requested rectangle is expressed by the scissor instead, which is also what the
// VK_KHR_depth_stencil_resolve arm next door expresses with its renderArea.
//
// An oversized triangle rather than WireDepthMipmap.vert's quad: there is no destination
// rectangle to clip to here, only the scissor, and a triangle rasterizes the same area with
// one fewer primitive and no shared edge.
void main() {
const vec2 corners[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(corners[gl_VertexIndex], 0.0, 1.0);
}
@@ -0,0 +1,103 @@
// Generated from WireMultisampleResolve.vert / WireMultisampleDepthResolve.frag /
// WireMultisampleStencilResolve.frag by scripts/bake_internal_shaders.py, which runs MG_Test's
// BakedInternalShadersTest with the IN-TREE glslang and splices the words it emits into the
// arrays below. Do not edit the words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test,
// run as a gate) recompiles every source in its table and reds on the first word that
// disagrees.
#pragma once
static constexpr Uint32 kWireMultisampleResolveVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000028u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0007000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000001au, 0x00030003u,
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x0000000bu,
0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u,
0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu,
0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u,
0x0000000du, 0x00000000u, 0x00060005u, 0x0000001au, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
0x00050005u, 0x0000001du, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030047u, 0x0000000bu, 0x00000002u,
0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u,
0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u,
0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x0000001au, 0x0000000bu, 0x0000002au,
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u,
0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u,
0x0004002bu, 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u,
0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu,
0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu,
0x00000020u, 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u,
0x00000006u, 0x00000002u, 0x0004002bu, 0x00000008u, 0x00000011u, 0x00000003u, 0x0004001cu, 0x00000012u,
0x00000010u, 0x00000011u, 0x0004002bu, 0x00000006u, 0x00000013u, 0xbf800000u, 0x0005002cu, 0x00000010u,
0x00000014u, 0x00000013u, 0x00000013u, 0x0004002bu, 0x00000006u, 0x00000015u, 0x40400000u, 0x0005002cu,
0x00000010u, 0x00000016u, 0x00000015u, 0x00000013u, 0x0005002cu, 0x00000010u, 0x00000017u, 0x00000013u,
0x00000015u, 0x0006002cu, 0x00000012u, 0x00000018u, 0x00000014u, 0x00000016u, 0x00000017u, 0x00040020u,
0x00000019u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000019u, 0x0000001au, 0x00000001u, 0x00040020u,
0x0000001cu, 0x00000007u, 0x00000012u, 0x00040020u, 0x0000001eu, 0x00000007u, 0x00000010u, 0x0004002bu,
0x00000006u, 0x00000021u, 0x00000000u, 0x0004002bu, 0x00000006u, 0x00000022u, 0x3f800000u, 0x00040020u,
0x00000026u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
0x000200f8u, 0x00000005u, 0x0004003bu, 0x0000001cu, 0x0000001du, 0x00000007u, 0x0004003du, 0x0000000eu,
0x0000001bu, 0x0000001au, 0x0003003eu, 0x0000001du, 0x00000018u, 0x00050041u, 0x0000001eu, 0x0000001fu,
0x0000001du, 0x0000001bu, 0x0004003du, 0x00000010u, 0x00000020u, 0x0000001fu, 0x00050051u, 0x00000006u,
0x00000023u, 0x00000020u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000024u, 0x00000020u, 0x00000001u,
0x00070050u, 0x00000007u, 0x00000025u, 0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x00050041u,
0x00000026u, 0x00000027u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000027u, 0x00000025u, 0x000100fdu,
0x00010038u,
};
static constexpr Uint32 kWireMultisampleDepthResolveFragmentSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001du, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000008u, 0x00000010u, 0x00030010u,
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u,
0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x00000008u, 0x465f6c67u, 0x44676172u,
0x68747065u, 0x00000000u, 0x00050005u, 0x0000000cu, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u,
0x00000010u, 0x465f6c67u, 0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu,
0x00000016u, 0x00040047u, 0x0000000cu, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000cu, 0x00000022u,
0x00000000u, 0x00040047u, 0x00000010u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u,
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040020u, 0x00000007u, 0x00000003u,
0x00000006u, 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00090019u, 0x00000009u, 0x00000006u,
0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x0000000au,
0x00000009u, 0x00040020u, 0x0000000bu, 0x00000000u, 0x0000000au, 0x0004003bu, 0x0000000bu, 0x0000000cu,
0x00000000u, 0x00040017u, 0x0000000eu, 0x00000006u, 0x00000004u, 0x00040020u, 0x0000000fu, 0x00000001u,
0x0000000eu, 0x0004003bu, 0x0000000fu, 0x00000010u, 0x00000001u, 0x00040017u, 0x00000011u, 0x00000006u,
0x00000002u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040017u, 0x00000015u, 0x00000014u,
0x00000002u, 0x0004002bu, 0x00000014u, 0x00000017u, 0x00000000u, 0x00040015u, 0x0000001au, 0x00000020u,
0x00000000u, 0x0004002bu, 0x0000001au, 0x0000001bu, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u,
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000au, 0x0000000du, 0x0000000cu,
0x0004003du, 0x0000000eu, 0x00000012u, 0x00000010u, 0x0007004fu, 0x00000011u, 0x00000013u, 0x00000012u,
0x00000012u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000015u, 0x00000016u, 0x00000013u, 0x00040064u,
0x00000009u, 0x00000018u, 0x0000000du, 0x0007005fu, 0x0000000eu, 0x00000019u, 0x00000018u, 0x00000016u,
0x00000040u, 0x00000017u, 0x00050051u, 0x00000006u, 0x0000001cu, 0x00000019u, 0x00000000u, 0x0003003eu,
0x00000008u, 0x0000001cu, 0x000100fdu, 0x00010038u,
};
static constexpr Uint32 kWireMultisampleStencilResolveFragmentSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001fu, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
0x00001395u, 0x0009000au, 0x5f565053u, 0x5f545845u, 0x64616873u, 0x735f7265u, 0x636e6574u, 0x655f6c69u,
0x726f7078u, 0x00000074u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u,
0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00000008u, 0x00000012u, 0x00030010u, 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x000013a3u,
0x00030003u, 0x00000002u, 0x000001c2u, 0x00090004u, 0x415f4c47u, 0x735f4252u, 0x65646168u, 0x74735f72u,
0x69636e65u, 0x78655f6cu, 0x74726f70u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00080005u, 0x00000008u, 0x465f6c67u, 0x53676172u, 0x636e6574u, 0x65526c69u, 0x42524166u, 0x00000000u,
0x00050005u, 0x0000000du, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u, 0x00000012u, 0x465f6c67u,
0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu, 0x00001396u, 0x00040047u,
0x0000000du, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000du, 0x00000022u, 0x00000000u, 0x00040047u,
0x00000012u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040020u, 0x00000007u, 0x00000003u, 0x00000006u,
0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00040015u, 0x00000009u, 0x00000020u, 0x00000000u,
0x00090019u, 0x0000000au, 0x00000009u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u,
0x00000000u, 0x0003001bu, 0x0000000bu, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000000u, 0x0000000bu,
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000000u, 0x00030016u, 0x0000000fu, 0x00000020u, 0x00040017u,
0x00000010u, 0x0000000fu, 0x00000004u, 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu,
0x00000011u, 0x00000012u, 0x00000001u, 0x00040017u, 0x00000013u, 0x0000000fu, 0x00000002u, 0x00040017u,
0x00000016u, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000006u, 0x00000018u, 0x00000000u, 0x00040017u,
0x0000001au, 0x00000009u, 0x00000004u, 0x0004002bu, 0x00000009u, 0x0000001cu, 0x00000000u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000bu,
0x0000000eu, 0x0000000du, 0x0004003du, 0x00000010u, 0x00000014u, 0x00000012u, 0x0007004fu, 0x00000013u,
0x00000015u, 0x00000014u, 0x00000014u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000016u, 0x00000017u,
0x00000015u, 0x00040064u, 0x0000000au, 0x00000019u, 0x0000000eu, 0x0007005fu, 0x0000001au, 0x0000001bu,
0x00000019u, 0x00000017u, 0x00000040u, 0x00000018u, 0x00050051u, 0x00000009u, 0x0000001du, 0x0000001bu,
0x00000000u, 0x0004007cu, 0x00000006u, 0x0000001eu, 0x0000001du, 0x0003003eu, 0x00000008u, 0x0000001eu,
0x000100fdu, 0x00010038u,
};
@@ -0,0 +1,19 @@
#version 450
// The stencil half, and it needs an extension to exist at all: a fragment shader cannot write
// the stencil aspect without VK_EXT_shader_stencil_export (SPV_EXT_shader_stencil_export, which
// this `#extension` line is how glslang is asked for). CONTRACT-P7 §3.2 says so and says what
// happens otherwise - a stencil-only resolve DECLINES on a device without it, because there is
// no second way to put a value in the stencil aspect from a pass.
//
// usampler2DMS and not sampler2DMS: the stencil aspect of a depth/stencil image is an unsigned
// integer, and a float view of it would round the index.
//
// Sample zero, for WireMultisampleDepthResolve.frag's reason, and GL is stricter here: 18.3.1
// says integer and stencil resolves SELECT one sample rather than combining them, so sample 0
// is the answer and not merely an acceptable one.
#extension GL_ARB_shader_stencil_export : require
layout(set = 0, binding = 0) uniform usampler2DMS sourceImage;
void main() {
gl_FragStencilRefARB = int(texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r);
}
+31 -1
View File
@@ -3091,7 +3091,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# NO KNOB: every driver that can host a 4x multisample RGBA8 renderbuffer reaches both
# entries with the plain Fm environment, and one that cannot says so as a SKIP.
foreach(multisampleBlitCase MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch
MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession)
MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession
MultisampleBlitFromASingleSampleSourceDeclinesAndKeepsTheSession)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.MsBlit."
TEST_FILTER "F1WireScenario.${multisampleBlitCase}"
@@ -3100,6 +3101,35 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msblit${mglItestArmLogTag}-${multisampleBlitCase}.log")
endforeach()
# ---- P7 wave 2-B2: `multisample-blit-aspect@P7`'s retirement, BOTH ARMS ---------------
#
# THE CASE IS PROMOTED, NOT WRITTEN. DepthStencilReadbackMatrixScenario already owns the
# observable - clear a 4x multisample DEPTH24_STENCIL8 renderbuffer, resolve it into a
# single-sampled framebuffer with glBlitFramebuffer, read both aspects back, and check
# that a depth-only resolve left the destination's stencil alone and vice versa - and it
# already ran on these three arms in the INFORMATIONAL tier (`…Full.`). What it did not
# have was a gated entry, which is what CONTRACT-P7 §3.2's aspect row needs.
#
# TWO ENTRIES OVER ONE CASE, and the second is the only way its arm is reachable here:
# lavapipe carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the
# extension arm answers every resolve on this host and the shader pass - which exists FOR
# the device that has neither, where the session used to end at
# `Magma:multisample-depth-resolve-capability` - would never execute.
# MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE drops the extension arm, and the two entries
# then assert the same depth values and the same stencil indices, which is the whole
# claim: sample zero either way.
#
# ITS OWN ENTRIES RATHER THAN A LANE-WIDE FLIP, the shader-mip block's reason: the knob
# changes how EVERY depth/stencil resolve in the process is performed.
foreach(shaderDepthResolve 0 1)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.MsResolve${shaderDepthResolve}."
TEST_FILTER "DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepthReadsBackFromTheDestination"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=${shaderDepthResolve}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msresolve${mglItestArmLogTag}-shader${shaderDepthResolve}.log")
endforeach()
foreach(inPlaceCase CopyImageInPlaceAcrossLevelsAndLayers
CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone)
gtest_discover_tests(MobileGLIntegrationTest
@@ -1837,6 +1837,84 @@ TEST_F(F1WireScenario, MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeep
Gl().EndFrame();
}
// P7 wave 2-B2, CONTRACT-P7 §3.2: the surviving half of `multisample-blit-aspect@P7`.
//
// The half WITH an arm is ResolveWireDepthStencil's: a multisample depth or stencil source now
// reaches a single-sample destination on every device - through VK_KHR_depth_stencil_resolve
// where it exists, through WireMultisampleResolve.inc's baked pass where it does not - and the
// two MsResolve entries on each arm are its reading. This is the half with none.
//
// ONE SAMPLE INTO FOUR, with rectangles that MATCH so the shape decline next door is not what
// fires. Vulkan has no command for it: vkCmdResolveImage only ever writes a single-sample
// destination, vkCmdBlitImage refuses a multisampled side outright, and vkCmdCopyImage needs
// the two counts to be equal. GL 4.6 core 18.3.1 calls this sample replication and the wire arm
// declines it by name rather than ending the session over it.
//
// Red once (executed, reverted): restore MagmaWireFatal("multisample-blit-aspect@P7") and this
// case dies as Fatal{UnmigratedVerb, "Magma:multisample-blit-aspect@P7"}.
TEST_F(F1WireScenario, MultisampleBlitFromASingleSampleSourceDeclinesAndKeepsTheSession) {
if (!Ready()) return;
GLuint msFbo = 0, msRenderbuffer = 0;
glGenFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glGenRenderbuffers(1, &msRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, msRenderbuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_RGBA8, 4, 2);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRenderbuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
GTEST_SKIP() << "this driver cannot host a 4x multisample RGBA8 renderbuffer";
}
while (glGetError() != GLenum(GL_NO_ERROR)) {}
glDisable(GL_SCISSOR_TEST);
glClearColor(1, 0, 1, 1); // magenta: the colour the declined blit must leave behind
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.setup";
constexpr GLfloat kYellow[4]{1, 1, 0, 1};
GLuint srcFbo = 0, srcTexture = 0;
MakeSingleSampleTarget(4, 2, kYellow, srcFbo, srcTexture);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.source";
glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, msFbo);
const auto before = PeekSplitRuntime().emitSeq;
glBlitFramebuffer(0, 0, 4, 2, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_GT(PeekSplitRuntime().emitSeq, before) << "F1.MsAspectDecline.wire";
glFinish(); // the decline's error rides a later reply - see the shape case next door
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION))
<< "F1.MsAspectDecline: the decline must record the error GL names for the shape";
constexpr GLfloat kBlue[4]{0, 0, 1, 1};
GLuint readFbo = 0, readTexture = 0;
MakeSingleSampleTarget(4, 2, kBlue, readFbo, readTexture);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, readFbo);
glBlitFramebuffer(0, 0, 4, 2, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.resolve";
glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo);
constexpr std::array<GLubyte, 4> kMagenta{255, 0, 255, 255};
for (int y = 0; y < 2; ++y)
for (int x = 0; x < 4; ++x)
EXPECT_EQ(ReadOnePixel(x, y), kMagenta)
<< "F1.MsAspectDecline: the declined blit wrote (" << x << "," << y << ")";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline: the session survived";
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &readFbo);
glDeleteTextures(1, &readTexture);
glDeleteFramebuffers(1, &srcFbo);
glDeleteTextures(1, &srcTexture);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
glBindTexture(GL_TEXTURE_2D, texture);
Gl().EndFrame();
}
namespace {
// One 8x8x2 RGBA8 array texture with two levels, filled so that EVERY subresource this pair
// of cases touches is distinguishable from every other one. A flat fill would let a copy that
@@ -59,6 +59,7 @@ namespace {
using Uint32 = std::uint32_t;
#include <MG_Backend/DirectVulkan/Renderer/WireColorBlitSpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireDepthMipmapSpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h>
struct BakedShader {
const char* name; // ctest entry name, and the bake file's stem
@@ -103,6 +104,28 @@ const BakedShader kBakedShaders[] = {
"kWireDepthMipmapFragmentSpirv", EShLangFragment,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireDepthMipmapFragmentSpirv),
// P7 wave 2-B2: the multisample depth/stencil resolve's own pass. THREE ROWS FOR TWO
// PIPELINES - one vertex stage shared by both, and a fragment stage per aspect, because a
// fragment shader cannot write the stencil aspect without VK_EXT_shader_stencil_export and
// a device without that extension must still be able to compile the depth half.
MGL_BAKED_ROW("WireMultisampleResolveVertex",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.vert",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h",
"kWireMultisampleResolveVertexSpirv", EShLangVertex,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireMultisampleResolveVertexSpirv),
MGL_BAKED_ROW("WireMultisampleDepthResolveFragment",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleDepthResolve.frag",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h",
"kWireMultisampleDepthResolveFragmentSpirv", EShLangFragment,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireMultisampleDepthResolveFragmentSpirv),
MGL_BAKED_ROW("WireMultisampleStencilResolveFragment",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleStencilResolve.frag",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h",
"kWireMultisampleStencilResolveFragmentSpirv", EShLangFragment,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireMultisampleStencilResolveFragmentSpirv),
// The two driver self-test probes. They are not Magma's, and they are in this table for
// the reason the contract names them: they are baked, so they can be stale, and one of
// them was.