[Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan

This commit is contained in:
2026-08-12 16:11:10 -04:00
parent 0b36621069
commit 2b46a3db96
15 changed files with 1286 additions and 9 deletions
+1
View File
@@ -284,6 +284,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
+10 -3
View File
@@ -1313,10 +1313,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); }
// Whether glBindImageTexture's `layered` means anything for this target - asked of the
// target the DRIVER will see, not the one the application named. A GL_TEXTURE_1D_ARRAY
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
// the state target instead answered "no" for it and pinned every 1D-array image binding
// to layer 0, whatever the application passed.
static Bool SupportsLayeredImageBinding(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray ||
target == TextureTarget::Texture2DMultisampleArray;
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
backendTarget == TextureTarget::Texture2DArray ||
backendTarget == TextureTarget::TextureCubeMapArray ||
backendTarget == TextureTarget::Texture2DMultisampleArray;
}
void SyncImageTextureBinding(Uint unit) {
@@ -4705,6 +4705,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &rectLoweredSpirv;
}
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
// as 2D for images without ever asking whether the type is arrayed, so a
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
// two-component constructor, which every driver rejects, taking the whole
// program with it. The pass does the conversion properly - type to 2D array,
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
Vector<unsigned int> arrayImageSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
arrayImageSpirv) &&
!arrayImageSpirv.empty()) {
effectiveSpirv = &arrayImageSpirv;
}
// GLSL ES demands a constant integral expression to index a fragment output
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
@@ -1721,6 +1721,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
return ProgramFactory::DescriptorBindingKind::StorageTexelBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER:
return ProgramFactory::DescriptorBindingKind::StorageBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE:
@@ -1750,6 +1752,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler ||
kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
const auto arraySuffix = name.find("[0]");
if (arraySuffix != String::npos) {
@@ -1839,8 +1842,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// UniformManager::BindProgramUniformBuffers: UBO instance arrays
// (uniform Block {...} b[N];), storage-block instance arrays, image uniform
// arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
// Anything else - a uniform TEXEL buffer array is the one remaining kind -
// must fail program creation cleanly rather than continue with corrupt state.
// Anything else - the two TEXEL buffer kinds are what remain, samplerBuffer[N]
// and imageBuffer[N] - must fail program creation cleanly rather than continue
// with corrupt state. Their per-draw path writes pTexelBufferView as the
// address of a vector element sized for one descriptor per binding, so an
// array would not merely be unresolved, it would dangle.
//
// Getting listed here is not cosmetic: a kind that is rejected leaves
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
@@ -2661,6 +2667,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type);
if (descriptorKind != DescriptorBindingKind::CombinedImageSampler &&
descriptorKind != DescriptorBindingKind::UniformTexelBuffer &&
descriptorKind != DescriptorBindingKind::StorageTexelBuffer &&
descriptorKind != DescriptorBindingKind::StorageImage &&
descriptorKind != DescriptorBindingKind::StorageBuffer) {
continue;
@@ -2790,6 +2797,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (descriptorKind == DescriptorBindingKind::StorageTexelBuffer) {
// Only the declared format is recorded, and only so the per-draw resolve can
// prefer it over the one glBindImageTexture named. Everything the StorageImage
// branch above does about ARRAYS is deliberately absent: an imageBuffer array
// is refused outright by the array gate in RemapDescriptorBindingsForVulkan,
// exactly as a samplerBuffer array is, so bindingDescriptorCounts stays at the
// default 1 and the descriptor write below may take the address of a vector
// element without reserving room for extra elements.
const VkFormat reflectedFormat =
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
reflectedFormat == VK_FORMAT_UNDEFINED ||
existingFormat == reflectedFormat,
"ProgramFactory::ReflectLayout: storage texel buffer binding %u ('%s') "
"has conflicting reflected formats (%d vs %d)",
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
static_cast<Int>(reflectedFormat));
if (existingFormat == VK_FORMAT_UNDEFINED) {
existingFormat = reflectedFormat;
}
}
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
@@ -2867,6 +2897,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.dynamicBindings.push_back(binding);
} else if (kind == DescriptorBindingKind::UniformTexelBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
} else if (kind == DescriptorBindingKind::StorageTexelBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
} else if (kind == DescriptorBindingKind::StorageBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
} else if (kind == DescriptorBindingKind::StorageImage) {
@@ -33,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
StorageImage,
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
// DescriptorKeyHash mixes the enumerator's value.
StorageTexelBuffer
};
enum class CompileOptionBit : Uint {
@@ -103,6 +109,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
// the other, never both, and both need exactly the same thing - the format the
// shader declared, so the per-draw resolve can tell a typed declaration from a
// formatless one. Kept as one pair rather than two so the move operations below
// cannot drift out of sync with a field that only one kind populates.
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding;
@@ -743,6 +743,142 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather
// than a VkImageView, so it is half ResolveStorageImageDescriptor (the resource comes from an
// IMAGE unit, i.e. from glBindImageTexture, not from a texture unit) and half
// ResolveTexelBufferDescriptor (the descriptor is a buffer view over the GL buffer the
// texture is attached to).
//
// Before this existed the descriptor kind reflected as SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_-
// TEXEL_BUFFER and fell into ReflectDescriptorTypeToBindingKind's `default:`, whose only
// complaint is an assert that compiles out above DEBUG - so a release build declared no
// binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline
// creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the
// case that carries it.
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 frameIndex,
VkBufferView& outBufferView) {
outBufferView = VK_NULL_HANDLE;
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
"ResolveStorageTexelBufferDescriptor: frame index out of range");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveStorageTexelBufferDescriptor: binding %u out of range", binding);
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
programObj.samplerNameByBinding[binding].c_str());
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
binding);
return false;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
const auto& texture = imageBinding.Texture;
if (texture == nullptr) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
binding);
return false;
}
if (texture->GetStorageType() != TextureStorageType::Buffer ||
texture->GetTarget() != TextureTarget::TextureBuffer) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
"unit %d, got textureId=%u target=%d storage=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
static_cast<Int>(texture->GetStorageType()));
return false;
}
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
imageUnit);
return false;
}
// Unlike the sampled texel buffer, the shader may WRITE this one, and those writes land in
// GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
// GetBufferSubData read. Same two calls, and for the same reason, as the storage-block
// path above.
bufferObject->EnsureGpuResidentStorage();
bufferObject->MarkGpuWritten();
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
!slice.IsValid()) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
return false;
}
// The format the SHADER declared wins over the one glBindImageTexture named, on the same
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
// r32ui whatever the texture's own attachment format says. Falling back, in order:
// reflected format, then the bind format, then the texture's attached format.
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
const auto internalFormat = textureBuffer->GetFormat();
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
VkFormat vkFormat = reflectedFormat;
if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) {
vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
}
if (vkFormat == VK_FORMAT_UNDEFINED) {
vkFormat = resourceFormat;
}
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
static_cast<Int>(internalFormat), imageBinding.Format);
return false;
}
// Sized against the VIEW's format, not the texture's attached one - they may differ by the
// paragraph above, and a range that is not a whole number of the view's texels is invalid.
const VkDeviceSize texelSize =
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
if (texelSize > 0) {
viewRange = (viewRange / texelSize) * texelSize;
}
if (viewRange == 0) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
texture->GetExternalIndex());
return false;
}
VkBufferViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
viewInfo.buffer = slice.buffer;
viewInfo.format = vkFormat;
viewInfo.offset = slice.offset + rangeOffset;
viewInfo.range = viewRange;
VkBufferView bufferView = VK_NULL_HANDLE;
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
return false;
}
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
outBufferView = bufferView;
return true;
}
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
@@ -1267,7 +1403,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
VkDescriptorPoolSize poolSizes[5]{};
VkDescriptorPoolSize poolSizes[6]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
poolSizes[0].descriptorCount = descriptorCount;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
@@ -1278,6 +1414,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
poolSizes[3].descriptorCount = descriptorCount;
poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
poolSizes[4].descriptorCount = descriptorCount;
poolSizes[5].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
poolSizes[5].descriptorCount = descriptorCount;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
@@ -1644,6 +1782,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer) {
// Shares texelBufferViews with the sampled kind above, and may do so safely for
// the same reason: neither kind can be an array, so each contributes exactly one
// element and the reserve of m_maxBindings cannot be outrun - which is what keeps
// the &back() below from dangling when a later binding pushes.
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
bufferView == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
"has no valid descriptor",
binding);
return false;
}
texelBufferViews.push_back(bufferView);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
// instance array occupies a single binding whose elements each come from their
@@ -175,6 +175,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
// block. Each element resolves through its own GL storage block, and so its own GL
// binding point, buffer and glBindBufferRange window.
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
// invalid unless the buffer was created with this bit. Nothing asked for it until
// imageBuffer support existed, so the omission was invisible.
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
@@ -714,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
// the two it is only becomes known when a shader that uses it is bound - long after
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
// invalid unless the buffer was created with the storage bit, so a buffer that
// acquired only the uniform bit could never be given one.
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect:
@@ -69,6 +69,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
@@ -0,0 +1,514 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.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 - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
//
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
// That is what it did here, on both backends, for two waves.
//
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
// ONE texture and checks the value that comes back, so a failure names the target kind and the
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
// bottom still does - a defect that only appears when several kinds share a program is invisible
// to the single-kind cases by construction.
//
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
//
// * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
// * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
// binding, with the image unit then assigned by glUniform1i. That combination is the one ES
// cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
// has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
// * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
// frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
// for a reason the conformance case excludes;
// * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
// to pass, and which is where a target kind whose layeredness a backend does not recognise
// goes wrong.
//
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
// conformance case never asks it for a multisample image at all. The two cases below are kept
// and skip on that same query, so the coverage is already written the day a backend advertises
// them - and so the skip is a standing record of WHY the conformance case passes without them.
#include <algorithm>
#include <cstddef>
#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 case's own dimensions: one level, 6 on every axis (which is also
// exactly one cube's worth for a cube array), and a single texel read at the origin.
constexpr int kExtent = 6;
constexpr GLuint kFilledValue = 7u;
constexpr GLuint kStoredValue = 13u;
// Everything that differs between the eleven kinds, in one row.
struct TargetKind {
const char* name; // this scenario's name for it, which failure messages carry
GLenum target; // the GL texture target
const char* imageType; // the GLSL image uniform type
const char* coord; // the coordinate expression imageLoad/imageStore takes
bool multisample; // needs GL_MAX_IMAGE_SAMPLES > 0
bool buffer; // storage comes from a buffer object, not TexStorage
};
constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
false};
constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
false};
constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
"ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
"ivec3(0, 0, 0)", true, false};
// A multisample image load/store takes the sample index as an extra argument; no other
// kind does. Keeping that in one place stops the two spellings drifting apart.
std::string LoadExpression(const TargetKind& kind, const std::string& name) {
return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
}
std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
value + ", 0, 0, 0));";
}
const char* kComputePrologue = "#version 440 core\n"
"\n"
"layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
"\n";
const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
" uint sum;\n"
"} ssb;\n"
"\n";
// The conformance case's shader, narrowed to a single image.
std::string SingleLoadSource(const TargetKind& kind) {
return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
" i0;\n" + kResultBlock + "void main()\n{\n uvec4 v = " + LoadExpression(kind, "i0") +
";\n ssb.sum = v.r;\n}\n";
}
// The other direction. Written as its own program rather than a read-write one so that a
// backend which gets the store right and the load wrong (or the reverse) is not able to
// cancel its own defect out.
std::string SingleStoreSource(const TargetKind& kind) {
return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
}
class ImageTargetKindScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
m_programs.clear();
m_textures.clear();
m_buffers.clear();
// Leave no image unit bound. These scenarios share one context, and a stale image
// binding is exactly the kind of state that makes the NEXT scenario's failure
// impossible to reproduce on its own.
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
}
// The conformance case's own multisample gate, asked the same way it asks it.
bool MultisampleImagesAreUsable() const {
GLint maxImageSamples = 0;
glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageSamples > 0;
}
GLuint MakeComputeProgram(const std::string& source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log << "\nsource:\n" << source;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log << "\nsource:\n" << source;
return 0;
}
return program;
}
// Storage plus a full fill with `value`, in the spelling each target kind needs.
// Returns 0 - having already reported - when the target could not be created.
GLuint MakeTexture(const TargetKind& kind, bool fill) {
const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, kFilledValue);
if (kind.buffer) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
m_buffers.push_back(buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
<< GLErrorName(error);
return 0;
}
return texture;
}
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(kind.target, texture);
switch (kind.target) {
case GL_TEXTURE_1D:
glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
break;
case GL_TEXTURE_2D:
case GL_TEXTURE_RECTANGLE:
case GL_TEXTURE_1D_ARRAY:
case GL_TEXTURE_CUBE_MAP:
glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
break;
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP_ARRAY:
glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
break;
case GL_TEXTURE_2D_MULTISAMPLE:
glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
break;
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
break;
default:
ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
return 0;
}
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
return 0;
}
// A multisample texture has no TexSubImage - the conformance case fills it with a
// compute pass, which is what the store cases below do.
if (!fill || kind.multisample) return texture;
switch (kind.target) {
case GL_TEXTURE_1D:
glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
break;
case GL_TEXTURE_2D:
case GL_TEXTURE_RECTANGLE:
case GL_TEXTURE_1D_ARRAY:
glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
texels.data());
break;
case GL_TEXTURE_CUBE_MAP:
for (int face = 0; face < 6; ++face) {
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
}
break;
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP_ARRAY:
glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
GL_UNSIGNED_INT, texels.data());
break;
default:
break;
}
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
return 0;
}
return texture;
}
// A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
GLuint MakeResultBuffer() {
GLuint ssbo = 0;
glGenBuffers(1, &ssbo);
m_buffers.push_back(ssbo);
const GLuint zero = 0u;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
return ssbo;
}
GLuint ReadResult(GLuint ssbo) {
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
GLuint value = 0xFFFFFFFFu;
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
return value;
}
// Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
// compute dispatch, and require the value back.
void RunLoadCase(const TargetKind& kind) {
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
if (program == 0) return;
const GLuint texture = MakeTexture(kind, true);
if (texture == 0) return;
const GLuint ssbo = MakeResultBuffer();
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
glUseProgram(program);
// The unit, by LOCATION - the conformance case's own redundant-but-legal
// assignment, and the one ES cannot take at the API level.
glUniform1i(0, 0);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
<< kind.name << ": the compute dispatch did not read the value the texture was filled with";
glUseProgram(0);
}
// The other direction: store through an image uniform, then read the same texel back
// through a SECOND program, so a defect cannot cancel itself out.
void RunStoreCase(const TargetKind& kind) {
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
if (storeProgram == 0 || loadProgram == 0) return;
const GLuint texture = MakeTexture(kind, false);
if (texture == 0) return;
const GLuint ssbo = MakeResultBuffer();
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
glUseProgram(storeProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";
glUseProgram(loadProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kStoredValue)
<< kind.name << ": the value stored through the image did not come back";
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
std::vector<GLuint> m_buffers;
};
} // namespace
// ---- the load direction, one target kind per case -----------------------
//
// Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
// failure names the kind.
#define MGL_DEFINE_LOAD_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, Loads##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunLoadCase(Kind); \
}
#define MGL_DEFINE_STORE_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, Stores##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunStoreCase(Kind); \
}
MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)
MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)
#undef MGL_DEFINE_LOAD_CASE
#undef MGL_DEFINE_STORE_CASE
// ---- and all of them at once -------------------------------------------
//
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
// needs several kinds in one program - a binding remap that only collides when two image
// types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
// - and that class of defect is precisely what "each kind passes alone but the case still
// fails" would mean. Each unit is filled with its own INDEX rather than a constant, so the
// sum names how many units contributed and a single mis-bound unit does not cancel out.
TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
const bool multisample = MultisampleImagesAreUsable();
std::vector<TargetKind> kinds{kKind1D, kKind1DArray, kKind2D, kKind2DArray,
kKind3D, kKindBuffer, kKindCube, kKindCubeArray,
kKindRect};
if (multisample) {
kinds.push_back(kKind2DMS);
kinds.push_back(kKind2DMSArray);
}
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
while (glGetError() != GL_NO_ERROR) {
}
const std::size_t count =
std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
maxImageUnits))));
if (count == 0) GTEST_SKIP() << "no image units";
kinds.resize(count);
std::string declarations;
std::string sum;
for (std::size_t i = 0; i < kinds.size(); ++i) {
const std::string name = "i" + std::to_string(i);
declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
kinds[i].imageType + " " + name + ";\n";
if (!sum.empty()) sum += " + ";
sum += LoadExpression(kinds[i], name);
}
const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
"void main()\n{\n uvec4 v = " + sum + ";\n ssb.sum = v.r;\n}\n";
const GLuint program = MakeComputeProgram(source);
if (program == 0) return;
// Each unit gets its own value, so the sum says how many units contributed.
GLuint expected = 0;
for (std::size_t i = 0; i < kinds.size(); ++i) {
const GLuint texture = MakeTexture(kinds[i], true);
if (texture == 0) return;
expected += kFilledValue;
glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
}
const GLuint ssbo = MakeResultBuffer();
glUseProgram(program);
for (std::size_t i = 0; i < kinds.size(); ++i) {
glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
}
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), expected)
<< "the sum over " << kinds.size()
<< " image target kinds is wrong; each kind contributes " << kFilledValue
<< ", so the shortfall is a whole number of kinds that read zero";
glUseProgram(0);
}
} // namespace MGITest
@@ -21,6 +21,7 @@
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
@@ -3481,3 +3482,162 @@ void main() { fragColor = vec4(texelFetch(Data, 3)); }
<< essl320;
}
namespace {
// OpTypeImage words: result id (+1), sampled type (+2), Dim (+3), Depth (+4), Arrayed (+5),
// MS (+6), Sampled (+7). Dim::Dim1D == 0, and Sampled == 2 is a storage image.
SizeT Count1DArrayStorageImageTypes(const Vector<Uint32>& spirv) {
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
SizeT count = 0;
for (SizeT i = 5; i < spirv.size();) {
const unsigned wordCount = spirv[i] >> 16;
const unsigned opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D && spirv[i + 5] == 1u &&
spirv[i + 7] == 2u) {
++count;
}
i += wordCount;
}
return count;
}
const char* k1DArrayImageCompute = R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; }
)";
} // namespace
// The negative control, and the whole reason the pass exists: SPIRV-Cross's ES emulation of 1D
// images does not ask whether the type is arrayed, so it wraps an already-two-component
// coordinate in a two-component constructor. Pinning the upstream behaviour here means that if a
// future SPIRV-Cross bump fixes it, this test fails and says so, rather than the pass quietly
// becoming dead weight.
TEST_F(ProgramUtilTest, SpirvCrossEmitsAMalformedCoordinateFor1DArrayImages) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
<< "glslang no longer emits a Dim1D/Arrayed/Sampled=2 image for uimage1DArray";
const String essl = DecompileToEssl(spirv);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("ivec2(ivec2("), String::npos)
<< "SPIRV-Cross is expected to emit ivec2(ivec2(...), 0) here - three components in a "
"two-component constructor, which every ES driver rejects. If this no longer happens, "
"Lower1DArrayImagesForEssl may no longer be needed:\n"
<< essl;
}
// The fix: the type becomes a 2D array and the coordinate becomes three components, so
// SPIRV-Cross's 1D path never fires and the emitted ESSL is something a driver accepts.
TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> raw = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(raw.empty());
// Through the shared chain first, exactly as the DirectGLES transpile path does: the pass
// runs on sanitized bytes, and the explicit uniform LOCATION this fixture carries (the
// conformance case's own spelling) is illegal on UniformConstant storage until
// StripUniformLocationsPass has removed it. Validating raw glslang output would latch that
// pre-existing property against this pass.
Vector<Uint32> spirv;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
<< "the shared chain must leave the 1D-array image for this pass to handle";
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
<< "no 1D-array storage image type may survive the pass:\n"
<< DisassembleSpirv(lowered);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the lowered module must stay validator-clean";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2DArray"), String::npos)
<< "the image must be declared as the 2D array the texture is stored as:\n" << essl;
EXPECT_EQ(essl.find("ivec2(ivec2("), String::npos)
<< "the malformed constructor must be gone:\n" << essl;
EXPECT_NE(essl.find("ivec3("), String::npos)
<< "the coordinate must have been widened to three components:\n" << essl;
}
// Scope, half one: a NON-arrayed 1D storage image is emitted correctly by the very same
// SPIRV-Cross code, so the pass must not touch it - replacing working emission with our own buys
// nothing and risks everything.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, 2).r; }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D "), String::npos)
<< "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl;
}
// Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check
// `arrayed` and does move the layer into the third component. The pass is storage-image only.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
uniform sampler1DArray uTex;
in vec2 vUv;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vUv); }
)",
GL_FRAGMENT_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
}
// The declined shape. After the rewrite the image is a 2D array, so a size query on it yields
// three components where the shader consumes two, and there is no correct two-component answer to
// substitute - the ES texture genuinely has a height the GL one does not. The module is handed
// back untouched rather than half-translated.
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeclinesAModuleThatQueriesTheImageSize) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(Lower1DArrayImagesPass::BinaryQueriesA1DArrayStorageImageSize(spirv))
<< "the fixture must contain the shape the pass declines";
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
<< "declining means the 1D-array type is still there for the driver to reject";
}
@@ -26,6 +26,7 @@
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/PrivateToEntryLocalPass.h"
#include "SpirvPasses/StripUniformLocationsPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -824,6 +825,34 @@ namespace MobileGL {
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
}
bool ShaderCompiler::Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
// Declined rather than half-translated: after the rewrite the image is a 2D
// array, so a size query on it yields three components where the shader consumes
// two. Handing back a differently-shaped size silently is worse than leaving the
// module alone and letting the driver say what it does not like - and unlike the
// access path there is no correct answer to substitute, because the ES texture
// genuinely has a height the GL one does not.
//
// MGLOG_I, deliberately: MGLOG_E/W are compiled out at the INFO level every CI,
// retrace and release build uses, and this is exactly the diagnostic that has to
// survive to explain the shader the driver is about to reject.
if (Lower1DArrayImagesPass::BinaryQueriesA1DArrayStorageImageSize(inputBinary)) {
MGLOG_I("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D-array "
"storage image, which cannot be answered in the 2D-array shape ES stores it in; "
"leaving the module alone, and a strict ES driver will reject it");
outputBinary = inputBinary;
return true;
}
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass());
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary);
}
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -89,6 +89,14 @@ namespace MobileGL {
// size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
// what it declines and why.
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
// GL_TEXTURE_1D_ARRAY storage images rewritten to the 2D-array shape the texture
// is actually stored in on ES, with the layer moved from the coordinate's second
// component to its third. DirectGLES transpile path only - Vulkan binds a real
// VK_IMAGE_VIEW_TYPE_1D_ARRAY and must see the module unchanged. Copies the input
// through untouched when the module declares no such image, which is every shader
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
@@ -0,0 +1,247 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.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
#include "Lower1DArrayImagesPass.h"
#include "spirv.hpp"
#include "source/opt/build_module.h"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_builder.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/opt/type_manager.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include <memory>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::InstructionBuilder;
using spvtools::opt::IRContext;
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
// 5 Sampled, 6 Format.
constexpr uint32_t kDimOperand = 1;
constexpr uint32_t kArrayedOperand = 3;
constexpr uint32_t kSampledOperand = 5;
// A 1D image that is arrayed AND is a storage image. Sampled == 2 is SPIR-V's
// "used without a sampler", i.e. exactly the image uniforms this pass exists for;
// Sampled == 1 (a sampled image) reaches SPIRV-Cross's sampler path, which
// already handles the 1D-array shape correctly and must be left to it.
bool Is1DArrayStorageImageType(const Instruction* imageType) {
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
imageType->NumInOperands() > kSampledOperand &&
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) == spv::Dim::Dim1D &&
imageType->GetSingleWordInOperand(kArrayedOperand) == 1u &&
imageType->GetSingleWordInOperand(kSampledOperand) == 2u;
}
// Any Dim1D image, sampled or storage. Used only to decide whether the Image1D
// capability is still needed - deliberately wider than the rewrite's own
// predicate, so a module that also holds a non-arrayed 1D image (which this pass
// leaves to SPIRV-Cross) keeps the capability it still requires.
bool IsDim1DImageType(const Instruction* imageType) {
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
imageType->NumInOperands() > kSampledOperand &&
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) == spv::Dim::Dim1D;
}
// The OpTypeImage behind whatever an image operation was handed - a bare image,
// or a pointer to one. Same unwrapping as NormalizeRectCoordinatesPass, minus the
// sampled-image case a storage image never has.
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
auto* defUseMgr = context->get_def_use_mgr();
Instruction* object = defUseMgr->GetDef(objectId);
if (object == nullptr) return nullptr;
Instruction* type = defUseMgr->GetDef(object->type_id());
while (type != nullptr) {
switch (type->opcode()) {
case spv::Op::OpTypeImage:
return type;
case spv::Op::OpTypeSampledImage:
case spv::Op::OpTypePointer:
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
// Each names its element type in its last in-operand, except arrays,
// whose element type is the FIRST. Both are reached here because an
// image uniform may be declared as an array of images.
type = defUseMgr->GetDef(type->opcode() == spv::Op::OpTypeArray ||
type->opcode() == spv::Op::OpTypeRuntimeArray
? type->GetSingleWordInOperand(0)
: type->GetSingleWordInOperand(type->NumInOperands() - 1));
continue;
default:
return nullptr;
}
}
return nullptr;
}
// The coordinate operand index for the operations that address an image's texels.
// OpImageRead and OpImageTexelPointer take (image, coordinate, ...); OpImageWrite
// takes (image, coordinate, texel).
bool TryGetCoordinateOperand(spv::Op opcode, uint32_t* coordinateOperand) {
switch (opcode) {
case spv::Op::OpImageRead:
case spv::Op::OpImageSparseRead:
case spv::Op::OpImageWrite:
case spv::Op::OpImageTexelPointer:
*coordinateOperand = 1;
return true;
default:
return false;
}
}
bool QueriesImageSize(spv::Op opcode) {
return opcode == spv::Op::OpImageQuerySize || opcode == spv::Op::OpImageQuerySizeLod ||
opcode == spv::Op::OpImageQueryLevels || opcode == spv::Op::OpImageQuerySamples;
}
} // namespace
bool Lower1DArrayImagesPass::BinaryQueriesA1DArrayStorageImageSize(const Vector<Uint32>& binary) {
if (binary.empty()) {
return false;
}
std::unique_ptr<IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
binary.data(), binary.size());
if (!context) {
return false;
}
for (auto& function : *context->module()) {
for (auto& block : function) {
for (auto& instruction : block) {
if (!QueriesImageSize(instruction.opcode()) || instruction.NumInOperands() < 1) {
continue;
}
if (Is1DArrayStorageImageType(
ResolveImageType(context.get(), instruction.GetSingleWordInOperand(0)))) {
return true;
}
}
}
}
return false;
}
spvtools::opt::Pass::Status Lower1DArrayImagesPass::Process() {
auto* irContext = context();
auto* typeMgr = irContext->get_type_mgr();
auto* constantMgr = irContext->get_constant_mgr();
// Nothing to do unless the module actually declares one. Every other shader pays
// one walk of the type table and is handed back unchanged.
bool hasType = false;
for (const Instruction& type : irContext->types_values()) {
if (Is1DArrayStorageImageType(&type)) {
hasType = true;
break;
}
}
if (!hasType) {
return Status::SuccessWithoutChange;
}
spvtools::opt::analysis::Integer signedInt(32, true);
spvtools::opt::analysis::Vector int3(&signedInt, 3);
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&int3);
const uint32_t intTypeId = typeMgr->GetTypeInstruction(&signedInt);
const uint32_t zeroId = constantMgr->GetSIntConstId(0);
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
return Status::Failure;
}
// (u, layer) -> (u, 0, layer). The height the ES 2D array carries is 1, so Y is
// always 0 and the layer has to move from the second component to the third; a
// plain widening that appended the 0 would read layer 0 of every access instead.
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& instruction : block) {
uint32_t coordinateOperand = 0;
if (!TryGetCoordinateOperand(instruction.opcode(), &coordinateOperand) ||
instruction.NumInOperands() <= coordinateOperand) {
continue;
}
if (!Is1DArrayStorageImageType(
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
continue;
}
const uint32_t coordinateId = instruction.GetSingleWordInOperand(coordinateOperand);
InstructionBuilder builder(
irContext, &instruction,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
Instruction* u =
builder.AddCompositeExtract(intTypeId, coordinateId, {0});
Instruction* layer =
builder.AddCompositeExtract(intTypeId, coordinateId, {1});
if (u == nullptr || layer == nullptr) {
return Status::Failure;
}
Instruction* widened = builder.AddCompositeConstruct(
int3TypeId, {u->result_id(), zeroId, layer->result_id()});
if (widened == nullptr) {
return Status::Failure;
}
instruction.SetInOperand(coordinateOperand, {widened->result_id()});
irContext->UpdateDefUse(&instruction);
}
}
}
// Only now, with no access still spelling the 1D-array coordinate, does the type
// become the 2D array one. Arrayed stays 1: this is a 2D ARRAY image, which is
// what the texture was stored as.
for (Instruction& type : irContext->types_values()) {
if (Is1DArrayStorageImageType(&type)) {
type.SetInOperand(kDimOperand, {static_cast<uint32_t>(spv::Dim::Dim2D)});
}
}
// Image1D describes the types just rewritten - but only drop it if no 1D image
// type is left at all. A module may hold a non-arrayed 1D storage image, which
// this pass deliberately leaves to SPIRV-Cross, and that one still needs the
// capability. Shader is always declared by any module reaching here, so restating
// it keeps the instruction valid without leaving a capability a consumer could
// key off.
bool anyDim1DLeft = false;
for (const Instruction& type : irContext->types_values()) {
if (IsDim1DImageType(&type)) {
anyDim1DLeft = true;
break;
}
}
if (!anyDim1DLeft) {
for (Instruction& capability : irContext->capabilities()) {
const auto value = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
if (value == spv::Capability::Image1D) {
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
}
}
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken Lower1DArrayImagesPass::CreateLower1DArrayImagesPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<Lower1DArrayImagesPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,78 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.h
// 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
#pragma once
#include "spirv-tools/optimizer.hpp"
#include "source/opt/pass.h"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// ES has no 1D texture of any kind, so a GL_TEXTURE_1D_ARRAY is stored as an ES 2D
// array with height 1 and the layers in depth (TextureImpl::MapToBackendTextureTarget
// and GetBackendUploadSize, MG_Backend/DirectGLES/Managers.h). The shader side has to
// agree, and for SAMPLERS it does: SPIRV-Cross rewrites a 1D-array lookup into a
// 2D-array one and moves the layer into the third component itself
// (spirv_glsl.cpp, `if (imgtype.image.arrayed) ... ".x, 0.0, " ... ".y"`).
//
// For IMAGES it does not. The image path applies the same 1D emulation without ever
// asking whether the type is arrayed:
//
// if (type.image.dim == Dim1D && options.es)
// coord_expr = join("ivec2(", coord_expr, ", 0)");
//
// For a non-arrayed 1D image that is right - a scalar coordinate becomes (u, 0). For
// a 1D ARRAY image the coordinate is already the two-component (u, layer), so the
// result is `ivec2(ivec2(u, layer), 0)`: three components crammed into a two-component
// constructor. Every ES driver rejects it outright, and the whole program is lost -
// which is how one uimage1DArray uniform took the entire eleven-image compute shader
// of KHR-GL44.multi_bind.dispatch_bind_image_textures down with it, with the driver
// saying only "'constructor' : too many arguments".
//
// Widening the constructor would not be enough either. `ivec3(u, layer, 0)` puts the
// layer in the 2D array's Y and reads layer 0, whereas the storage this has to match
// puts height at 1 and the layers in Z, so the correct coordinate is (u, 0, layer).
//
// So this pass does the whole conversion in the module, before SPIRV-Cross sees it:
// every 1D-array STORAGE image type becomes a 2D-array one, and every read and write
// through it has its coordinate widened from (u, layer) to (u, 0, layer). SPIRV-Cross
// is then looking at an ordinary 2D array image and its 1D path never fires.
//
// Deliberately narrow, on three axes:
//
// * STORAGE images only (Sampled == 2). Sampled images reach SPIRV-Cross's sampler
// path, which is correct today; rewriting them would replace working emission
// with our own for no reason.
// * ARRAYED only. A non-arrayed 1D storage image is emitted correctly by the same
// SPIRV-Cross code, and is left to it.
// * ESSL only. Vulkan has VK_IMAGE_VIEW_TYPE_1D_ARRAY natively and Magma binds it
// directly, so the module must reach that backend unchanged.
//
// A size query on one of these images is DECLINED rather than half-translated: after
// the rewrite OpImageQuerySize yields three components where the shader consumes two,
// and silently handing back a differently-shaped size is worse than refusing. The
// caller logs it and leaves the module alone.
class Lower1DArrayImagesPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-lower-1d-array-images"; }
Status Process() override;
// True when the module declares a 1D-array storage image whose size is queried,
// which is the shape this pass refuses to translate. Checked by the caller before
// running, so a declined module is handed on untouched rather than partly
// rewritten.
static bool BinaryQueriesA1DArrayStorageImageSize(const Vector<Uint32>& binary);
static spvtools::Optimizer::PassToken CreateLower1DArrayImagesPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL