mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc34d34706 | ||
|
|
757b31592d | ||
|
|
dbae4eda10 | ||
|
|
fa5ff5d168 | ||
|
|
994ae372f8 | ||
|
|
7ba012adf9 | ||
|
|
b1fdffd767 | ||
|
|
18c17ae5ca |
@@ -4386,11 +4386,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
log.back() = '\0';
|
log.back() = '\0';
|
||||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||||
m_backendProgramUsable = false;
|
m_backendProgramUsable = false;
|
||||||
|
// Nothing will ever attach this one, so nothing else can free it.
|
||||||
|
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
|
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
|
||||||
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
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());
|
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2578,6 +2578,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
entry.storageBlockNameByBinding[binding] = uniformName;
|
entry.storageBlockNameByBinding[binding] = uniformName;
|
||||||
entry.storageBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Uint32 binding,
|
Uint32 binding, Uint32 element,
|
||||||
VkDescriptorBufferInfo& outBufferInfo) const {
|
VkDescriptorBufferInfo& outBufferInfo) const {
|
||||||
outBufferInfo = {};
|
outBufferInfo = {};
|
||||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
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];
|
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
||||||
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
||||||
binding);
|
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 =
|
const GLuint frontendBinding =
|
||||||
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex));
|
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||||
const Uint32 bindingPointCount =
|
const Uint32 bindingPointCount =
|
||||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
||||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||||
@@ -1416,12 +1419,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
dynamicOffsets.clear();
|
dynamicOffsets.clear();
|
||||||
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
// 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.
|
// 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;
|
Uint32 uboArrayExtra = 0;
|
||||||
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
||||||
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
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);
|
writes.reserve(m_maxBindings);
|
||||||
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
|
bufferInfos.reserve(m_maxBindings + uboArrayExtra + ssboArrayExtra);
|
||||||
imageInfos.reserve(m_maxBindings);
|
imageInfos.reserve(m_maxBindings);
|
||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||||
@@ -1493,18 +1501,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
write.pTexelBufferView = &texelBufferViews.back();
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
||||||
VkDescriptorBufferInfo bufferInfo{};
|
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
||||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, bufferInfo)) {
|
// instance array occupies a single binding whose elements each come from their
|
||||||
MGLOG_E(
|
// own GL binding point.
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u has no valid descriptor",
|
const Uint32 descriptorCount =
|
||||||
binding);
|
binding < programObj.bindingDescriptorCounts.size()
|
||||||
return false;
|
? 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;
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
write.pBufferInfo = &bufferInfos.back();
|
write.descriptorCount = descriptorCount;
|
||||||
|
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||||
VkDescriptorImageInfo imageInfo{};
|
VkDescriptorImageInfo imageInfo{};
|
||||||
|
|||||||
@@ -166,9 +166,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
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,
|
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
VkDescriptorBufferInfo& outBufferInfo) const;
|
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
|||||||
@@ -3016,6 +3016,7 @@ void main() {
|
|||||||
DestroySubmitFencePool();
|
DestroySubmitFencePool();
|
||||||
|
|
||||||
DestroyDeferredDepthMipmapCleanup();
|
DestroyDeferredDepthMipmapCleanup();
|
||||||
|
DestroyMultisampleResolveScratchImage();
|
||||||
DestroyComputePipelines();
|
DestroyComputePipelines();
|
||||||
|
|
||||||
// No sweep runs during teardown, but the observers point at this renderer
|
// No sweep runs during teardown, but the observers point at this renderer
|
||||||
@@ -7194,6 +7195,158 @@ void main() {
|
|||||||
return true;
|
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
|
// 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
|
// 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
|
// 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)) {
|
if (!m_clearManager->GetPendingClear(attachment, payload)) {
|
||||||
return true;
|
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 ||
|
MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr ||
|
||||||
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
||||||
"MaterializePendingClearForDefaultFramebuffer requires no active render pass");
|
"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);
|
const VkImage swapchainImage = m_swapchainObject.GetImage(m_imageIndexAcquired);
|
||||||
if (swapchainImage == VK_NULL_HANDLE) {
|
if (swapchainImage == VK_NULL_HANDLE) {
|
||||||
return false;
|
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;
|
// A clear queued for the destination predates this blit in API order;
|
||||||
// execute it now, or its deferred materialization would later stomp the
|
// execute it now, or its deferred materialization would later stomp the
|
||||||
// copied contents (MC 26.3 OIT clears cloud_depth, then blits the main
|
// copied contents (MC 26.3 OIT clears cloud_depth, then blits the main
|
||||||
// depth into it - the stale loadOp=CLEAR erased the copy).
|
// depth into it - the stale loadOp=CLEAR erased the copy).
|
||||||
const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask);
|
|
||||||
const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType);
|
const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType);
|
||||||
if (auto destTexture = destAttachment.GetTexture(); destTexture != nullptr) {
|
if (auto destTexture = destAttachment.GetTexture(); destTexture != nullptr) {
|
||||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
|
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
|
||||||
@@ -7668,7 +7829,15 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(ok, "%s: failed to transition depth destination image", __func__);
|
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
|
// 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.
|
// depth/stencil blit anyway, and the GL front end already rejects the others.
|
||||||
VkImageBlit blitRegion{};
|
VkImageBlit blitRegion{};
|
||||||
@@ -7684,6 +7853,14 @@ void main() {
|
|||||||
blitRegion.dstSubresource.layerCount = dstBinding.layerCount;
|
blitRegion.dstSubresource.layerCount = dstBinding.layerCount;
|
||||||
blitRegion.dstOffsets[0] = {dstX0, dstY0, 0};
|
blitRegion.dstOffsets[0] = {dstX0, dstY0, 0};
|
||||||
blitRegion.dstOffsets[1] = {dstX1, dstY1, 1};
|
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,
|
vkCmdBlitImage(frame.commandBuffer,
|
||||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_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
|
// 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.
|
// it now, or its deferred materialization would later stomp the blitted color.
|
||||||
const auto& destAttachment = drawFbo->GetAttachment(drawFbo->GetDrawBuffers()[0]);
|
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) {
|
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
|
// 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{};
|
VkImageResolve resolveRegion{};
|
||||||
resolveRegion.srcSubresource = blitRegion.srcSubresource;
|
resolveRegion.srcSubresource = blitRegion.srcSubresource;
|
||||||
resolveRegion.srcOffset = {std::min(srcX0, srcX1), std::min(srcY0, srcY1), 0};
|
|
||||||
resolveRegion.dstSubresource = blitRegion.dstSubresource;
|
resolveRegion.dstSubresource = blitRegion.dstSubresource;
|
||||||
resolveRegion.dstOffset = {std::min(dstX0, dstX1), std::min(dstY0, dstY1), 0};
|
resolveRegion.extent = {resolveWidth, resolveHeight, 1};
|
||||||
resolveRegion.extent = {static_cast<Uint32>(std::abs(srcX1 - srcX0)),
|
if (useScratchResolve) {
|
||||||
static_cast<Uint32>(std::abs(srcY1 - srcY0)), 1};
|
// The scratch copy is a plain single-layer colour image, and the resolve reads the
|
||||||
vkCmdResolveImage(frame.commandBuffer,
|
// SOURCE band the (possibly inverted) transformed region names - taking its min so
|
||||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
// an inverted pair still describes the same band.
|
||||||
dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
resolveRegion.srcOffset = {std::min(blitRegion.srcOffsets[0].x, blitRegion.srcOffsets[1].x),
|
||||||
1, &resolveRegion);
|
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 {
|
} else {
|
||||||
vkCmdBlitImage(frame.commandBuffer,
|
vkCmdBlitImage(frame.commandBuffer,
|
||||||
srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
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,
|
void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y,
|
||||||
GLsizei width, GLsizei height, GLenum format, GLenum type,
|
GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels) {
|
void* pixels) {
|
||||||
if (readFbo.IsDefaultFramebuffer()) {
|
|
||||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: default framebuffer readback is unsupported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -8743,10 +8983,13 @@ void main() {
|
|||||||
// framebuffers lacking either, so resolving via the depth attachment is enough.
|
// framebuffers lacking either, so resolving via the depth attachment is enough.
|
||||||
const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth
|
const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth
|
||||||
: MobileGL::FramebufferAttachmentType::Stencil;
|
: MobileGL::FramebufferAttachmentType::Stencil;
|
||||||
const auto& attachment = readFbo.GetAttachment(attachmentType);
|
const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer();
|
||||||
if (!attachment.IsValid() || attachment.IsEmpty()) {
|
if (!readIsDefaultFbo) {
|
||||||
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
|
const auto& attachment = readFbo.GetAttachment(attachmentType);
|
||||||
return;
|
if (!attachment.IsValid() || attachment.IsEmpty()) {
|
||||||
|
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
@@ -8757,6 +9000,47 @@ void main() {
|
|||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
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;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
|
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
|
||||||
VkImageLayout* trackedLayout = nullptr;
|
VkImageLayout* trackedLayout = nullptr;
|
||||||
@@ -8807,7 +9091,8 @@ void main() {
|
|||||||
void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
||||||
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
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 wantDepth = format != GL_STENCIL_INDEX;
|
||||||
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
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);
|
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__);
|
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]{};
|
VkBufferImageCopy regions[2]{};
|
||||||
Uint32 regionCount = 0;
|
Uint32 regionCount = 0;
|
||||||
if (wantDepth) {
|
if (wantDepth) {
|
||||||
@@ -8881,7 +9181,7 @@ void main() {
|
|||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
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};
|
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||||
}
|
}
|
||||||
if (wantStencil) {
|
if (wantStencil) {
|
||||||
@@ -8891,7 +9191,7 @@ void main() {
|
|||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
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};
|
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||||
}
|
}
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||||
@@ -8916,6 +9216,38 @@ void main() {
|
|||||||
const Uint8* depthSrc = mapped;
|
const Uint8* depthSrc = mapped;
|
||||||
const Uint8* stencilSrc = mapped + stencilOffset;
|
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 {
|
const auto depthValueAt = [&](SizeT i) -> Float {
|
||||||
switch (vkFormat) {
|
switch (vkFormat) {
|
||||||
case VK_FORMAT_D16_UNORM: {
|
case VK_FORMAT_D16_UNORM: {
|
||||||
|
|||||||
@@ -211,10 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||||
// expects command recording to be active and any render pass already ended.
|
// 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,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
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
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
@@ -363,6 +368,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 samplerBinding = 0;
|
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 {
|
struct DeferredDepthMipmapCleanup {
|
||||||
Vector<VkImageView> imageViews;
|
Vector<VkImageView> imageViews;
|
||||||
Vector<VkFramebuffer> framebuffers;
|
Vector<VkFramebuffer> framebuffers;
|
||||||
@@ -1125,6 +1155,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
||||||
MG_State::GLState::FramebufferObject& fbo,
|
MG_State::GLState::FramebufferObject& fbo,
|
||||||
FramebufferAttachmentType attachmentType);
|
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);
|
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::ITextureObject& texture,
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/PixelStoreSweepScenario.cpp
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
Scenarios/FragCoordOriginScenario.cpp
|
Scenarios/FragCoordOriginScenario.cpp
|
||||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackScenario.cpp
|
||||||
|
Scenarios/SsboArrayLengthScenario.cpp
|
||||||
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -180,4 +180,156 @@ void main() { o_color = vec4(0.1, 0.2, 0.3, 1.0); }
|
|||||||
gl.EndFrame();
|
gl.EndFrame();
|
||||||
glDeleteProgram(program);
|
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
|
} // 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