mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
8
Commits
16c010985f
...
cc34d34706
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc34d34706 | ||
|
|
757b31592d | ||
|
|
dbae4eda10 | ||
|
|
fa5ff5d168 | ||
|
|
994ae372f8 | ||
|
|
7ba012adf9 | ||
|
|
b1fdffd767 | ||
|
|
18c17ae5ca |
@@ -4386,11 +4386,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
log.back() = '\0';
|
||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||
m_backendProgramUsable = false;
|
||||
// Nothing will ever attach this one, so nothing else can free it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
continue;
|
||||
}
|
||||
|
||||
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
|
||||
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
||||
// Hand the shader's lifetime to the program, immediately and unconditionally.
|
||||
//
|
||||
// glDeleteShader only FLAGS a shader; the driver frees it when it is attached to
|
||||
// nothing. Flagging it here is what makes the program own it, so deleting the
|
||||
// program (or the detach loop above, on a relink) is what actually frees it.
|
||||
// Without this call every program build leaked its shader objects for the process
|
||||
// lifetime, and a relink leaked them twice - the detach loop above dropped the
|
||||
// program's reference to shaders nothing had flagged, so they became unreachable
|
||||
// AND undeletable. The GL swizzle conformance test builds 1,296 programs per case,
|
||||
// so a handful of cases left tens of thousands of live driver shaders behind and
|
||||
// the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*).
|
||||
// Same class of defect as the missing framebuffer/renderbuffer/sampler destructors
|
||||
// fixed in Wave 1, and the last of that family: this is the one backend GL object
|
||||
// MobileGL creates without an owning wrapper to destroy it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
|
||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||
}
|
||||
|
||||
@@ -2578,6 +2578,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
entry.storageBlockNameByBinding[binding] = uniformName;
|
||||
entry.storageBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
||||
|
||||
// A block INSTANCE array is ONE Vulkan binding carrying `count`
|
||||
// descriptors, while GL assigns its elements consecutive binding points
|
||||
// starting at the declared one (GL 4.6 core 7.8). Recording only element 0 -
|
||||
// which is all this used to do - left the layout claiming descriptorCount 1,
|
||||
// so every element past the first read a descriptor nobody wrote and
|
||||
// `b[1].data.length()` answered from an unconstrained buffer instead of its
|
||||
// own bound range (KHR-GL43.shader_storage_buffer_object.-
|
||||
// advanced-unsizedArrayLength-*).
|
||||
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(std::max<Uint32>(1u, sampler->count));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -677,7 +677,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding,
|
||||
Uint32 binding, Uint32 element,
|
||||
VkDescriptorBufferInfo& outBufferInfo) const {
|
||||
outBufferInfo = {};
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
||||
@@ -688,8 +688,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
||||
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
||||
binding);
|
||||
// A block instance array declares one block whose elements take consecutive GL binding
|
||||
// points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole
|
||||
// array to that one block - so the element index IS the offset from its binding.
|
||||
const GLuint frontendBinding =
|
||||
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex));
|
||||
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||
const Uint32 bindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||
@@ -1416,12 +1419,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
dynamicOffsets.clear();
|
||||
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
||||
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
|
||||
// Arrayed SSBO bindings contribute extra buffer infos too (but no dynamic offsets).
|
||||
Uint32 uboArrayExtra = 0;
|
||||
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
||||
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
||||
}
|
||||
Uint32 ssboArrayExtra = 0;
|
||||
for (const Uint16 count : programObj.bindingDescriptorCounts) {
|
||||
if (count > 1) ssboArrayExtra += static_cast<Uint32>(count) - 1u;
|
||||
}
|
||||
writes.reserve(m_maxBindings);
|
||||
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
|
||||
bufferInfos.reserve(m_maxBindings + uboArrayExtra + ssboArrayExtra);
|
||||
imageInfos.reserve(m_maxBindings);
|
||||
texelBufferViews.reserve(m_maxBindings);
|
||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||
@@ -1493,18 +1501,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
write.pTexelBufferView = &texelBufferViews.back();
|
||||
writes.push_back(write);
|
||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, bufferInfo)) {
|
||||
MGLOG_E(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u has no valid descriptor",
|
||||
binding);
|
||||
return false;
|
||||
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
||||
// instance array occupies a single binding whose elements each come from their
|
||||
// own GL binding point.
|
||||
const Uint32 descriptorCount =
|
||||
binding < programObj.bindingDescriptorCounts.size()
|
||||
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
||||
: 1u;
|
||||
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
||||
MGLOG_E(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
||||
"element %u has no valid descriptor",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
}
|
||||
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
fastRebindKindsEligible = false;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
write.pBufferInfo = &bufferInfos.back();
|
||||
write.descriptorCount = descriptorCount;
|
||||
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
|
||||
writes.push_back(write);
|
||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||
VkDescriptorImageInfo imageInfo{};
|
||||
|
||||
@@ -166,9 +166,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||
// binding point, buffer and glBindBufferRange window.
|
||||
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
|
||||
@@ -3016,6 +3016,7 @@ void main() {
|
||||
DestroySubmitFencePool();
|
||||
|
||||
DestroyDeferredDepthMipmapCleanup();
|
||||
DestroyMultisampleResolveScratchImage();
|
||||
DestroyComputePipelines();
|
||||
|
||||
// No sweep runs during teardown, but the observers point at this renderer
|
||||
@@ -7194,6 +7195,158 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::DestroyMultisampleResolveScratchImage() {
|
||||
if (m_msResolveScratch.image != VK_NULL_HANDLE) {
|
||||
vmaDestroyImage(m_allocator, m_msResolveScratch.image, m_msResolveScratch.allocation);
|
||||
}
|
||||
m_msResolveScratch = {};
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
|
||||
VkExtent2D extent) {
|
||||
if (extent.width == 0 || extent.height == 0 || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
// Grow-only, and never shrink: these blits repeat at one or two sizes, so the steady state
|
||||
// is one allocation for the whole process.
|
||||
if (m_msResolveScratch.image == VK_NULL_HANDLE || m_msResolveScratch.format != format ||
|
||||
m_msResolveScratch.extent.width < extent.width || m_msResolveScratch.extent.height < extent.height) {
|
||||
const VkExtent2D grown = {std::max(extent.width, m_msResolveScratch.extent.width),
|
||||
std::max(extent.height, m_msResolveScratch.extent.height)};
|
||||
DestroyMultisampleResolveScratchImage();
|
||||
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
imageInfo.format = format;
|
||||
imageInfo.extent = {grown.width, grown.height, 1};
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
VmaAllocationCreateInfo allocationInfo{};
|
||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
if (vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &m_msResolveScratch.image,
|
||||
&m_msResolveScratch.allocation, nullptr) != VK_SUCCESS) {
|
||||
// Soft failure: the caller keeps the direct resolve, which is what shipped before.
|
||||
MGLOG_E("AcquireMultisampleResolveScratchImage: vmaCreateImage failed (format=%d %ux%u)",
|
||||
static_cast<Int>(format), grown.width, grown.height);
|
||||
m_msResolveScratch = {};
|
||||
return false;
|
||||
}
|
||||
m_msResolveScratch.format = format;
|
||||
m_msResolveScratch.extent = grown;
|
||||
m_msResolveScratch.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(m_msResolveScratch.layout, srcStageMask, srcAccessMask);
|
||||
if (!VkTextureManager::TransitionImageLayout(commandBuffer, m_msResolveScratch.image,
|
||||
m_msResolveScratch.layout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The aspects a depth/stencil format actually carries. VkTextureManager keeps its own copy of
|
||||
// this private, and the swapchain's depth/stencil image has no TextureResource to ask.
|
||||
static VkImageAspectFlags GetDepthStencilAspectMaskForFormat(VkFormat format) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_D16_UNORM:
|
||||
case VK_FORMAT_X8_D24_UNORM_PACK32:
|
||||
case VK_FORMAT_D32_SFLOAT:
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
case VK_FORMAT_D16_UNORM_S8_UINT:
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
case VK_FORMAT_D32_SFLOAT_S8_UINT:
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
case VK_FORMAT_S8_UINT:
|
||||
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
default:
|
||||
return VK_IMAGE_ASPECT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// The depth/stencil half of MaterializePendingClearForDefaultFramebuffer. Separate only
|
||||
// because the image, the aspects and the clear value are all different from the colour one;
|
||||
// the reason it exists is the same - a readback with no intervening draw has no render pass
|
||||
// to fold the parked clear into.
|
||||
Bool VulkanRenderer::MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const ClearAttachmentPayload& payload) {
|
||||
const VkImage depthStencilImage = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired);
|
||||
if (depthStencilImage == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
const VkImageAspectFlags imageAspects =
|
||||
GetDepthStencilAspectMaskForFormat(m_swapchainObject.GetDepthStencilFormat());
|
||||
VkImageAspectFlags clearAspects = 0;
|
||||
if ((payload.mask & GL_DEPTH_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
if ((payload.mask & GL_STENCIL_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (clearAspects == 0) {
|
||||
// Nothing this image can express; drop the pending clear rather than leave it to a
|
||||
// later render pass that would load it against an aspect that does not exist.
|
||||
m_clearManager->PopPendingClear(attachment);
|
||||
return true;
|
||||
}
|
||||
|
||||
VkImageLayout currentLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired);
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(currentLayout, srcStageMask, srcAccessMask);
|
||||
VkImageLayout clearLayout = currentLayout;
|
||||
if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, clearLayout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, imageAspects)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VkClearDepthStencilValue clearValue{};
|
||||
clearValue.depth = payload.depth;
|
||||
clearValue.stencil = payload.stencil;
|
||||
VkImageSubresourceRange range{};
|
||||
range.aspectMask = clearAspects;
|
||||
range.baseMipLevel = 0;
|
||||
range.levelCount = 1;
|
||||
range.baseArrayLayer = 0;
|
||||
range.layerCount = 1;
|
||||
vkCmdClearDepthStencilImage(commandBuffer, depthStencilImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue,
|
||||
1, &range);
|
||||
|
||||
VkImageLayout settledLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags dstAccessMask = 0;
|
||||
GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, dstStageMask,
|
||||
dstAccessMask);
|
||||
if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, settledLayout,
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, imageAspects)) {
|
||||
return false;
|
||||
}
|
||||
m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired,
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
||||
// The image now holds real values, so the next render pass must LOAD them rather than
|
||||
// treat the attachment as undefined and discard the clear that just executed.
|
||||
m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true);
|
||||
|
||||
m_clearManager->PopPendingClear(attachment);
|
||||
MGLOG_D("MaterializePendingClearForDefaultFramebuffer: swapchain depth/stencil image %u pending clear "
|
||||
"materialized (aspects=0x%x)",
|
||||
m_imageIndexAcquired, static_cast<Uint32>(clearAspects));
|
||||
return true;
|
||||
}
|
||||
|
||||
// A glClear on the DEFAULT framebuffer is parked as a pending clear and folded into the next
|
||||
// render pass's loadOp. With no draw in between there is no render pass, so a readback that
|
||||
// followed such a clear blitted the untouched swapchain image and returned the PREVIOUS
|
||||
@@ -7217,15 +7370,14 @@ void main() {
|
||||
if (!m_clearManager->GetPendingClear(attachment, payload)) {
|
||||
return true;
|
||||
}
|
||||
if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) {
|
||||
// Depth/stencil on the default framebuffer keeps the loadOp route; the readback
|
||||
// path for it declines default framebuffers outright (ReadDepthStencilPixels).
|
||||
return true;
|
||||
}
|
||||
MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr ||
|
||||
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
||||
"MaterializePendingClearForDefaultFramebuffer requires no active render pass");
|
||||
|
||||
if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) {
|
||||
return MaterializePendingDepthStencilClearForDefaultFramebuffer(commandBuffer, attachment, payload);
|
||||
}
|
||||
|
||||
const VkImage swapchainImage = m_swapchainObject.GetImage(m_imageIndexAcquired);
|
||||
if (swapchainImage == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
@@ -7570,12 +7722,21 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawIsDefaultFbo) {
|
||||
const auto destAttachmentType =
|
||||
ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask);
|
||||
if (drawIsDefaultFbo) {
|
||||
// Same ordering rule for the default framebuffer's depth/stencil - see the
|
||||
// colour twin below.
|
||||
const Bool dstClearReady = MaterializePendingClearForDefaultFramebuffer(
|
||||
frame.commandBuffer, *drawFbo, destAttachmentType);
|
||||
MOBILEGL_ASSERT(dstClearReady,
|
||||
"BlitFramebuffer: failed to materialize the default framebuffer's pending "
|
||||
"depth/stencil clear");
|
||||
} else {
|
||||
// A clear queued for the destination predates this blit in API order;
|
||||
// execute it now, or its deferred materialization would later stomp the
|
||||
// copied contents (MC 26.3 OIT clears cloud_depth, then blits the main
|
||||
// depth into it - the stale loadOp=CLEAR erased the copy).
|
||||
const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask);
|
||||
const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType);
|
||||
if (auto destTexture = destAttachment.GetTexture(); destTexture != nullptr) {
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
|
||||
@@ -7668,7 +7829,15 @@ void main() {
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to transition depth destination image", __func__);
|
||||
}
|
||||
|
||||
if (depthBlitScales) {
|
||||
// The default framebuffer is stored display-side-up, so a rect aimed at it (or read
|
||||
// from it) has to be converted out of GL's bottom-origin space - the same conversion
|
||||
// the colour blit below applies. vkCmdCopyImage cannot express it (it has no second
|
||||
// offset to invert), so a default-framebuffer side forces the vkCmdBlitImage form even
|
||||
// at equal size. Without this a scissored depth blit into the default framebuffer
|
||||
// wrote the MIRRORED band: KHR-GL*.framebuffer_blit.scissor_blit clips to the lower
|
||||
// left quadrant, and the depth landed in the upper one.
|
||||
const Bool depthBlitNeedsOrientation = readIsDefaultFbo || drawIsDefaultFbo;
|
||||
if (depthBlitScales || depthBlitNeedsOrientation) {
|
||||
// vkCmdCopyImage cannot resize; NEAREST is the only filter Vulkan allows for a
|
||||
// depth/stencil blit anyway, and the GL front end already rejects the others.
|
||||
VkImageBlit blitRegion{};
|
||||
@@ -7684,6 +7853,14 @@ void main() {
|
||||
blitRegion.dstSubresource.layerCount = dstBinding.layerCount;
|
||||
blitRegion.dstOffsets[0] = {dstX0, dstY0, 0};
|
||||
blitRegion.dstOffsets[1] = {dstX1, dstY1, 1};
|
||||
if (readIsDefaultFbo) {
|
||||
ApplyNativeBlitDefaultFramebufferSourceTransform(m_swapchainObject.GetPreTransform(), srcBinding,
|
||||
blitRegion);
|
||||
}
|
||||
if (drawIsDefaultFbo) {
|
||||
ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding,
|
||||
blitRegion);
|
||||
}
|
||||
vkCmdBlitImage(frame.commandBuffer,
|
||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
@@ -7780,7 +7957,19 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawIsDefaultFbo) {
|
||||
if (drawIsDefaultFbo) {
|
||||
// The default framebuffer needs the same ordering, and needed it before anything
|
||||
// consumed its parked clear: Minecraft clears the default framebuffer, renders the
|
||||
// world into its own framebuffer and BLITS the result out, so nothing between the
|
||||
// clear and the blit ever opens a render pass on the default framebuffer to fold the
|
||||
// clear in as a loadOp. The clear therefore stayed pending across the whole frame,
|
||||
// and the first path that did materialize it - the readback - executed it AFTER the
|
||||
// blit and handed back a blank frame (every DirectVulkan retrace, ssim 0.000005).
|
||||
const Bool dstClearReady = MaterializePendingClearForDefaultFramebuffer(
|
||||
frame.commandBuffer, *drawFbo, drawFbo->GetDrawBuffers()[0]);
|
||||
MOBILEGL_ASSERT(dstClearReady,
|
||||
"BlitFramebuffer: failed to materialize the default framebuffer's pending clear");
|
||||
} else {
|
||||
// A clear queued for the destination predates this blit in API order; execute
|
||||
// it now, or its deferred materialization would later stomp the blitted color.
|
||||
const auto& destAttachment = drawFbo->GetAttachment(drawFbo->GetDrawBuffers()[0]);
|
||||
@@ -7878,23 +8067,78 @@ void main() {
|
||||
}
|
||||
|
||||
if (srcBinding.sampleCount != VK_SAMPLE_COUNT_1_BIT && dstBinding.sampleCount == VK_SAMPLE_COUNT_1_BIT) {
|
||||
// NOTE: vkCmdResolveImage cannot flip, and this region is still built from the raw GL
|
||||
// offsets. A multisample-resolve blit whose source or destination is the default
|
||||
// framebuffer therefore keeps the pre-fix behaviour; it needs a resolve-then-blit
|
||||
// (or blit-then-resolve) split, which is its own change.
|
||||
// GL multisample resolve blits are 1:1 by spec; vkCmdBlitImage cannot read a
|
||||
// multisampled source.
|
||||
// multisampled source, so the samples have to come down through vkCmdResolveImage.
|
||||
const Uint32 resolveWidth = static_cast<Uint32>(std::abs(srcX1 - srcX0));
|
||||
const Uint32 resolveHeight = static_cast<Uint32>(std::abs(srcY1 - srcY0));
|
||||
|
||||
// vkCmdResolveImage takes ONE offset per side, so it cannot express the axis inversion
|
||||
// that a default-framebuffer rect needs - it would land the mirrored band. When the
|
||||
// transforms above actually moved the region, split the operation: resolve into a
|
||||
// single-sample scratch image at raw offsets, then blit THAT into the destination with
|
||||
// the (already transformed) region, which vkCmdBlitImage can invert.
|
||||
const Bool regionWasTransformed =
|
||||
(readIsDefaultFbo || drawIsDefaultFbo) &&
|
||||
(blitRegion.srcOffsets[0].x != srcX0 || blitRegion.srcOffsets[0].y != srcY0 ||
|
||||
blitRegion.srcOffsets[1].x != srcX1 || blitRegion.srcOffsets[1].y != srcY1 ||
|
||||
blitRegion.dstOffsets[0].x != dstX0 || blitRegion.dstOffsets[0].y != dstY0 ||
|
||||
blitRegion.dstOffsets[1].x != dstX1 || blitRegion.dstOffsets[1].y != dstY1);
|
||||
const Bool useScratchResolve =
|
||||
regionWasTransformed && resolveWidth > 0 && resolveHeight > 0 &&
|
||||
AcquireMultisampleResolveScratchImage(frame.commandBuffer, srcBinding.format,
|
||||
{resolveWidth, resolveHeight});
|
||||
|
||||
VkImageResolve resolveRegion{};
|
||||
resolveRegion.srcSubresource = blitRegion.srcSubresource;
|
||||
resolveRegion.srcOffset = {std::min(srcX0, srcX1), std::min(srcY0, srcY1), 0};
|
||||
resolveRegion.dstSubresource = blitRegion.dstSubresource;
|
||||
resolveRegion.dstOffset = {std::min(dstX0, dstX1), std::min(dstY0, dstY1), 0};
|
||||
resolveRegion.extent = {static_cast<Uint32>(std::abs(srcX1 - srcX0)),
|
||||
static_cast<Uint32>(std::abs(srcY1 - srcY0)), 1};
|
||||
vkCmdResolveImage(frame.commandBuffer,
|
||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, &resolveRegion);
|
||||
resolveRegion.extent = {resolveWidth, resolveHeight, 1};
|
||||
if (useScratchResolve) {
|
||||
// The scratch copy is a plain single-layer colour image, and the resolve reads the
|
||||
// SOURCE band the (possibly inverted) transformed region names - taking its min so
|
||||
// an inverted pair still describes the same band.
|
||||
resolveRegion.srcOffset = {std::min(blitRegion.srcOffsets[0].x, blitRegion.srcOffsets[1].x),
|
||||
std::min(blitRegion.srcOffsets[0].y, blitRegion.srcOffsets[1].y), 0};
|
||||
resolveRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
resolveRegion.dstSubresource.mipLevel = 0;
|
||||
resolveRegion.dstSubresource.baseArrayLayer = 0;
|
||||
resolveRegion.dstSubresource.layerCount = 1;
|
||||
resolveRegion.dstOffset = {0, 0, 0};
|
||||
vkCmdResolveImage(frame.commandBuffer,
|
||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
m_msResolveScratch.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, &resolveRegion);
|
||||
|
||||
VkImageLayout scratchLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
const Bool scratchReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, m_msResolveScratch.image, scratchLayout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
MOBILEGL_ASSERT(scratchReady, "%s: failed to transition the resolve scratch image", __func__);
|
||||
m_msResolveScratch.layout = scratchLayout;
|
||||
|
||||
// Second leg: the scratch image holds the resolved band at its own origin, so the
|
||||
// source side of the region becomes the whole scratch rect and only the
|
||||
// destination keeps the transform.
|
||||
VkImageBlit scratchBlit = blitRegion;
|
||||
scratchBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
scratchBlit.srcSubresource.mipLevel = 0;
|
||||
scratchBlit.srcSubresource.baseArrayLayer = 0;
|
||||
scratchBlit.srcSubresource.layerCount = 1;
|
||||
scratchBlit.srcOffsets[0] = {0, 0, 0};
|
||||
scratchBlit.srcOffsets[1] = {static_cast<Int32>(resolveWidth), static_cast<Int32>(resolveHeight), 1};
|
||||
vkCmdBlitImage(frame.commandBuffer,
|
||||
m_msResolveScratch.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, &scratchBlit, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
||||
} else {
|
||||
resolveRegion.srcOffset = {std::min(srcX0, srcX1), std::min(srcY0, srcY1), 0};
|
||||
resolveRegion.dstOffset = {std::min(dstX0, dstX1), std::min(dstY0, dstY1), 0};
|
||||
vkCmdResolveImage(frame.commandBuffer,
|
||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, &resolveRegion);
|
||||
}
|
||||
} else {
|
||||
vkCmdBlitImage(frame.commandBuffer,
|
||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
@@ -8729,10 +8973,6 @@ void main() {
|
||||
void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
if (readFbo.IsDefaultFramebuffer()) {
|
||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: default framebuffer readback is unsupported");
|
||||
return;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -8743,10 +8983,13 @@ void main() {
|
||||
// framebuffers lacking either, so resolving via the depth attachment is enough.
|
||||
const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth
|
||||
: MobileGL::FramebufferAttachmentType::Stencil;
|
||||
const auto& attachment = readFbo.GetAttachment(attachmentType);
|
||||
if (!attachment.IsValid() || attachment.IsEmpty()) {
|
||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
|
||||
return;
|
||||
const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer();
|
||||
if (!readIsDefaultFbo) {
|
||||
const auto& attachment = readFbo.GetAttachment(attachmentType);
|
||||
if (!attachment.IsValid() || attachment.IsEmpty()) {
|
||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
@@ -8757,6 +9000,47 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
// The default framebuffer's depth/stencil lives in the swapchain, not in an
|
||||
// attachment object: its placeholder ITextureObject describes the format but backs no
|
||||
// image, so the branches below would have synced (and read back) an unrelated one.
|
||||
// Declining outright is what made every glReadPixels(GL_DEPTH_COMPONENT/
|
||||
// GL_STENCIL_INDEX) of the default framebuffer leave the caller's buffer untouched -
|
||||
// the whole KHR-GL*.framebuffer_blit family checks exactly that before it blits.
|
||||
if (readIsDefaultFbo) {
|
||||
const VkImage swapchainDepthImage = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired);
|
||||
if (swapchainDepthImage == VK_NULL_HANDLE) {
|
||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: the default framebuffer has no "
|
||||
"depth/stencil image");
|
||||
return;
|
||||
}
|
||||
// Per aspect, because the default framebuffer carries a SEPARATE placeholder
|
||||
// attachment for depth and for stencil (MG_Impl/Init.cpp) and each parks its own
|
||||
// pending clear; materializing only one would read the other back un-cleared.
|
||||
if (wantDepth) {
|
||||
const Bool clearReady = MaterializePendingClearForDefaultFramebuffer(
|
||||
frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Depth);
|
||||
MOBILEGL_ASSERT(clearReady,
|
||||
"ReadDepthStencilPixels: failed to materialize the default framebuffer's pending "
|
||||
"depth clear");
|
||||
}
|
||||
if (wantStencil) {
|
||||
const Bool clearReady = MaterializePendingClearForDefaultFramebuffer(
|
||||
frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Stencil);
|
||||
MOBILEGL_ASSERT(clearReady,
|
||||
"ReadDepthStencilPixels: failed to materialize the default framebuffer's pending "
|
||||
"stencil clear");
|
||||
}
|
||||
const VkFormat swapchainDepthFormat = m_swapchainObject.GetDepthStencilFormat();
|
||||
VkImageLayout trackedLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired);
|
||||
ReadDepthStencilImageToClient(swapchainDepthImage, swapchainDepthFormat, &trackedLayout,
|
||||
GetDepthStencilAspectMaskForFormat(swapchainDepthFormat), 0, 0, x, y,
|
||||
width, height, format, type, pixels,
|
||||
/*defaultFramebufferOrientation=*/true);
|
||||
m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, trackedLayout);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& attachment = readFbo.GetAttachment(attachmentType);
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout* trackedLayout = nullptr;
|
||||
@@ -8807,7 +9091,8 @@ void main() {
|
||||
void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels,
|
||||
Bool defaultFramebufferOrientation) {
|
||||
const Bool wantDepth = format != GL_STENCIL_INDEX;
|
||||
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
@@ -8872,6 +9157,21 @@ void main() {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, imageAspect, mipLevel, 1);
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to transition depth-stencil source image", __func__);
|
||||
|
||||
// The swapchain's depth/stencil image is stored display-side-up like its colour twin, so
|
||||
// the GL rect has to be mapped into that space before the copy and the copied rows
|
||||
// re-oriented afterwards - the same two halves the colour ReadPixels path applies.
|
||||
Int32 copyOffsetX = x;
|
||||
Int32 copyOffsetY = y;
|
||||
if (defaultFramebufferOrientation) {
|
||||
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||
const DefaultFramebufferRectMapping mapping =
|
||||
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
||||
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
||||
mapping.mirrorX);
|
||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
||||
mapping.flipY);
|
||||
}
|
||||
|
||||
VkBufferImageCopy regions[2]{};
|
||||
Uint32 regionCount = 0;
|
||||
if (wantDepth) {
|
||||
@@ -8881,7 +9181,7 @@ void main() {
|
||||
region.imageSubresource.mipLevel = mipLevel;
|
||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageOffset = {x, y, 0};
|
||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
}
|
||||
if (wantStencil) {
|
||||
@@ -8891,7 +9191,7 @@ void main() {
|
||||
region.imageSubresource.mipLevel = mipLevel;
|
||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageOffset = {x, y, 0};
|
||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
}
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||
@@ -8916,6 +9216,38 @@ void main() {
|
||||
const Uint8* depthSrc = mapped;
|
||||
const Uint8* stencilSrc = mapped + stencilOffset;
|
||||
|
||||
// Re-orient the copied band per aspect, before any repacking reads it: the depth and
|
||||
// stencil aspects were copied into their own tightly packed sub-buffers, so each is a
|
||||
// plain width x height image of its own texel size.
|
||||
Vector<Uint8> remappedDepth;
|
||||
Vector<Uint8> remappedStencil;
|
||||
if (defaultFramebufferOrientation) {
|
||||
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
||||
Bool remapped = true;
|
||||
if (wantDepth && depthCopyBytes > 0) {
|
||||
remappedDepth.resize(pixelCount * depthCopyBytes);
|
||||
remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform,
|
||||
depthCopyBytes, remappedDepth.data());
|
||||
}
|
||||
if (remapped && wantStencil) {
|
||||
remappedStencil.resize(pixelCount);
|
||||
remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform, 1,
|
||||
remappedStencil.data());
|
||||
}
|
||||
if (remapped) {
|
||||
if (!remappedDepth.empty()) depthSrc = remappedDepth.data();
|
||||
if (!remappedStencil.empty()) stencilSrc = remappedStencil.data();
|
||||
} else {
|
||||
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer
|
||||
// models one. MGLOG_I because the INFO builds are the ones that run conformance.
|
||||
MGLOG_I("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d "
|
||||
"preTransform=%d); falling back to raw readback",
|
||||
width, height, static_cast<Int>(preTransform));
|
||||
}
|
||||
}
|
||||
|
||||
const auto depthValueAt = [&](SizeT i) -> Float {
|
||||
switch (vkFormat) {
|
||||
case VK_FORMAT_D16_UNORM: {
|
||||
|
||||
@@ -211,10 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
//
|
||||
// `defaultFramebufferOrientation` is set only when the source is the swapchain's
|
||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
||||
// the way back, exactly as the colour ReadPixels path does.
|
||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels);
|
||||
void* pixels, Bool defaultFramebufferOrientation = false);
|
||||
// Same-extent depth blit between images of different depth formats: host
|
||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||
@@ -363,6 +368,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 samplerBinding = 0;
|
||||
};
|
||||
|
||||
// A single-sample staging image for multisample-resolve blits that also have to change
|
||||
// orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
|
||||
// invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
|
||||
// framebuffer used to land the mirrored band. Resolving here first and then blitting from
|
||||
// here separates the two operations, and each one then does only what it can express.
|
||||
//
|
||||
// Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
|
||||
// create-destroy per call would both cost allocations and, worse, need per-call deferred
|
||||
// destruction to outlive the recording. It grows to the largest extent asked for and is
|
||||
// reused; format changes recreate it.
|
||||
struct MultisampleResolveScratchImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D extent = {0, 0};
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
MultisampleResolveScratchImage m_msResolveScratch;
|
||||
// Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
|
||||
// TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
|
||||
// back to the direct resolve).
|
||||
Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
|
||||
VkExtent2D extent);
|
||||
void DestroyMultisampleResolveScratchImage();
|
||||
|
||||
struct DeferredDepthMipmapCleanup {
|
||||
Vector<VkImageView> imageViews;
|
||||
Vector<VkFramebuffer> framebuffers;
|
||||
@@ -1125,6 +1155,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::FramebufferObject& fbo,
|
||||
FramebufferAttachmentType attachmentType);
|
||||
// Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
|
||||
// different clear command and per-aspect masking.
|
||||
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const ClearAttachmentPayload& payload);
|
||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
|
||||
@@ -58,6 +58,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -180,4 +180,156 @@ void main() { o_color = vec4(0.1, 0.2, 0.3, 1.0); }
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// The other half of the same rule, and the one the first version of this fix got wrong: a
|
||||
// parked clear must be executed BEFORE whatever writes the framebuffer next, not whenever the
|
||||
// readback happens to notice it. Minecraft clears the default framebuffer, renders the world
|
||||
// into its own framebuffer and blits the result out; nothing in between opens a render pass on
|
||||
// the default framebuffer, so the clear stays parked across the whole frame. Materializing it
|
||||
// at readback time therefore ran it AFTER the blit and returned a blank frame - which is what
|
||||
// took every DirectVulkan retrace to ssim 0.000005.
|
||||
TEST_F(ClearThenReadPixelsScenario, ABlitIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// Paint a source framebuffer, exactly as a game renders its world off-screen.
|
||||
ColorFbo source = MakeColorFbo(width, height);
|
||||
ASSERT_NE(source.fbo, 0u);
|
||||
BindFbo(source);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
|
||||
// Clear the DEFAULT framebuffer, then blit the source over it. The clear is white so a
|
||||
// frame that lost the blit is unmistakable, and the blit's colour is fshSimple's.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, source.fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image blitted = ReadPixels(width, height);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
const Rgba8 centre = blitted.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 26, 2) << "the blit into the default framebuffer did not survive the clear that "
|
||||
"preceded it; read back rgba(" << static_cast<int>(centre.r) << ", "
|
||||
<< static_cast<int>(centre.g) << ", " << static_cast<int>(centre.b) << ", "
|
||||
<< static_cast<int>(centre.a) << ")";
|
||||
EXPECT_NEAR(centre.g, 51, 2);
|
||||
EXPECT_NEAR(centre.b, 77, 2);
|
||||
|
||||
DestroyColorFbo(source);
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// A MULTISAMPLE-RESOLVE blit into the default framebuffer has to change orientation like any
|
||||
// other, but vkCmdResolveImage takes one offset per side and cannot invert an axis, so it used
|
||||
// to land the mirrored band. The renderer now resolves into a single-sample scratch image and
|
||||
// blits from there. The source is painted in two horizontal bands so the mirror is visible;
|
||||
// a full-extent uniform blit is a fixed point of the flip and would prove nothing.
|
||||
TEST_F(ClearThenReadPixelsScenario, AMultisampleResolveBlitIntoTheDefaultFramebufferKeepsItsOrientation) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
if (maxSamples < 2) {
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
|
||||
GLuint fbo = 0, rbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenRenderbuffers(1, &rbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 2, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
GTEST_SKIP() << "no complete 2x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
glViewport(0, 0, width, height);
|
||||
|
||||
// Bottom half red, top half blue - via scissored clears, so no shader is involved.
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width, height / 2);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glScissor(0, height / 2, width, height - height / 2);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
const Rgba8 bottom = resolved.At(width / 2, height / 4);
|
||||
const Rgba8 top = resolved.At(width / 2, height - 1 - height / 4);
|
||||
EXPECT_GT(bottom.r, 200) << "the bottom band should be red after the resolve, got rgba("
|
||||
<< static_cast<int>(bottom.r) << ", " << static_cast<int>(bottom.g) << ", "
|
||||
<< static_cast<int>(bottom.b) << ") - blue there means the resolve landed "
|
||||
<< "in the mirrored band";
|
||||
EXPECT_LT(bottom.b, 60);
|
||||
EXPECT_GT(top.b, 200) << "the top band should be blue after the resolve, got rgba("
|
||||
<< static_cast<int>(top.r) << ", " << static_cast<int>(top.g) << ", "
|
||||
<< static_cast<int>(top.b) << ")";
|
||||
EXPECT_LT(top.r, 60);
|
||||
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The same ordering claim for the path that DOES open a render pass. It passes today (the
|
||||
// render pass folds the clear into its loadOp and pops it), and it is here so a future change
|
||||
// to the pending-clear lifecycle cannot quietly reverse clear and draw.
|
||||
TEST_F(ClearThenReadPixelsScenario, ADrawIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 26, 2) << "the draw did not survive the clear that preceded it";
|
||||
EXPECT_NEAR(centre.g, 51, 2);
|
||||
EXPECT_NEAR(centre.b, 77, 2);
|
||||
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glReadPixels OF DEPTH AND STENCIL FROM THE DEFAULT FRAMEBUFFER.
|
||||
//
|
||||
// DirectVulkan's depth/stencil readback used to decline the default framebuffer outright
|
||||
// (`ReadDepthStencilPixels` returned at its first line) because that framebuffer's depth and
|
||||
// stencil "attachments" are placeholder texture objects backing no image - the real one is the
|
||||
// swapchain's depth/stencil twin. Declining meant the call raised no GL error and wrote NOTHING,
|
||||
// so the caller kept whatever its buffer already held.
|
||||
//
|
||||
// That silence is what the framebuffer_blit family trips over. Every one of its cases begins by
|
||||
// clearing the default framebuffer's depth and stencil and reading them straight back as a
|
||||
// sanity check, into a local pre-initialised to 0.2 (depth) and 50 (stencil); an untouched
|
||||
// buffer therefore reports "expected DEPTH[0.25] but got DEPTH[0.2]" and "expected STENCIL[1] but
|
||||
// got STENCIL[50]" - the exact strings in the 15 Magma failures - long before any blit happens.
|
||||
// A test that only checked "no GL error" would pass against the broken path, so every case here
|
||||
// poisons its destination with a value the correct answer cannot be.
|
||||
//
|
||||
// The orientation case is the second half. This renderer stores the default framebuffer
|
||||
// display-side-up and converts GL rects on their way in, so the depth copy needs the same rect
|
||||
// mapping and row re-ordering the colour readback got in the M-1 fix; without them a
|
||||
// vertically-varying depth buffer reads back mirrored, which no full-extent uniform-value test
|
||||
// can see.
|
||||
//
|
||||
// Depth/stencil readback through a USER framebuffer already worked and is asserted here too, as
|
||||
// the built-in control: it shares ReadDepthStencilImageToClient with the default-framebuffer
|
||||
// path, so it is what says a failure is about the default framebuffer specifically.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Values no correct read can produce, so "the backend wrote nothing" fails loudly instead
|
||||
// of passing on whatever happened to be in the variable. These are the CTS's own poison
|
||||
// values, which is why its logs report exactly them.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
|
||||
class DepthStencilReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
// DirectGLES reads depth and stencil back through the ES driver, which has no
|
||||
// guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and
|
||||
// absent on both the Adreno device and Mesa's ES). That gap is tracked separately as
|
||||
// the packed_depth_stencil cluster and needs a shader-sampling emulation, not this
|
||||
// change; asserting it here would only pin a known-missing feature.
|
||||
bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
int ReadStencilAt(int x, int y) const {
|
||||
int stencil = kStencilPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||
return stencil;
|
||||
}
|
||||
};
|
||||
|
||||
// A depth buffer whose value depends on the row: bottom half `bottom`, top half `top`.
|
||||
// Built with a scissored clear rather than a draw so the test stays independent of
|
||||
// depth-test and shader behaviour.
|
||||
void ClearDepthInBands(int width, int height, float bottom, float top) {
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width, height / 2);
|
||||
glClearDepth(bottom);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
glScissor(0, height / 2, width, height - height / 2);
|
||||
glClearDepth(top);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
||||
<< " has no depth readback path (ES lacks GL_NV_read_depth); see the packed_depth_stencil "
|
||||
"cluster";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.25);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.25f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of the default framebuffer returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferStencilClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
||||
<< " has no stencil readback path (ES lacks GL_NV_read_stencil); see the "
|
||||
"packed_depth_stencil cluster";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glStencilMask(0xFFu);
|
||||
glClearStencil(3);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
const int centre = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre, 3) << "glReadPixels(GL_STENCIL_INDEX) of the default framebuffer returned " << centre
|
||||
<< (centre == kStencilPoison ? " - the destination was never written at all" : "");
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The orientation half: a depth buffer that varies with the row must read back in GL's
|
||||
// bottom-up order. A full-extent uniform clear is a fixed point of the flip, so only a banded
|
||||
// buffer can tell the two apart.
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthReadbackKeepsTheGLRowOrder) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDepthMask(GL_TRUE);
|
||||
ClearDepthInBands(width, height, /*bottom=*/0.25f, /*top=*/0.75f);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const float bottom = ReadDepthAt(width / 2, height / 4);
|
||||
const float top = ReadDepthAt(width / 2, height - 1 - height / 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(bottom, 0.25f, 1.0f / 4096.0f)
|
||||
<< "GL row " << (height / 4) << " is in the bottom band and was cleared to 0.25, but read back " << bottom
|
||||
<< " (0.75 there means the readback is upside down)";
|
||||
EXPECT_NEAR(top, 0.75f, 1.0f / 4096.0f)
|
||||
<< "GL row " << (height - 1 - height / 4) << " is in the top band and was cleared to 0.75, but read back "
|
||||
<< top << " (0.25 there means the readback is upside down)";
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// A depth blit INTO the default framebuffer has to convert its rect out of GL's bottom-origin
|
||||
// space, exactly as the colour blit does. The colour path had that conversion and the
|
||||
// depth path did not, so a scissored depth blit landed in the mirrored band - which is the
|
||||
// whole of KHR-GL*.framebuffer_blit.scissor_blit once the readback above works well enough to
|
||||
// see it (before that the test died on the poison values and never reached the blit).
|
||||
TEST_F(DepthStencilReadbackScenario, AScissoredDepthBlitIntoTheDefaultFramebufferLandsInTheScissorBox) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
// Source: a user framebuffer whose depth is uniformly 0.75.
|
||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenTextures(1, &colorTex);
|
||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.75);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Destination: the default framebuffer, depth 0 everywhere.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Blit the whole rect, but scissored to the BOTTOM-LEFT quadrant in GL coordinates.
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width / 2, height / 2);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const float inside = ReadDepthAt(width / 4, height / 4);
|
||||
const float above = ReadDepthAt(width / 4, height - 1 - height / 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(inside, 0.75f, 1.0f / 4096.0f)
|
||||
<< "GL (" << (width / 4) << ", " << (height / 4) << ") is inside the scissor box and should hold the "
|
||||
<< "blitted 0.75, but read back " << inside;
|
||||
EXPECT_NEAR(above, 0.0f, 1.0f / 4096.0f)
|
||||
<< "GL (" << (width / 4) << ", " << (height - 1 - height / 4)
|
||||
<< ") is ABOVE the scissor box and must still hold the cleared 0.0, but read back " << above
|
||||
<< " (0.75 there means the depth blit landed in the mirrored band)";
|
||||
|
||||
glDeleteTextures(1, &depthTex);
|
||||
glDeleteTextures(1, &colorTex);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The control: the same read against a user framebuffer, which never went through the
|
||||
// declined path. It is what makes a failure above specific to the default framebuffer.
|
||||
TEST_F(DepthStencilReadbackScenario, UserFramebufferDepthClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = 64;
|
||||
const int height = 48;
|
||||
|
||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenTextures(1, &colorTex);
|
||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glClearDepth(0.5);
|
||||
glClearStencil(7);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, 0.5f, 1.0f / 4096.0f) << "user-framebuffer depth readback returned " << depth;
|
||||
EXPECT_EQ(stencil, 7) << "user-framebuffer stencil readback returned " << stencil;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteTextures(1, &depthTex);
|
||||
glDeleteTextures(1, &colorTex);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,215 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SsboArrayLengthScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - length() ON AN SSBO's UNSIZED ARRAY.
|
||||
//
|
||||
// GLSL's `arr.length()` on the trailing runtime array of a shader storage block is not a compile
|
||||
// time constant: it is (bound range - the array's byte offset inside the block) / array stride,
|
||||
// evaluated against whatever the descriptor actually covers. Three separate pieces of MobileGL
|
||||
// have to agree for that to come out right - the byte offsets the block layout was compiled with,
|
||||
// the buffer the frontend binding resolves to, and the offset/size a glBindBufferRange asked for -
|
||||
// and a defect in any one of them shows up only as a wrong integer, never as an error.
|
||||
//
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-unsizedArrayLength-* (28 Magma failures, all 28
|
||||
// passing on Espryt) reports exactly that: lengths too large by roughly the size of the members
|
||||
// preceding the array. The cases here are the same shape, reduced to what can be asserted in one
|
||||
// dispatch: a block with no preamble, a block with one, a two-element ARRAY OF BLOCKS (which
|
||||
// consumes two consecutive bindings and is where the conformance failures concentrate), and the
|
||||
// two glBindBufferRange forms.
|
||||
//
|
||||
// Every length is written into one output SSBO and read back, so a failure names the block and
|
||||
// prints the number the shader saw.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Bindings 0..3 are inputs (2 and 3 are the block array), 4 is the output.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) readonly buffer Input0 {
|
||||
ivec4 g_input0[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer Input1 {
|
||||
ivec4 pad1;
|
||||
ivec4 data[];
|
||||
} g_input1;
|
||||
layout(std430, binding = 2) readonly buffer Input23 {
|
||||
ivec4 data[];
|
||||
} g_input23[2];
|
||||
layout(std430, binding = 4) buffer Output {
|
||||
int g_length[];
|
||||
};
|
||||
void main() {
|
||||
g_length[0] = g_input0.length();
|
||||
g_length[1] = g_input1.data.length();
|
||||
g_length[2] = g_input23[0].data.length();
|
||||
g_length[3] = g_input23[1].data.length();
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kElementBytes = 16; // ivec4, std430
|
||||
|
||||
class SsboArrayLengthScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
GLint blocks = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
|
||||
if (blocks < 5) {
|
||||
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 5";
|
||||
}
|
||||
m_program = CompileComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// A buffer of `elements` ivec4s, filled with a recognisable pattern.
|
||||
GLuint MakeStorageBuffer(int elements) {
|
||||
std::vector<int> contents(static_cast<std::size_t>(elements) * 4, 41);
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
||||
static_cast<GLsizeiptr>(elements) * kElementBytes, contents.data(), GL_DYNAMIC_COPY);
|
||||
m_buffers.push_back(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Dispatches once and returns the four lengths the shader observed.
|
||||
std::vector<int> RunAndReadLengths(GLuint outputBuffer) {
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<int> lengths(4, -1);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, outputBuffer);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(lengths.size() * sizeof(int)), lengths.data());
|
||||
return lengths;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
std::string m_buildLog;
|
||||
std::vector<GLuint> m_buffers;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// glBindBufferBase everywhere: the plain case, and the one that pins the block array.
|
||||
TEST_F(SsboArrayLengthScenario, WholeBufferBindingsReportTheElementCount) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// input1 carries one ivec4 of preamble before its runtime array, so a length that ignores
|
||||
// the member offset comes back one too large there and only there.
|
||||
const GLuint input0 = MakeStorageBuffer(7);
|
||||
const GLuint input1 = MakeStorageBuffer(1 + 5);
|
||||
const GLuint input2 = MakeStorageBuffer(3);
|
||||
const GLuint input3 = MakeStorageBuffer(4);
|
||||
const GLuint output = MakeStorageBuffer(4);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, input1);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, input2);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<int> lengths = RunAndReadLengths(output);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(lengths[0], 7) << "Input0 (no preamble, 7 elements) reported length " << lengths[0];
|
||||
EXPECT_EQ(lengths[1], 5) << "Input1 (1 ivec4 of preamble, 6 elements of storage) reported length "
|
||||
<< lengths[1] << "; 6 means the array's byte offset inside the block was ignored";
|
||||
EXPECT_EQ(lengths[2], 3) << "Input23[0] (binding 2, 3 elements) reported length " << lengths[2];
|
||||
EXPECT_EQ(lengths[3], 4) << "Input23[1] (binding 3, 4 elements) reported length " << lengths[3]
|
||||
<< "; a block array's second element must resolve to the NEXT binding";
|
||||
}
|
||||
|
||||
// glBindBufferRange with a non-zero offset: length() must see only the bound window.
|
||||
TEST_F(SsboArrayLengthScenario, RangeBindingsReportTheBoundWindow) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLint alignment = 1;
|
||||
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &alignment);
|
||||
if (alignment > 2 * kElementBytes) {
|
||||
GTEST_SKIP() << "GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is " << alignment
|
||||
<< "; a two-element offset cannot be expressed";
|
||||
}
|
||||
|
||||
const GLuint input0 = MakeStorageBuffer(7);
|
||||
const GLuint input1 = MakeStorageBuffer(1 + 5);
|
||||
const GLuint input2 = MakeStorageBuffer(3);
|
||||
const GLuint input3 = MakeStorageBuffer(4);
|
||||
const GLuint output = MakeStorageBuffer(4);
|
||||
// Input0: window starts two elements in, so 5 remain.
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, input0, 2 * kElementBytes, 5 * kElementBytes);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, input1);
|
||||
// Both elements of the block array get a window, so a failure says whether the array's
|
||||
// FIRST element is handled and only the later ones are lost, or neither is.
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, input2, 0, 2 * kElementBytes);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, input3, 0, 2 * kElementBytes);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<int> lengths = RunAndReadLengths(output);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(lengths[0], 5) << "Input0 bound as [2 elements, 5 elements) reported length " << lengths[0]
|
||||
<< "; 7 means glBindBufferRange's offset/size never reached the descriptor";
|
||||
EXPECT_EQ(lengths[2], 2) << "Input23[0] bound as [0, 2 elements) reported length " << lengths[2];
|
||||
EXPECT_EQ(lengths[3], 2) << "Input23[1] bound as [0, 2 elements) reported length " << lengths[3];
|
||||
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,310 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - EVERY TEXTURE ACCESS ROUTINE READS THE SAME TEXEL OUT OF A usampler2DArray.
|
||||
//
|
||||
// KHR-GL33/GL40.texture_swizzle.smoke_access_idx_* sweeps the fourteen GLSL texture access
|
||||
// routines against a 1x1x1 GL_RGBA32UI GL_TEXTURE_2D_ARRAY and asserts the fetched channel. On
|
||||
// Espryt, `texture` and `textureGrad` pass while `textureLod`, `textureOffset`, `texelFetch`,
|
||||
// `texelFetchOffset` and `textureLodOffset` fail - 21 cases per version, 42 across GL33 and GL40.
|
||||
// The discriminator is the important part: the swizzle state is IDENTICAL across all of them, so
|
||||
// swizzle delivery is not the defect; what differs is only how the routine is spelled, i.e. what
|
||||
// SPIRV-Cross has to emit into ESSL for it.
|
||||
//
|
||||
// This scenario is that discriminator, reduced to something that fails in milliseconds: one draw
|
||||
// per access routine against the same texture and the same swizzle, all reading the same texel.
|
||||
// A routine that disagrees with the others is the defect, and the failure message names it.
|
||||
//
|
||||
// The shader shape is copied from the conformance test rather than idealised - including its
|
||||
// `int(0)` level-of-detail argument, which is a desktop-GLSL implicit int->float conversion that
|
||||
// ESSL does not have, and its zero offsets. Both are exactly the things a GLSL -> SPIR-V -> ESSL
|
||||
// round trip can lose.
|
||||
//
|
||||
// DirectVulkan is the built-in control: it consumes the SPIR-V directly and never runs the ESSL
|
||||
// emission, so a failure there would mean the scenario, not the backend.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The conformance test's own source texel, one recognisable value per channel.
|
||||
constexpr std::uint32_t kSourceTexel[4] = {0x3FFFFFFFu, 0x7FFFFFFFu, 0xBFFFFFFFu, 0xFFFFFFFFu};
|
||||
|
||||
constexpr int kOutputWidth = 8;
|
||||
constexpr int kOutputHeight = 8;
|
||||
|
||||
// The blank vertex shader the smoke test uses: a full-viewport strip with no attributes.
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
struct AccessRoutine {
|
||||
const char* name; // as it appears in the conformance case name
|
||||
const char* callText; // the whole TEXTURE_ACCESS(sampler, ARGUMENTS) expression
|
||||
};
|
||||
|
||||
// Spelled exactly as gl3cTextureSwizzleTests.cpp's prepareArguments builds them for
|
||||
// GL_TEXTURE_2D_ARRAY: three coordinates, `int(0)` for the level, ivec2 offsets.
|
||||
constexpr AccessRoutine kRoutines[] = {
|
||||
{"texture", "texture(smp, vec3(0, 0, 0))"},
|
||||
{"textureLod", "textureLod(smp, vec3(0, 0, 0), int(0))"},
|
||||
{"textureOffset", "textureOffset(smp, vec3(0, 0, 0), ivec2(0, 0))"},
|
||||
{"texelFetch", "texelFetch(smp, ivec3(0, 0, 0), int(0))"},
|
||||
{"texelFetchOffset", "texelFetchOffset(smp, ivec3(0, 0, 0), int(0), ivec2(0, 0))"},
|
||||
{"textureLodOffset", "textureLodOffset(smp, vec3(0, 0, 0), int(0), ivec2(0, 0))"},
|
||||
{"textureGrad", "textureGrad(smp, vec3(0, 0, 0), vec2(0, 0), vec2(0, 0))"},
|
||||
{"textureGradOffset", "textureGradOffset(smp, vec3(0, 0, 0), vec2(0, 0), vec2(0, 0), ivec2(0, 0))"},
|
||||
};
|
||||
|
||||
constexpr const char* kChannels[4] = {"x", "y", "z", "w"};
|
||||
|
||||
std::string FragmentSource(const AccessRoutine& routine, int channel) {
|
||||
return std::string("#version 330 core\n\nuniform usampler2DArray smp;\n\nout uint out_color;\n\n"
|
||||
"void main()\n{\n uint result = ") +
|
||||
routine.callText + "." + kChannels[channel] + ";\n\n out_color = result;\n}\n";
|
||||
}
|
||||
|
||||
class SwizzleAccessRoutineScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
// 1x1x1 RGBA32UI 2D array. Integer textures are not filterable, so NEAREST is
|
||||
// mandatory, and a single level means every LOD argument must resolve to 0.
|
||||
glGenTextures(1, &m_sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA32UI, 1, 1, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 1, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT,
|
||||
kSourceTexel);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
|
||||
|
||||
// 8x8 R32UI render target, read back with glReadPixels.
|
||||
glGenTextures(1, &m_outputTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, kOutputWidth, kOutputHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
|
||||
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void SetSwizzle(GLenum r, GLenum g, GLenum b, GLenum a) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_R, static_cast<GLint>(r));
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_G, static_cast<GLint>(g));
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_B, static_cast<GLint>(b));
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_A, static_cast<GLint>(a));
|
||||
}
|
||||
|
||||
// Renders one access routine into the 8x8 target and returns every texel it wrote.
|
||||
// Returns an empty vector (with a gtest failure already recorded) if the program did
|
||||
// not build.
|
||||
std::vector<std::uint32_t> Render(const AccessRoutine& routine, int channel) {
|
||||
const std::string fragment = FragmentSource(routine, channel);
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << routine.name << " channel " << kChannels[channel]
|
||||
<< ": program did not build: " << error << "\n--- source ---\n"
|
||||
<< fragment;
|
||||
return {};
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
const GLuint clearValue[4] = {0xDEADBEEFu, 0u, 0u, 0u};
|
||||
glClearBufferuiv(GL_COLOR, 0, clearValue);
|
||||
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
||||
const GLint location = glGetUniformLocation(program, "smp");
|
||||
glUniform1i(location, 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
std::vector<std::uint32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
// Asserts every texel equals `expected`, naming the routine and the first offender.
|
||||
void ExpectAllTexels(const AccessRoutine& routine, int channel, std::uint32_t expected,
|
||||
const std::vector<std::uint32_t>& texels) {
|
||||
if (texels.empty()) return;
|
||||
std::size_t offenders = 0;
|
||||
std::uint32_t firstBad = 0;
|
||||
std::size_t firstIndex = 0;
|
||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
||||
if (texels[i] == expected) continue;
|
||||
if (offenders == 0) {
|
||||
firstBad = texels[i];
|
||||
firstIndex = i;
|
||||
}
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u)
|
||||
<< routine.name << "(...)." << kChannels[channel] << " returned 0x" << std::hex << firstBad
|
||||
<< " instead of 0x" << expected << std::dec << " at texel " << firstIndex << " (" << offenders
|
||||
<< " of " << texels.size() << " wrong)";
|
||||
}
|
||||
|
||||
GLuint m_sourceTexture = 0;
|
||||
GLuint m_outputTexture = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Identity swizzle: every routine must fetch the channel it was asked for. This is the
|
||||
// scenario's floor - it does not involve swizzling at all, so a failure here is purely about
|
||||
// how the access routine itself survives the trip to the backend.
|
||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineFetchesTheSameTexelUnderTheIdentitySwizzle) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
SetSwizzle(GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
for (const AccessRoutine& routine : kRoutines) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
||||
ExpectAllTexels(routine, channel, kSourceTexel[channel], texels);
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// A real swizzle, applied to every routine. Reversing the channels means a routine that
|
||||
// silently drops the swizzle returns the UNSWIZZLED texel rather than nothing, so the
|
||||
// failure distinguishes "swizzle lost" from "fetch broken".
|
||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineSeesAReversedSwizzle) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
SetSwizzle(GL_ALPHA, GL_BLUE, GL_GREEN, GL_RED);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::uint32_t expected[4] = {kSourceTexel[3], kSourceTexel[2], kSourceTexel[1], kSourceTexel[0]};
|
||||
for (const AccessRoutine& routine : kRoutines) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
||||
ExpectAllTexels(routine, channel, expected[channel], texels);
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// Program churn: the shape that made the conformance suite fail, reduced.
|
||||
//
|
||||
// The swizzle smoke test builds one program per swizzle combination - 1,296 per case - and
|
||||
// DirectGLES created a driver shader object per attached shader without ever calling
|
||||
// glDeleteShader. glDeleteShader only FLAGS a shader for deletion (the driver frees it once
|
||||
// nothing has it attached), so without that call the program's own deletion could not free
|
||||
// them either: eight cases left ~20,000 live driver shaders behind, the Adreno ES driver
|
||||
// passed its ceiling, and it began mis-serving shaders - first the sampling variants with the
|
||||
// most image operands (textureLod/texelFetch/*Offset), while plain texture/textureGrad still
|
||||
// worked. On device this loop plus a value check is the whole defect.
|
||||
//
|
||||
// HONEST LIMIT OF THIS TEST: llvmpipe has no such ceiling, so this passes here whether or not
|
||||
// the leak is present - it cannot fail on the CI lane. It is a standing guard for the SHAPE
|
||||
// (build many programs, keep reading the right texel) and the place to raise the iteration
|
||||
// count if a driver ceiling ever needs reproducing; the leak itself is pinned by device
|
||||
// measurement (VmRSS flat at ~137 MB across the 32-case family, against 132 -> 154 MB and
|
||||
// still climbing before the fix).
|
||||
TEST_F(SwizzleAccessRoutineScenario, RepeatedProgramBuildsKeepFetchingTheSameTexel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
SetSwizzle(GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// One routine from each side of the device's failure order, so a ceiling that takes the
|
||||
// vulnerable one down first is still caught.
|
||||
const AccessRoutine& plain = kRoutines[0]; // texture
|
||||
const AccessRoutine& explicitLod = kRoutines[1]; // textureLod
|
||||
constexpr int kIterations = 200;
|
||||
|
||||
for (int i = 0; i < kIterations; ++i) {
|
||||
const AccessRoutine& routine = (i % 2 == 0) ? plain : explicitLod;
|
||||
const int channel = i % 4;
|
||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
||||
if (::testing::Test::HasFailure()) return; // a build failure repeats 200 times; say it once
|
||||
ExpectAllTexels(routine, channel, kSourceTexel[channel], texels);
|
||||
if (::testing::Test::HasFailure()) {
|
||||
ADD_FAILURE() << "diverged at iteration " << i << " of " << kIterations;
|
||||
return;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the churn loop left a GL error behind";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// GL_ONE and GL_ZERO, which the conformance table spells as the literal values 1 and 0 and
|
||||
// which the backend has to synthesise rather than fetch.
|
||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineSeesConstantSwizzleSources) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
SetSwizzle(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::uint32_t expected[4] = {1u, 0u, 1u, 0u};
|
||||
for (const AccessRoutine& routine : kRoutines) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
||||
ExpectAllTexels(routine, channel, expected[channel], texels);
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user