Compare commits

..
9 Commits
Author SHA1 Message Date
swung0x48 6ae3245a0d [Test] (CTS): raise the no-output abort threshold - consecutive instant-crash cases are real progress once device liveness is confirmed 2026-07-26 19:23:05 -04:00
swung0x48 7e048fc2bf [Fix] (DirectVulkan): map RGB10_A2(UI) to A2B10G10R10 - GL 2_10_10_10_REV puts R in bits 0-9 so the A2R10G10B10 mapping silently swapped R/B on upload; also decode both 1010102 variants in readback 2026-07-26 19:13:46 -04:00
swung0x48 83cdfd6bdd [Fix] (DirectVulkan): GetTexImage reads all 3D slices/array layers with PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics, and sRGB readback returns raw sRGB-encoded bytes instead of linearizing 2026-07-26 18:30:43 -04:00
swung0x48 1c76f886cf [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass 2026-07-26 18:30:42 -04:00
swung0x48 a2e109beff [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else 2026-07-26 18:30:41 -04:00
swung0x48 63f0756644 [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt 2026-07-26 18:30:41 -04:00
swung0x48 450215d12c [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) 2026-07-26 18:30:40 -04:00
swung0x48 3a9e520170 [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured 2026-07-26 18:30:39 -04:00
swung0x48 d2996ba1cf [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary 2026-07-26 18:30:39 -04:00
20 changed files with 64 additions and 1310 deletions
-3
View File
@@ -194,9 +194,6 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
-5
View File
@@ -80,11 +80,6 @@ namespace MobileGL::MG_Config {
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
-1
View File
@@ -135,7 +135,6 @@ namespace MobileGL::MG_ConfigLoader {
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
@@ -449,20 +449,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
// Textures attached only to the READ framebuffer (blit / ReadPixels sources) need
// their content synced too, or the backend reads stale texel data.
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (readFBO && readFBO != currentFBO) {
for (const auto& attachment : readFBO->GetAllAttachmentObjects()) {
if (!attachment.IsTexture()) continue;
auto& textureObject = attachment.GetTexture();
if (textureObject) {
SyncTextureObjectToBackend(textureObject);
}
}
}
}
static Bool SupportsLayeredImageBinding(TextureTarget target) {
+2 -104
View File
@@ -1262,31 +1262,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& allAttributes = stateVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
const Uint32 attribBit = 1u << attribIndex;
// An enabled attrib with neither a buffer object nor a client pointer has no
// source; GL tolerates the state (only draws consuming it are undefined), but
// Adreno's ES driver memcpys the "client array" from address 0 at draw time
// (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them
// the moment they gain a source - the mask-vs-current compare below triggers
// the enable even when only the Buffer/Format versions changed.
const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0;
const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0;
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
m_syncedAttributeVersions[attribIndex].SwitchVersion;
if (needsSyncSwitch || unsourceable != wasForceDisabled) {
if (attrib.Enabled && !unsourceable) {
if (needsSyncSwitch) {
if (attrib.Enabled) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
}
}
if (unsourceable) {
m_forceDisabledAttribsMask |= attribBit;
} else {
m_forceDisabledAttribsMask &= ~attribBit;
}
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
m_syncedAttributeVersions[attribIndex].FormatVersion;
@@ -1294,17 +1278,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer) continue;
if (unsourceable) continue;
// Client-side array with a non-null pointer: the pointer is uploaded and applied
// per draw by SyncClientSideAttributesForDrawArrays.
if (!attrib.Buffer) continue;
if (!BindAttributeBuffer(attrib)) {
if (attrib.Enabled) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
m_forceDisabledAttribsMask |= attribBit;
}
continue;
}
@@ -3291,19 +3265,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
// miscompiles non-constant-index writes and constant-index gl_in element reads,
// crashes on whole-array gl_in reads) and cannot dynamically index the global
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
MG_Config::Features.ClipDistanceQuirk;
const Bool applyClipDistanceQuirk =
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
pActiveBackendObject &&
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
for (int index = 0; index < attachedShaders.size(); ++index) {
auto& shader = attachedShaders[index];
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
@@ -3326,63 +3287,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &loweredSpirv;
}
// GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but
// Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array
// index into poison that corrupts the whole shader's output. Clamp every
// access-chain index to its declared bounds before transpiling.
Vector<unsigned int> clampedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv,
clampedSpirv) &&
!clampedSpirv.empty()) {
effectiveSpirv = &clampedSpirv;
} else {
MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V.");
}
// SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates
// to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL
// texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the
// coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)).
Vector<unsigned int> foldedOffsetSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(
*effectiveSpirv, foldedOffsetSpirv) &&
!foldedOffsetSpirv.empty()) {
effectiveSpirv = &foldedOffsetSpirv;
} else {
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
}
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
// ESSL only writes the builtin with literal constant indices (flushed before
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
// indices - the shapes this driver compiles correctly. Must run after the
// access-chain clamp above so the flush indices stay literal constants.
Vector<unsigned int> clipDistanceSpirv;
if (applyClipDistanceQuirk &&
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
*effectiveSpirv, clipDistanceSpirv) &&
!clipDistanceSpirv.empty()) {
effectiveSpirv = &clipDistanceSpirv;
} else {
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
}
}
// Adreno quirk: split single constant-composite stores of struct arrays so
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
// driver cannot dynamically index ("Cannot offset into the structure").
Vector<unsigned int> structLutSpirv;
if (applyClipDistanceQuirk) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
*effectiveSpirv, structLutSpirv) &&
!structLutSpirv.empty()) {
effectiveSpirv = &structLutSpirv;
} else {
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
}
}
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
@@ -3441,12 +3345,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
source = RemoveLayoutBinding(source);
if (applyClipDistanceQuirk) {
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
// ("reserved built-in name") but accepts plain usage with
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
source = RemoveClipDistanceRedeclaration(source);
}
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
@@ -258,11 +258,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
// Attribs the frontend has Enabled but that have no source at all (no buffer object
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
// driver treats them as client arrays and memcpys from address 0 at draw time
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
Uint32 m_forceDisabledAttribsMask = 0;
Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
-35
View File
@@ -342,41 +342,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return result;
}
String RemoveClipDistanceRedeclaration(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
static const std::regex redeclarationRegex(
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
Bool firstLine = true;
while (lineStart <= glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
if (!std::regex_match(line, redeclarationRegex)) {
if (!firstLine) {
result += '\n';
}
result += line;
firstLine = false;
}
if (lastLine) {
break;
}
lineStart = lineEnd + 1;
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
-1
View File
@@ -105,7 +105,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
String RemoveLayoutBinding(const String& glslCode);
String RemoveClipDistanceRedeclaration(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -1910,8 +1910,10 @@ void main() {
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true;
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, true}; return true;
// GL GetTexImage/ReadPixels of sRGB textures return the raw sRGB-encoded
// bytes (GL 3.3 has no FRAMEBUFFER_SRGB read decode) - do NOT linearize.
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8}; return true;
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
// --- SNORM ---
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
@@ -1949,6 +1951,8 @@ void main() {
// --- packed / special ---
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
case VK_FORMAT_R5G6B5_UNORM_PACK16:
@@ -1958,7 +1962,8 @@ void main() {
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
out.sourceClass = format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ?
out.sourceClass = (format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ||
format == VK_FORMAT_A2R10G10B10_UINT_PACK32) ?
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
out.special = format;
return true;
@@ -2034,6 +2039,15 @@ void main() {
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
return;
}
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
rgba[0] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
return;
}
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
@@ -2185,6 +2199,13 @@ void main() {
rgba[1] = (word >> 10) & 0x3FFu;
rgba[2] = (word >> 20) & 0x3FFu;
rgba[3] = (word >> 30) & 0x3u;
} else if (srcFormat == VK_FORMAT_A2R10G10B10_UINT_PACK32) {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = word & 0x3FFu;
rgba[1] = (word >> 10) & 0x3FFu;
rgba[0] = (word >> 20) & 0x3FFu;
rgba[3] = (word >> 30) & 0x3u;
} else {
for (Int c = 0; c < desc.channels; ++c) {
if (desc.componentBits == 8) {
@@ -2216,8 +2237,9 @@ void main() {
}
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels) {
if (width <= 0 || height <= 0) {
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
void* pixels, Bool applyPackImageParams) {
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
return true;
}
@@ -2230,7 +2252,8 @@ void main() {
Vector<Uint8> wide;
GLenum wideType = GL_FLOAT;
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) {
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width,
sliceHeight * sliceCount, wide, wideType)) {
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
static_cast<Int>(srcFormat));
return false;
@@ -2243,9 +2266,9 @@ void main() {
return false;
}
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height,
/*sliceCount=*/1, mapping, type, pixels,
/*applyPackImageParams=*/false);
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight,
sliceCount, mapping, type, pixels,
applyPackImageParams);
}
} // namespace
@@ -6333,7 +6356,8 @@ void main() {
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
sourceTexelSize,
remapped.data())) {
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
/*applyPackImageParams=*/false);
return;
}
}
@@ -6342,7 +6366,8 @@ void main() {
width, height, swapchainExtent.width, swapchainExtent.height,
static_cast<Int>(preTransform));
}
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels,
/*applyPackImageParams=*/false);
}
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
@@ -6395,6 +6420,17 @@ void main() {
if (width <= 0 || height <= 0) {
return;
}
// GetTexImage returns every slice of a 3D level and every layer of an array
// level; GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to the 3D/array
// destination layout (GL 3.3 section 6.1.4).
const auto imageTextureTarget = textureObject->GetTarget();
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
imageTextureTarget == TextureTarget::Texture2DArray ||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format);
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
@@ -6415,7 +6451,8 @@ void main() {
return;
}
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
static_cast<VkDeviceSize>(height) * sourceTexelSize;
static_cast<VkDeviceSize>(height) *
static_cast<VkDeviceSize>(sliceCount) * sourceTexelSize;
VkBufferObject readback;
if (!readback.Create({
.allocator = m_allocator,
@@ -6443,8 +6480,9 @@ void main() {
copyRegion.imageSubresource.aspectMask = resource->aspect;
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = 1;
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
static_cast<Uint32>(depthSlices)};
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), 1, &copyRegion);
@@ -6470,7 +6508,8 @@ void main() {
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
return;
}
PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels);
PackReadbackToClientOrPbo(mapped, resource->format, width, height, sliceCount, format, type, pixels,
/*applyPackImageParams=*/is3dImage || isArrayImage);
}
void VulkanRenderer::GenerateMipmap(GLenum target) {
@@ -133,9 +133,11 @@ namespace MobileGL {
case TextureInternalFormat::RGBA8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10A2:
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
// GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, which is Vulkan's
// A2B10G10R10 layout - A2R10G10B10 silently swaps R and B on upload.
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
case TextureInternalFormat::RGB10A2UI:
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
case TextureInternalFormat::RGBA16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGBA16Snorm:
+3 -7
View File
@@ -9,14 +9,10 @@
#pragma once
#include <Includes.h>
// Severity-ordered: a build compiled at level X keeps X and everything MORE
// severe. INFO builds must keep WARN/ERROR/FATAL — the old ordering
// (WARN=1/ERROR=2 below INFO=3) compiled every warning and error out of
// release builds and hid real backend failures.
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_WARN 1
#define MOBILEGL_LOG_LEVEL_ERROR 2
#define MOBILEGL_LOG_LEVEL_INFO 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
@@ -22,9 +22,6 @@
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "SpirvPasses/StripNoPerspectivePass.h"
#include "SpirvPasses/EmulateNoPerspectivePass.h"
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
@@ -326,55 +323,6 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(CreateGraphicsRobustAccessPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -27,36 +27,6 @@ namespace MobileGL {
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Clamps every access-chain index to its declared bounds (spirv-tools
// GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for
// out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably
// out-of-bounds local-array index into poison that corrupts the whole shader's
// output; clamping restores the "some value from the array" contract. Only for
// the DirectGLES transpile path.
static bool ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer
// coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross
// emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps
// the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int,
// scalar) overload, so Adreno rejects the shader. Only for the DirectGLES
// transpile path.
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
// the decompiled ESSL only writes the builtin with literal constant indices
// (flush before EmitVertex/return) and only reads gl_in clip distances with
// dynamic loop indices (copy loop): the other shapes miscompile or crash
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Splits a Function-storage array-of-structs variable's single constant-composite
// store into per-element stores so SPIRV-Cross does not hoist it into a global
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Drops RelaxedPrecision member decorations from uniform-block structs so
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
// drivers reject cross-stage uniform blocks whose member precisions differ.
@@ -1,175 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 "DefeatConstStructArrayLutPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.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 <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::BasicBlock;
using spvtools::opt::Function;
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
namespace analysis = spvtools::opt::analysis;
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
analysis::Pointer ptr(pointee, sc);
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
}
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
analysis::Integer i(32, true);
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
}
// True when |var| (a Function-storage OpVariable) points to an array of structs.
// Reports the struct type id on success.
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
auto* defUse = ctx->get_def_use_mgr();
Instruction* ptrType = defUse->GetDef(var->type_id());
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
structTypeId = element->result_id();
return true;
}
} // namespace
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
auto* ctx = context();
auto* defUse = ctx->get_def_use_mgr();
bool modified = false;
for (Function& function : *get_module()) {
if (function.begin() == function.end()) continue;
BasicBlock* entryBlock = &*function.begin();
// Candidate variables: Function-storage arrays of structs declared in this
// function's entry block (where OpVariables must live).
struct Candidate {
Instruction* var;
uint32_t structTypeId;
};
std::vector<Candidate> candidates;
for (Instruction& inst : *entryBlock) {
if (inst.opcode() != spv::Op::OpVariable) break;
// Variables with initializers keep SPIRV-Cross's initializer path; the
// glslang pattern under attack is initializer-free with one OpStore.
if (inst.NumInOperands() > 1) continue;
uint32_t structTypeId = 0;
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
candidates.push_back({&inst, structTypeId});
}
}
for (const Candidate& candidate : candidates) {
Instruction* var = candidate.var;
// The variable qualifies only when its single write is one direct
// OpStore of an OpConstantComposite; any other write shape already
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
Instruction* singleStore = nullptr;
bool disqualified = false;
defUse->ForEachUser(var, [&](Instruction* user) {
if (user->opcode() == spv::Op::OpStore &&
user->GetSingleWordInOperand(0) == var->result_id()) {
if (singleStore != nullptr) {
disqualified = true;
} else {
singleStore = user;
}
} else if (user->opcode() == spv::Op::OpCopyMemory) {
disqualified = true;
} else if (user->opcode() == spv::Op::OpAccessChain ||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
defUse->ForEachUser(user, [&](Instruction* chainUser) {
if (chainUser->opcode() == spv::Op::OpStore ||
chainUser->opcode() == spv::Op::OpCopyMemory) {
disqualified = true;
}
});
}
});
if (disqualified || singleStore == nullptr) continue;
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
if (composite == nullptr ||
composite->opcode() != spv::Op::OpConstantComposite) {
continue;
}
// The store must sit in the entry block: that is the only placement
// SPIRV-Cross treats as a LUT initializer.
bool storeInEntryBlock = false;
for (Instruction& inst : *entryBlock) {
if (&inst == singleStore) {
storeInEntryBlock = true;
break;
}
}
if (!storeInEntryBlock) continue;
// Split the composite store into one constant-index store per element.
const uint32_t ptrFnStruct =
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
const uint32_t chainId = ctx->TakeNextId();
Instruction* chain =
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
ctx->AnalyzeDefUse(chain);
Instruction* store =
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpStore, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {chainId}},
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
ctx->AnalyzeDefUse(store);
}
ctx->KillInst(singleStore);
modified = true;
}
}
if (!modified) {
return Status::SuccessWithoutChange;
}
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,36 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
// the one composite store into per-element constant-index stores makes
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
// transpile path on Qualcomm devices.
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "defeat-const-struct-array-lut"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,131 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 "FoldConstOffsetFor1DFetchPass.h"
#include "spirv.hpp"
#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/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::InstructionBuilder;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
// Number of ImageOperands ids that precede the ConstOffset id: one per
// lower-order bit set in the mask, except Grad which carries two ids.
uint32_t CountIdsBeforeConstOffset(uint32_t mask) {
uint32_t count = 0;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Bias)) count += 1;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) count += 1;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Grad)) count += 2;
return count;
}
} // namespace
spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
Bool modified = false;
constexpr uint32_t kConstOffsetBit =
static_cast<uint32_t>(spv::ImageOperandsMask::ConstOffset);
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpImageFetch) continue;
// In-operands: image, coordinate, [ImageOperands mask, ids...].
if (inst.NumInOperands() < 3) continue;
const uint32_t operandsMask = inst.GetSingleWordInOperand(2);
if ((operandsMask & kConstOffsetBit) == 0) continue;
Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
if (imageInst == nullptr) continue;
Instruction* imageType = defUseMgr->GetDef(imageInst->type_id());
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage ||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) {
continue;
}
const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask);
const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex);
const uint32_t coordId = inst.GetSingleWordInOperand(1);
Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id());
InstructionBuilder builder(
irContext, &inst,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
uint32_t newCoordId = 0;
if (coordType->opcode() == spv::Op::OpTypeVector) {
// Arrayed 1D fetch: component 0 is the texel coordinate,
// component 1 the layer - only component 0 takes the offset.
const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0);
Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0});
Instruction* sum =
builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId);
Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeInsert, coordType->result_id(),
irContext->TakeNextId(),
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {sum->result_id()}},
{SPV_OPERAND_TYPE_ID, {coordId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}}));
newCoordId = inserted->result_id();
} else {
Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId);
newCoordId = sum->result_id();
}
const uint32_t newMask = operandsMask & ~kConstOffsetBit;
// 3 fixed operands + the offset id: anything beyond that is another
// image-operand id that must keep the mask word alive.
const Bool otherOperandIdsRemain = inst.NumInOperands() > 4;
irContext->ForgetUses(&inst);
std::vector<Operand> newOperands;
newOperands.push_back(inst.GetInOperand(0));
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}});
if (newMask != 0 || otherOperandIdsRemain) {
Operand maskOperand = inst.GetInOperand(2);
maskOperand.words[0] = newMask;
newOperands.push_back(maskOperand);
for (uint32_t i = 3; i < inst.NumInOperands(); ++i) {
if (i == offsetOperandIndex) continue;
newOperands.push_back(inst.GetInOperand(i));
}
}
inst.SetInOperands(std::move(newOperands));
irContext->AnalyzeUses(&inst);
modified = true;
}
}
}
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}
spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() {
return spvtools::Optimizer::PassToken(MakeUnique<FoldConstOffsetFor1DFetchPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,36 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// SPIRV-Cross emulates 1D textures as 2D for ES targets: it widens the texelFetch
// coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has
// no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers
// (Adreno) reject the transpiled shader. This pass folds the constant offset into
// the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) ==
// texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand,
// so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate
// component 0 is offset (component 1 is the layer). Only meant for the DirectGLES
// transpile path.
class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "fold-const-offset-for-1d-fetch"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,613 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 "LowerClipDistanceForEsslPass.h"
#include "spirv.hpp"
#include "source/opt/basic_block.h"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.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::BasicBlock;
using spvtools::opt::Function;
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
namespace analysis = spvtools::opt::analysis;
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
for (Instruction& ep : ctx->module()->entry_points()) {
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
}
return spv::ExecutionModel::Max;
}
uint32_t EntryFunctionId(IRContext* ctx) {
for (Instruction& ep : ctx->module()->entry_points()) {
// OpEntryPoint <model> <function> "name" <interface...>
return ep.GetSingleWordInOperand(1);
}
return 0;
}
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
// OpTypePointer <storage-class> <pointee>
return ptrType->GetSingleWordInOperand(1);
}
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
analysis::Pointer ptr(pointee, sc);
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
}
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
analysis::Integer i(32, isSigned);
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
}
uint32_t UintType(IRContext* ctx) {
analysis::Integer i(32, false);
return ctx->get_type_mgr()->GetTypeInstruction(&i);
}
uint32_t BoolType(IRContext* ctx) {
analysis::Bool b;
return ctx->get_type_mgr()->GetTypeInstruction(&b);
}
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
return 0;
}
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
return 0;
}
return length->GetSingleWordInOperand(0);
}
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
def->GetSingleWordInOperand(0) == value;
}
bool IsAccessChain(const Instruction* inst) {
return inst->opcode() == spv::Op::OpAccessChain ||
inst->opcode() == spv::Op::OpInBoundsAccessChain;
}
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
const uint32_t varId = ctx->TakeNextId();
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpVariable, ptrType, varId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpName, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {varId}},
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
return ctx->get_def_use_mgr()->GetDef(varId);
}
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
// operands and switching the result pointer's storage class to Private.
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
uint32_t dropIndexCount) {
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
newOperands.push_back(chain->GetInOperand(i));
}
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
}
// ---- Output side --------------------------------------------------------------
struct OutputTarget {
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
bool isBlockMember = false;
uint32_t memberIndex = 0; // valid when isBlockMember
uint32_t arrayTypeId = 0; // float[N]
uint32_t elemTypeId = 0; // float
uint32_t arrayLen = 0; // N
};
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
// |before|. Constant-index writes are the only write shape Adreno links correctly.
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
uint32_t mgVarId) {
const uint32_t ptrPrivElem =
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
const uint32_t ptrOutElem =
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
for (uint32_t k = 0; k < target.arrayLen; ++k) {
const uint32_t kConst = IntConstant(ctx, true, k);
const uint32_t srcChainId = ctx->TakeNextId();
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
{SPV_OPERAND_TYPE_ID, {kConst}}}));
const uint32_t valId = ctx->TakeNextId();
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
const uint32_t dstChainId = ctx->TakeNextId();
std::vector<Operand> dstOperands;
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
if (target.isBlockMember) {
dstOperands.push_back(
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
}
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpStore, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
{SPV_OPERAND_TYPE_ID, {valId}}}));
}
}
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
auto* defUse = ctx->get_def_use_mgr();
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
// standalone variables decorated BuiltIn ClipDistance.
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
std::vector<uint32_t> plainTargets; // variable ids
for (Instruction& ann : ctx->annotations()) {
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
spv::BuiltIn::ClipDistance) {
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
ann.GetSingleWordInOperand(1));
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
spv::BuiltIn::ClipDistance) {
plainTargets.push_back(ann.GetSingleWordInOperand(0));
}
}
std::vector<OutputTarget> targets;
for (Instruction& inst : ctx->module()->types_values()) {
if (inst.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
spv::StorageClass::Output) {
continue;
}
const uint32_t pointee = VariablePointeeType(ctx, &inst);
for (const auto& [structId, member] : memberTargets) {
if (pointee != structId) continue;
Instruction* structType = defUse->GetDef(structId);
if (structType == nullptr || member >= structType->NumInOperands()) continue;
OutputTarget target;
target.var = &inst;
target.isBlockMember = true;
target.memberIndex = member;
target.arrayTypeId = structType->GetSingleWordInOperand(member);
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
targets.push_back(target);
}
for (const uint32_t varId : plainTargets) {
if (inst.result_id() != varId) continue;
OutputTarget target;
target.var = &inst;
target.isBlockMember = false;
target.arrayTypeId = pointee;
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
targets.push_back(target);
}
}
bool changed = false;
for (OutputTarget& target : targets) {
if (target.arrayLen == 0) continue;
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
// Collect the accesses to redirect. For the block form only chains whose
// leading index selects the ClipDistance member count; for the standalone
// form every chain plus whole-variable loads/stores.
std::vector<Instruction*> chains;
std::vector<Instruction*> directAccesses;
bool unsupportedUse = false;
defUse->ForEachUser(target.var, [&](Instruction* user) {
if (IsAccessChain(user) &&
user->GetSingleWordInOperand(0) == target.var->result_id()) {
if (target.isBlockMember) {
if (user->NumInOperands() >= 2 &&
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
target.memberIndex)) {
chains.push_back(user);
}
} else {
chains.push_back(user);
}
} else if (!target.isBlockMember) {
if (user->opcode() == spv::Op::OpLoad ||
(user->opcode() == spv::Op::OpStore &&
user->GetSingleWordInOperand(0) == target.var->result_id())) {
directAccesses.push_back(user);
} else if (user->opcode() == spv::Op::OpCopyMemory) {
unsupportedUse = true;
}
}
});
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
continue;
}
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
const uint32_t mgVarId = mgVar->result_id();
for (Instruction* chain : chains) {
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
if (chain->NumInOperands() == 1 + dropCount) {
// Pointer to the whole float[N]: reuse the private variable itself.
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
ctx->KillInst(chain);
} else {
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
}
}
for (Instruction* access : directAccesses) {
ctx->ForgetUses(access);
access->SetInOperand(0, {mgVarId});
ctx->AnalyzeUses(access);
}
// Flush the shadow into the real builtin: geometry right before every
// EmitVertex, vertex before every return of the entry point. The flush is
// also what keeps the builtin statically used for cross-stage IO matching.
std::vector<Instruction*> flushSites;
if (isGeometry) {
for (Function& function : *ctx->module()) {
function.ForEachInst([&](Instruction* inst) {
if (inst->opcode() == spv::Op::OpEmitVertex) {
flushSites.push_back(inst);
}
});
}
} else {
const uint32_t entryFuncId = EntryFunctionId(ctx);
for (Function& function : *ctx->module()) {
if (function.result_id() != entryFuncId) continue;
function.ForEachInst([&](Instruction* inst) {
if (inst->opcode() == spv::Op::OpReturn ||
inst->opcode() == spv::Op::OpReturnValue) {
flushSites.push_back(inst);
}
});
}
}
for (Instruction* site : flushSites) {
InsertFlushBefore(ctx, site, target, mgVarId);
}
changed = true;
}
return changed;
}
// ---- Input side (geometry gl_in) ----------------------------------------------
bool LowerInputClipDistance(IRContext* ctx) {
auto* defUse = ctx->get_def_use_mgr();
auto* typeMgr = ctx->get_type_mgr();
// Locate the gl_in block member decorated ClipDistance.
Instruction* glInVar = nullptr;
uint32_t memberIndex = 0;
uint32_t arrayTypeId = 0; // float[N]
for (Instruction& ann : ctx->annotations()) {
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
spv::Decoration::BuiltIn ||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
spv::BuiltIn::ClipDistance) {
continue;
}
const uint32_t structId = ann.GetSingleWordInOperand(0);
const uint32_t member = ann.GetSingleWordInOperand(1);
for (Instruction& inst : ctx->module()->types_values()) {
if (inst.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
const uint32_t pointee = VariablePointeeType(ctx, &inst);
Instruction* pointeeType = defUse->GetDef(pointee);
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
pointeeType->GetSingleWordInOperand(0) != structId) {
continue;
}
Instruction* structType = defUse->GetDef(structId);
if (structType == nullptr || member >= structType->NumInOperands()) continue;
glInVar = &inst;
memberIndex = member;
arrayTypeId = structType->GetSingleWordInOperand(member);
break;
}
if (glInVar != nullptr) break;
}
if (glInVar == nullptr) {
return false;
}
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
if (clipCount == 0 || vertexCount == 0) {
return false;
}
// Every gl_in chain that selects the ClipDistance member:
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
std::vector<Instruction*> chains;
defUse->ForEachUser(glInVar, [&](Instruction* user) {
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
user->NumInOperands() >= 3 &&
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
chains.push_back(user);
}
});
if (chains.empty()) {
return false;
}
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
analysis::Array outerArray(
innerType, analysis::Array::LengthInfo{
vertexCountConst,
{analysis::Array::LengthInfo::kConstant, vertexCount}});
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
const uint32_t mgInVarId = mgInVar->result_id();
// Copy loop at the top of the entry point:
// for (uint t = 0; t < vertexCount * clipCount; ++t)
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
// miscompile and whole-array reads crash the Adreno compiler.
const uint32_t entryFuncId = EntryFunctionId(ctx);
Function* entryFn = nullptr;
for (Function& function : *ctx->module()) {
if (function.result_id() == entryFuncId) {
entryFn = &function;
break;
}
}
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
return false;
}
const uint32_t uintTypeId = UintType(ctx);
const uint32_t boolTypeId = BoolType(ctx);
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
const uint32_t uint0 = IntConstant(ctx, false, 0);
const uint32_t uint1 = IntConstant(ctx, false, 1);
const uint32_t uintN = IntConstant(ctx, false, clipCount);
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
BasicBlock* entryBlock = &*entryFn->begin();
auto splitPoint = entryBlock->begin();
while (splitPoint != entryBlock->end() &&
splitPoint->opcode() == spv::Op::OpVariable) {
++splitPoint;
}
// Loop counter lives with the other function-local variables.
const uint32_t counterVarId = ctx->TakeNextId();
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
const uint32_t restLabelId = ctx->TakeNextId();
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
const uint32_t headerLabelId = ctx->TakeNextId();
const uint32_t checkLabelId = ctx->TakeNextId();
const uint32_t bodyLabelId = ctx->TakeNextId();
const uint32_t continueLabelId = ctx->TakeNextId();
auto makeBlock = [&](uint32_t labelId) {
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
};
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
uint32_t resultId, std::vector<Operand> operands) {
block->AddInstruction(spvtools::MakeUnique<Instruction>(
ctx, opcode, typeId, resultId, std::move(operands)));
};
// entry: t = 0; branch header
addInst(entryBlock, spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
// header: structured loop header
auto headerBlock = makeBlock(headerLabelId);
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
{SPV_OPERAND_TYPE_LOOP_CONTROL,
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
// check: t < vertexCount * clipCount ?
auto checkBlock = makeBlock(checkLabelId);
const uint32_t tCheckId = ctx->TakeNextId();
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t condId = ctx->TakeNextId();
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
{{SPV_OPERAND_TYPE_ID, {condId}},
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
auto bodyBlock = makeBlock(bodyLabelId);
const uint32_t tBodyId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t vertexIdxId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
const uint32_t clipIdxId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
const uint32_t srcChainId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
{SPV_OPERAND_TYPE_ID, {memberConst}},
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
const uint32_t valId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
const uint32_t dstChainId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
// continue: ++t
auto continueBlock = makeBlock(continueLabelId);
const uint32_t tContinueId = ctx->TakeNextId();
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t tIncId = ctx->TakeNextId();
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
// Redirect the pre-existing accesses to the shadow copy.
for (Instruction* chain : chains) {
if (chain->NumInOperands() == 3) {
// (vertex, member): whole float[N] of one vertex.
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType =
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
newOperands.push_back(chain->GetInOperand(1));
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
} else {
// (vertex, member, k, ...): drop the member index.
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType =
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
newOperands.push_back(chain->GetInOperand(1));
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
newOperands.push_back(chain->GetInOperand(i));
}
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
}
}
return true;
}
} // namespace
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
auto* ctx = context();
const spv::ExecutionModel model = EntryExecutionModel(ctx);
const bool isVertex = model == spv::ExecutionModel::Vertex;
const bool isGeometry = model == spv::ExecutionModel::Geometry;
if (!isVertex && !isGeometry) {
return Status::SuccessWithoutChange;
}
bool changed = LowerOutputClipDistance(ctx, isGeometry);
if (isGeometry) {
changed |= LowerInputClipDistance(ctx);
}
if (!changed) {
return Status::SuccessWithoutChange;
}
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,44 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
// - writes through non-constant indices silently fail to link,
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
// ("array indexing out of boundary") while dynamic-index reads work,
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
// compiler backend (libllvm-qgl.so).
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
// literal constant indices is inserted before every OpEmitVertex (geometry) or
// every return of the entry point (vertex). Input side (geometry): accesses to
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
// array-of-arrays filled once at the top of the entry point by a structured loop
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
// statically referenced by the flush/copy so cross-stage IO matching is intact.
// Only meant for the DirectGLES transpile path on Qualcomm devices.
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "lower-clip-distance-for-essl"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+1 -1
View File
@@ -87,7 +87,7 @@ def main():
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
ap.add_argument("--max-rounds", type=int, default=4000)
ap.add_argument("--max-empty-streak", type=int, default=8,
ap.add_argument("--max-empty-streak", type=int, default=64,
help="abort after this many consecutive chunks that produce no log at all")
ap.add_argument("--min-mem-kb", type=int, default=400000,
help="pause when the device drops below this much available memory")