mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Merge] (DirectGLES, GLState, GLImpl, ShaderTranspiler): land GL43 wave6 and wave7
This commit is contained in:
+3
-1
@@ -293,7 +293,9 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
@@ -301,7 +303,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
|
||||
@@ -1463,8 +1463,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
|
||||
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
|
||||
const GLint layer = layerable ? imageBinding.Layer : 0;
|
||||
// The bind half of the image-format widening. SyncTextureObjectToBackend has just
|
||||
// allocated this texture's storage in the core carrier of its format (the call above
|
||||
// is the one that marks it image-bindable), and glBindImageTexture's `format` has to
|
||||
// name the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on
|
||||
// Adreno for nineteen of the twenty-six non-core formats and on both Malis for
|
||||
// twenty-five, and every driver that DOES accept a narrow texture through a wide
|
||||
// image accepts it silently, reading and writing out of bounds. The frontend's own
|
||||
// ImageTextureBinding keeps the application's format untouched, so
|
||||
// GL_IMAGE_BINDING_FORMAT still answers what was passed in.
|
||||
//
|
||||
// Widened from the format the APPLICATION named rather than from the texture's own,
|
||||
// because GL lets the two differ inside one format class and the shader was widened
|
||||
// from the class the application named too (an r32ui view of an r32f image is a legal
|
||||
// reinterpretation). The two carriers always have the same texel size - every format
|
||||
// in a class widens to the four-channel form of that same class - so the storage
|
||||
// still describes what the bind claims. Gated on the TEXTURE having been widened, so
|
||||
// a bind format that names a class the storage does not have is left alone: GL
|
||||
// already calls that undefined, and inventing a carrier for it would only make the
|
||||
// out-of-class read wider.
|
||||
GLenum bindFormat = imageBinding.Format;
|
||||
if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
||||
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||
if (boundFormatWidening) {
|
||||
bindFormat = boundFormatWidening.InternalFormat;
|
||||
}
|
||||
}
|
||||
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
||||
layered, layer, imageBinding.Access, imageBinding.Format);
|
||||
layered, layer, imageBinding.Access, bindFormat);
|
||||
}
|
||||
|
||||
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
||||
@@ -2366,7 +2393,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// different one makes what was built wrong. Asked of the twin because only it
|
||||
// knows which units its own images address - and answered by an empty-vector
|
||||
// test for every program that declares its formats, which is nearly all of them.
|
||||
!twin->ImageUnitFormatsStillMatch()) {
|
||||
!twin->ImageUnitFormatsStillMatch() ||
|
||||
// A fifth of the same shape, for the programs ES will not link at all: one whose
|
||||
// tessellation evaluation stage has no control stage gets a synthesized
|
||||
// pass-through one, and GL_PATCH_VERTICES is compiled INTO it as
|
||||
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
|
||||
// built program wrong. -1 is "this program needed no such stage", which compares
|
||||
// equal to itself and costs every other program one integer test.
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
|
||||
@@ -2282,6 +2282,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_isInitialized = false;
|
||||
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
|
||||
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
|
||||
// and a widened texture's sampled view has to answer the channels the logical format
|
||||
// does not have with 0 and 1 - which is a swizzle. The parameter sync is gated on the
|
||||
// frontend's params version, which this transition does not move, so without the
|
||||
// override an application that never touched GL_TEXTURE_SWIZZLE_* would keep the
|
||||
// driver at its defaults and sample the carrier's surplus channels raw.
|
||||
m_forceTextureParamsResync = true;
|
||||
}
|
||||
|
||||
void BackendTextureObject::RecreateBackendTexture() {
|
||||
@@ -2579,7 +2587,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<Uint8>& widenedData, Bool integerData) {
|
||||
Uint8 oneBits[8] = {};
|
||||
SizeT componentSize = 0;
|
||||
if (componentCount != 3 || data == nullptr || byteSize == 0 ||
|
||||
// One and two source components as well as three: the image-format widening carries
|
||||
// GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see
|
||||
// TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the
|
||||
// same values the three-channel case gives its single added one - zeroes, and the
|
||||
// format's implied 1 in alpha.
|
||||
if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 ||
|
||||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
|
||||
return data;
|
||||
}
|
||||
@@ -2606,7 +2619,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Memcpy(dst, src, srcTexelBytes);
|
||||
src += srcTexelBytes;
|
||||
}
|
||||
Memcpy(dst + srcTexelBytes, oneBits, componentSize);
|
||||
// ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA
|
||||
// channel, and a one- or two-component source leaves the channels between it and
|
||||
// alpha at the zero `assign` already wrote. For three components the two
|
||||
// expressions coincide, which is what this used to be written as.
|
||||
Memcpy(dst + componentSize * 3, oneBits, componentSize);
|
||||
}
|
||||
return widenedData.data();
|
||||
}
|
||||
@@ -2689,6 +2706,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
widenedData, IsIntegerWidenableFormat(format));
|
||||
}
|
||||
|
||||
// The transfer half of the image-format widening: an image-bindable texture whose ES
|
||||
// storage was widened to a core carrier is described to the driver as a four-component
|
||||
// transfer, so its one- or two-component client data has to be repacked the same way the
|
||||
// three-channel colour-renderable widening repacks its own.
|
||||
//
|
||||
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
|
||||
// no-op by construction: none of the seventeen widened formats is a three-channel one
|
||||
// (GetWidenableClientComponentCount reports 0 for every one of them), and the SNORM
|
||||
// shadow-to-float conversion only fires for a GL_FLOAT transfer type, which the widened
|
||||
// triple never picks for the two SNORM8 formats. So the shadow reaches this untouched and
|
||||
// one repack is all that runs.
|
||||
static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening,
|
||||
const IntVec3& texelSize, const void* data, SizeT byteSize,
|
||||
Vector<Uint8>& widenedData) {
|
||||
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels >= 4) {
|
||||
return data;
|
||||
}
|
||||
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
|
||||
widenedData, widening.IntegerData);
|
||||
}
|
||||
|
||||
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
|
||||
// with the widened carrier's. Deliberately unconditional on anything but the widening
|
||||
// itself: whatever renderability fallback the triple carried, an image the driver refuses
|
||||
// to bind is useless, so the image constraint wins.
|
||||
static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening,
|
||||
GLenum* inOutInternalFormat, GLenum* inOutFormat,
|
||||
GLenum* inOutType) {
|
||||
if (!widening) {
|
||||
return;
|
||||
}
|
||||
if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat;
|
||||
if (inOutFormat) *inOutFormat = widening.Format;
|
||||
if (inOutType) *inOutType = widening.Type;
|
||||
}
|
||||
|
||||
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
|
||||
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
|
||||
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
|
||||
@@ -2884,6 +2937,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bind(target);
|
||||
}
|
||||
|
||||
// Only a texture that is actually image-bound pays for the widening: it doubles
|
||||
// or quadruples the storage, and RequireImageBindableStorage is sticky, so a
|
||||
// texture that is merely sampled keeps its narrow format for life. See
|
||||
// TextureImpl::GetImageBindableStorageWidening for what widens and why.
|
||||
const TextureImpl::ImageBindableStorageWidening imageWidening =
|
||||
m_imageBindableStorageRequired
|
||||
? TextureImpl::GetImageBindableStorageWidening(textureMipmapObject->GetFormat())
|
||||
: TextureImpl::ImageBindableStorageWidening{};
|
||||
|
||||
const Bool canAppendMipmaps =
|
||||
m_isInitialized &&
|
||||
!m_imageBindableStorageRequired &&
|
||||
@@ -2989,6 +3051,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLenum glInternalFormat, glType, glFormat;
|
||||
TextureImpl::GenerateTextureFormatInfo(textureMipmapObject->GetFormat(), &glInternalFormat,
|
||||
&glFormat, &glType, targetInternal);
|
||||
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
|
||||
|
||||
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
|
||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
||||
@@ -3104,6 +3167,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
uploadData =
|
||||
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
uploadData, levelByteSize, &glType, packedUploadData);
|
||||
Vector<Uint8> imageWidenedUploadData;
|
||||
uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData,
|
||||
levelByteSize, imageWidenedUploadData);
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||
@@ -3243,6 +3309,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLenum glInternalFormat, glType, glFormat;
|
||||
TextureImpl::GenerateTextureFormatInfo(textureMipmapObject->GetFormat(), &glInternalFormat,
|
||||
&glFormat, &glType, targetInternal);
|
||||
// The storage this level is being written into was widened when it was minted
|
||||
// (see above), so the transfer pair has to describe the carrier here too - ES
|
||||
// requires glTexSubImage's `format` to match the storage's base internal
|
||||
// format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION.
|
||||
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
|
||||
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
|
||||
ScopedDefaultUnpackState unpackState;
|
||||
for (auto& uploadTarget : uploadTargets) {
|
||||
@@ -3281,6 +3352,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
|
||||
uploadData, byteSize, &glType, packedUploadData);
|
||||
// Leaves `uploadData` pointing at its own buffer when it fires, which
|
||||
// is exactly what takes the sub-rect fast path below out of play: that
|
||||
// path strides into the SHADOW, and the widened texels are four
|
||||
// components wide where the shadow's are one or two.
|
||||
Vector<Uint8> imageWidenedUploadData;
|
||||
uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize,
|
||||
imageWidenedUploadData);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
||||
// Sub-rect upload: when only a region of the level changed (a
|
||||
@@ -3726,6 +3804,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The same composition for the image-format widening, which can add TWO or THREE
|
||||
// channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the
|
||||
// format does not have as 0, except alpha, which reads as 1 - so a sampler must see
|
||||
// those constants and not whatever the widened storage holds. The upload and the
|
||||
// shader's own store mask already keep them at exactly these values; this covers
|
||||
// storage nothing has written yet (glTexStorage with no upload), whose surplus
|
||||
// channels are undefined. Composed with the application's own swizzle for the same
|
||||
// reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the
|
||||
// logical texel, so it is the source that is substituted, never the destination.
|
||||
if (const auto imageWidening =
|
||||
m_imageBindableStorageRequired
|
||||
? TextureImpl::GetImageBindableStorageWidening(stateTextureObject->GetFormat())
|
||||
: TextureImpl::ImageBindableStorageWidening{}) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
switch (swizzleParams[channel]) {
|
||||
case TextureSwizzleParam::Green:
|
||||
if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero;
|
||||
break;
|
||||
case TextureSwizzleParam::Blue:
|
||||
if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero;
|
||||
break;
|
||||
case TextureSwizzleParam::Alpha:
|
||||
if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (swizzleParams != m_cacheSwizzleParams) {
|
||||
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
||||
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
||||
@@ -5042,6 +5149,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The GL internal format a glslang layout format names, for the seventeen non-core
|
||||
// formats WidenImageFormatsForEssl carries exactly plus nothing else: the only
|
||||
// question asked of it is "does this DECLARED format widen", and answering 0 for
|
||||
// everything else is the same "no" a non-widenable format gets. Kept as its own
|
||||
// switch rather than routed through the frontend's enum converters because a
|
||||
// TLayoutFormat is a glslang value and the reflection snapshot stores it raw.
|
||||
Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) {
|
||||
switch (format) {
|
||||
case glslang::ElfRg32f: return 0x8230; // GL_RG32F
|
||||
case glslang::ElfRg16f: return 0x822F; // GL_RG16F
|
||||
case glslang::ElfR16f: return 0x822D; // GL_R16F
|
||||
case glslang::ElfRg8: return 0x822B; // GL_RG8
|
||||
case glslang::ElfR8: return 0x8229; // GL_R8
|
||||
case glslang::ElfRg8Snorm: return 0x8F95; // GL_RG8_SNORM
|
||||
case glslang::ElfR8Snorm: return 0x8F94; // GL_R8_SNORM
|
||||
case glslang::ElfRg32i: return 0x823B; // GL_RG32I
|
||||
case glslang::ElfRg16i: return 0x8239; // GL_RG16I
|
||||
case glslang::ElfR16i: return 0x8233; // GL_R16I
|
||||
case glslang::ElfRg8i: return 0x8237; // GL_RG8I
|
||||
case glslang::ElfR8i: return 0x8231; // GL_R8I
|
||||
case glslang::ElfRg32ui: return 0x823C; // GL_RG32UI
|
||||
case glslang::ElfRg16ui: return 0x823A; // GL_RG16UI
|
||||
case glslang::ElfR16ui: return 0x8234; // GL_R16UI
|
||||
case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI
|
||||
case glslang::ElfR8ui: return 0x8232; // GL_R8UI
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the ESSL chain will re-declare an image of this format in a core carrier
|
||||
// and mask its accesses (WidenImageFormatsForEssl). The same rule
|
||||
// TextureImpl::GetImageBindableStorageWidening applies to the storage and the bind -
|
||||
// the three layers move together or the shader addresses a texel size the storage
|
||||
// does not have.
|
||||
//
|
||||
// Without GL_NV_image_formats there is no legal spelling for any non-core format, so
|
||||
// everything carriable widens. WITH the extension only the formats SPIRV-Cross
|
||||
// refuses to print do: it throws for its is_desktop_only_format set instead of
|
||||
// emitting a token, and the throw loses the stage however willing the driver was.
|
||||
Bool ImageFormatWillBeWidened(Uint glInternalFormat) {
|
||||
if (glInternalFormat == 0) return false;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) == 0) {
|
||||
return false;
|
||||
}
|
||||
return !g_GLESCapabilities.SupportsExtendedImageFormats ||
|
||||
!MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(
|
||||
glInternalFormat);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// What the format bake needs from the frontend, collected in one walk of the uniform
|
||||
@@ -5074,17 +5231,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
const auto& type = stateProgramObject.GetUniformTypeFacts(loc);
|
||||
if (type.hasFormat) {
|
||||
// Declared, and therefore left exactly as written - but a non-core spelling
|
||||
// still needs the extension directive to survive the ES compiler.
|
||||
if (!IsCoreEsslLayoutFormat(static_cast<glslang::TLayoutFormat>(type.layoutFormat))) {
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
// From the OWNED TypeFacts, not from a live TType: the reflection
|
||||
// snapshot already carries the declared layout format, and there is
|
||||
// no glslang object to ask on a translation-cache L1 hit.
|
||||
recordUnspellableFormat(
|
||||
name, glslang::TQualifier::getLayoutFormatString(
|
||||
static_cast<glslang::TLayoutFormat>(type.layoutFormat)));
|
||||
// Declared, and therefore never overridden by the BAKE - but a non-core
|
||||
// spelling still has to become legal ESSL somehow.
|
||||
const auto declaredFormat = static_cast<glslang::TLayoutFormat>(type.layoutFormat);
|
||||
if (!IsCoreEsslLayoutFormat(declaredFormat)) {
|
||||
// Seventeen of the twenty-six non-core formats are re-declared in the core
|
||||
// format that carries them exactly, with every access masked back to the
|
||||
// channels GL says they have (WidenImageFormatsForEssl, and the matching
|
||||
// storage/bind widening in TextureImpl). Those need neither the extension
|
||||
// nor the diagnostic: there IS a legal spelling for them now.
|
||||
if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) {
|
||||
inputs.declaresWidenableImageFormat = true;
|
||||
} else {
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
// From the OWNED TypeFacts, not from a live TType: the reflection
|
||||
// snapshot already carries the declared layout format, and there
|
||||
// is no glslang object to ask on a translation-cache L1 hit.
|
||||
recordUnspellableFormat(
|
||||
name, glslang::TQualifier::getLayoutFormatString(declaredFormat));
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -5102,20 +5268,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (boundFormat == 0) continue;
|
||||
if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) {
|
||||
// Outside the GLSL ES core set, so the emitted ESSL only compiles with
|
||||
// GL_NV_image_formats. Without the extension there is no legal spelling at
|
||||
// all, and baking one would trade a "no format qualifier" compile error for
|
||||
// an "unsupported format" one - so the image is left format-less. Its unit
|
||||
// stays in the key, so a rebind to a core format still rebuilds and works.
|
||||
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
|
||||
"core cannot spell and this driver has no GL_NV_image_formats for.",
|
||||
// The same three-way split the DECLARED branch above makes, and it has to be
|
||||
// the same one: a format-less image is baked with the bound format, so from
|
||||
// WidenImageFormatsForEssl's point of view the two routes hand it identical
|
||||
// modules and must arm it identically.
|
||||
if (ImageFormatWillBeWidened(boundFormat)) {
|
||||
// The bake writes this format INTO the module, so the widening that runs
|
||||
// straight after has to be armed for it even though nothing DECLARED it -
|
||||
// and armed WHETHER OR NOT the driver has GL_NV_image_formats. SPIRV-Cross
|
||||
// throws for its is_desktop_only_format set the moment it targets ESSL,
|
||||
// however willing the driver was, so the extension decides HOW MUCH gets
|
||||
// widened (widenOnlyUnprintableImageFormats) and never WHETHER. Arming
|
||||
// this only on the no-extension path left the shader half of the widening
|
||||
// switched off while TextureImpl's storage/bind half - which keys on
|
||||
// SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran:
|
||||
// the stage threw, the program linked without it, and every dispatch
|
||||
// silently did nothing. That is the whole of
|
||||
// KHR-GL43.stencil_texturing.functional's compute half, whose uni_image is
|
||||
// a format-less uimage2D bound to an R8UI texture.
|
||||
inputs.declaresWidenableImageFormat = true;
|
||||
} else if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
// Outside the GLSL ES core set, with no GL_NV_image_formats to spell it
|
||||
// and no core format that carries it exactly: there is no legal ESSL for
|
||||
// this stage at all. Leaving the image format-LESS is NOT a softer
|
||||
// failure: all three test devices reject a format-less image declaration
|
||||
// outright ("all images have to define layout format"), readonly and
|
||||
// writeonly alike, so it trades one hard compile error for another. The
|
||||
// unit stays in the rebuild key either way, so a rebind to a spellable
|
||||
// format still rebuilds and works.
|
||||
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which "
|
||||
"GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and "
|
||||
"no core format carries exactly.",
|
||||
name.c_str(), unit, boundFormat);
|
||||
recordUnspellableFormat(
|
||||
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
|
||||
continue;
|
||||
} else {
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
}
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
}
|
||||
const String baseName = ImageUniformBaseName(name);
|
||||
const auto existing = inputs.glFormatByUniformName.find(baseName);
|
||||
@@ -5147,6 +5337,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) {
|
||||
continue;
|
||||
}
|
||||
// A format the widening carries stays on the module route even though SPIRV-Cross
|
||||
// would not print it: by the time SPIRV-Cross sees the declaration it names the
|
||||
// core carrier, which it does print. Writing the narrow spelling into the text
|
||||
// instead would put back exactly the token the driver rejects.
|
||||
if (ImageFormatWillBeWidened(entry.second)) {
|
||||
continue;
|
||||
}
|
||||
String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second);
|
||||
if (spelling.empty()) continue; // no image-format spelling at all; nothing to write
|
||||
inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling));
|
||||
@@ -5170,6 +5367,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
// Every image ARRAY whose elements the application did NOT leave on units consecutive from
|
||||
// element zero - the only shape ESSL can spell, since an image unit there comes solely
|
||||
// from the one layout(binding=N) an array declaration carries. Desktop GL assigns them
|
||||
// per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform,
|
||||
// so there is nothing to fix at the API end and the emitted text has to carry it
|
||||
// (RemapImageArrayElementUnits). Empty for every program that does not do this, which is
|
||||
// very nearly all of them - one walk of the reflection and no allocation in that case.
|
||||
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
Vector<ImageArrayUnitPlan> plans;
|
||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
// Reflection repeats the array's "g_image[0]" spelling at EVERY location the array
|
||||
// spans, so only the location that name resolves back to is the array itself.
|
||||
if (stateProgramObject.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
|
||||
const String baseName = ImageUniformBaseName(name);
|
||||
if (baseName == name) continue; // a scalar image: one binding says it all
|
||||
|
||||
ImageArrayUnitPlan plan;
|
||||
plan.name = baseName;
|
||||
for (Uint element = loc; element <= maxUniformLoc &&
|
||||
stateProgramObject.UniformLocationsAliasSameUniform(
|
||||
static_cast<Int>(loc), static_cast<Int>(element));
|
||||
++element) {
|
||||
plan.units.push_back(stateProgramObject.GetUniformSamplerOrImageUnitIndex(element));
|
||||
}
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
Bool consecutive = true;
|
||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
||||
if (plan.units[element] != plan.units[0] + static_cast<Int>(element)) {
|
||||
consecutive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// What ESSL does unaided is already right; leaving these out is what keeps the
|
||||
// emitted text of every ordinary image shader byte-identical to before.
|
||||
if (consecutive) continue;
|
||||
plans.push_back(Move(plan));
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
|
||||
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
|
||||
Uint64 signature = 0;
|
||||
@@ -5197,8 +5440,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray,
|
||||
// MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples,
|
||||
// SupportsNoperspectiveInterpolation, GLESVersion} (the last via
|
||||
// ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
|
||||
// SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion}
|
||||
// (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
|
||||
//
|
||||
// Deliberately NOT in here, and therefore NOT in the key: the text-level passes that
|
||||
// follow in SyncToBackend. They are cheap string work and they read a long tail of
|
||||
@@ -5255,6 +5498,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
||||
// The image-format widening is armed on EVERY driver, so its probe has to ride the
|
||||
// shared parse rather than add one: it is asked of every stage of every program, and
|
||||
// a BuildModule per stage per gate is exactly what cost compile-heavy CTS cases ~10%
|
||||
// before this struct existed. What differs per driver is only HOW MUCH it widens -
|
||||
// everything carriable where there is no GL_NV_image_formats to spell the narrow
|
||||
// format, and only the formats SPIRV-Cross refuses to print where there is.
|
||||
const Bool widenOnlyUnprintableImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats;
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
||||
if (viewportLoweringArmed || sampleClampArmed) {
|
||||
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
||||
@@ -5425,6 +5675,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &arrayImageSpirv;
|
||||
}
|
||||
|
||||
// The SAMPLER half of the same 1D story, and a defect one layer deeper than the one
|
||||
// above. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - it just prints the
|
||||
// OFFSET and the two GRADIENT operands with the arity the desktop shader spelled, so
|
||||
// textureLodOffset(sampler1DArray, vec2, float, int) is emitted against a
|
||||
// sampler2DArray and the driver answers "no matching overloaded function found",
|
||||
// losing the stage and silently no-oping every dispatch that used it. Widening the
|
||||
// operands alone would be an INVALID module (the validator derives the required arity
|
||||
// from the image's own Dim), so the pass moves the type to 2D and widens coordinate,
|
||||
// offset and gradients together.
|
||||
//
|
||||
// NO KEY MATERIAL, by the same test LegalizeResourceArrayIndexingForEssl passes:
|
||||
// it takes the module and nothing else, no capability bit arms it, and it self-gates
|
||||
// on the module's own content (BinaryHasOffsetOrGrad1DSampledImage). The module is
|
||||
// already the largest thing in the L2 key, so it is covered completely.
|
||||
Vector<unsigned int> sampled1DSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DSampledImagesForEssl(
|
||||
*effectiveSpirv, sampled1DSpirv, enableSpirvValidation) &&
|
||||
!sampled1DSpirv.empty()) {
|
||||
effectiveSpirv = &sampled1DSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
||||
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
||||
// format"), which loses the whole program. Give each such image the format the
|
||||
@@ -5445,6 +5716,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &imageFormatSpirv;
|
||||
}
|
||||
|
||||
// GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven
|
||||
// reach ES only through GL_NV_image_formats, which no tested driver advertises. A
|
||||
// shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws
|
||||
// for some of them and the driver rejects the token for the rest ("'rg32f' : not a
|
||||
// legal layout qualifier id"), and dropping the qualifier is refused too ("all images
|
||||
// have to define layout format") - so the stage is lost and every draw with the
|
||||
// program silently renders nothing. Seventeen of them widen EXACTLY into a core format
|
||||
// of the same per-channel width, and this rewrites those declarations to the carrier
|
||||
// and masks every access back to the channels GL says the format has. The other nine
|
||||
// have no exact carrier and keep the honest diagnostic
|
||||
// CollectImageFormatBakeInputs emits.
|
||||
//
|
||||
// A driver that HAS GL_NV_image_formats still needs part of this. SPIRV-Cross throws
|
||||
// for its is_desktop_only_format set when it targets ESSL rather than printing a
|
||||
// token, and the throw loses the stage however willing the driver was - Mesa
|
||||
// advertises the extension and `layout(r8ui) uimage2D` lost its whole program there
|
||||
// until the widening ran for it too. So the driver bit decides HOW MUCH is widened,
|
||||
// never WHETHER.
|
||||
//
|
||||
// AFTER the bake above, deliberately: a format-less image whose unit holds a non-core
|
||||
// format is baked with that format and widened here, so both routes end in the same
|
||||
// place and there is no second widening rule for baked declarations.
|
||||
//
|
||||
// KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which selects the
|
||||
// mode - see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes
|
||||
// no other input: what it rewrites is a pure function of the module's own declared
|
||||
// formats and that mode, and the module is already the largest thing in the L2 key.
|
||||
// The ARMING flag is deliberately NOT key material: it only decides whether the pass
|
||||
// runs, and the module below is adopted only when the pass actually changed the bytes
|
||||
// - so a program-wide flag that over-arms a stage costs an optimizer round trip and
|
||||
// changes no output.
|
||||
//
|
||||
// DirectVulkan is deliberately not given this: it takes the declared format natively
|
||||
// and resolves the descriptor's view format from the same bind state.
|
||||
Vector<unsigned int> widenedImageFormatSpirv;
|
||||
if (imageFormatBake.declaresWidenableImageFormat &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl(
|
||||
*effectiveSpirv, widenedImageFormatSpirv, widenOnlyUnprintableImageFormats,
|
||||
enableSpirvValidation) &&
|
||||
!widenedImageFormatSpirv.empty() && widenedImageFormatSpirv != *effectiveSpirv) {
|
||||
effectiveSpirv = &widenedImageFormatSpirv;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -5461,24 +5775,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &outputIndexSpirv;
|
||||
}
|
||||
|
||||
// Same rule, different resource, every stage: GL 4.3 lets an array of storage
|
||||
// blocks be indexed with any dynamically-uniform expression, GLSL ES keeps the
|
||||
// ES 3.1 constant-expression rule, and the Qualcomm compiler enforces it
|
||||
// Same rule, two more resources, every stage: desktop GL lets an array of
|
||||
// storage blocks and an array of image uniforms be indexed with any
|
||||
// dynamically-uniform expression, GLSL ES keeps the ES 3.1
|
||||
// constant-expression rule for both, and the drivers enforce it - Qualcomm
|
||||
// ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted") - losing the stage, the program, and every dispatch that used
|
||||
// it, while the frontend keeps reporting the link glslang performed. Fold or
|
||||
// lower the index here, on the ESSL path only: the same module is legal for
|
||||
// DirectVulkan, which binds the array as one descriptor array.
|
||||
// permitted"), Mesa ("image arrays indexed with non-constant expressions are
|
||||
// forbidden in GLSL ES") - losing the stage, the program, and every draw or
|
||||
// dispatch that used it, while the frontend keeps reporting the link glslang
|
||||
// performed. Fold or lower the index here, on the ESSL path only: the same
|
||||
// module is legal for DirectVulkan, which binds the array as one descriptor
|
||||
// array.
|
||||
//
|
||||
// The image half is also what makes RemapImageArrayElementUnits below possible
|
||||
// at all: that pass rewrites `g_image[k]` into a per-element declaration, and it
|
||||
// can only do that once every k the emitted ESSL spells is a literal.
|
||||
//
|
||||
// NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the
|
||||
// module and nothing else - no capability bit arms it, no per-program plan steers
|
||||
// it - and it self-gates on the module's own content
|
||||
// (BinaryHasDynamicStorageBlockArrayIndexing). The module is already the largest
|
||||
// (BinaryHasDynamicResourceArrayIndexing). The module is already the largest
|
||||
// thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl,
|
||||
// whose signature is equally module-only but which SupportsViewportArray ARMS -
|
||||
// that bit is in the key precisely because of it.
|
||||
Vector<unsigned int> blockArrayIndexSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeResourceArrayIndexingForEssl(
|
||||
*effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) &&
|
||||
!blockArrayIndexSpirv.empty()) {
|
||||
effectiveSpirv = &blockArrayIndexSpirv;
|
||||
@@ -5560,6 +5881,123 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL
|
||||
// shader: the input patch is passed through unmodified and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such
|
||||
// state and no such allowance - it rejects the program at link, and with an EMPTY info
|
||||
// log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only:
|
||||
// link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and
|
||||
// atomic counter the failing conformance case also declares). The frontend's own glslang
|
||||
// link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every
|
||||
// draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and
|
||||
// an untouched SSBO, with no error anywhere.
|
||||
//
|
||||
// So the missing stage is synthesized and attached here, alongside the program's own.
|
||||
// DirectVulkan already does exactly this for the same structural reason
|
||||
// (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather
|
||||
// than inventing an approach.
|
||||
//
|
||||
// Nothing that works today can be harmed by it: it fires ONLY for a program that has an
|
||||
// evaluation stage and no control stage, and every such program fails to link on ES right
|
||||
// now. The worst case is that the synthesized stage fails to compile or link, which leaves
|
||||
// the program exactly as dead as it already was - but with a driver log that says why,
|
||||
// where today there is an empty one.
|
||||
void BackendProgramObjectImpl::AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, const Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl) {
|
||||
// PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output
|
||||
// patch size - so a program built for one value is stale for another. Recorded here
|
||||
// and compared on the draw path (SyncCurrentProgram), the same shape as the
|
||||
// storage-block and image-format signatures next to it.
|
||||
const Uint patchVertices = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchVertices()
|
||||
: 3u;
|
||||
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
|
||||
|
||||
if (tessEvalShaderIndex < 0 ||
|
||||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no "
|
||||
"SPIR-V for it; the pass-through control stage GL describes cannot be checked, so "
|
||||
"the program is left to fail its ES link.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// The one shape the pass-through cannot stand in for. It forwards gl_Position and
|
||||
// nothing else, so an evaluation stage that reads a user-defined varying or a
|
||||
// per-patch input - both of which carry a Location, where every built-in it needs
|
||||
// does not - would start reading undefined values the moment a control stage sat
|
||||
// between it and the vertex stage. Declining keeps that from being silent; it is the
|
||||
// identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed.
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput(
|
||||
shaderSpirvs[static_cast<SizeT>(tessEvalShaderIndex)])) {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a "
|
||||
"user-defined input through it; a synthesized pass-through control stage cannot "
|
||||
"forward that, so the program is declined rather than fed an undefined varying.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares
|
||||
// gl_PerVertex, and with which members, depends on what the application's shaders
|
||||
// touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage
|
||||
// it feeds is an ES link error against a program with no other problem. gl_in copies
|
||||
// the vertex stage's OUT block (that is what arrives) and gl_out the evaluation
|
||||
// stage's IN block (that is what is expected). A neighbour that redeclared nothing
|
||||
// yields an empty list, which leaves the driver's own built-in declaration in place -
|
||||
// which is exactly what matching it requires.
|
||||
const String inMembers =
|
||||
ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String());
|
||||
const String outMembers =
|
||||
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
|
||||
|
||||
const String source =
|
||||
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers);
|
||||
|
||||
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
|
||||
if (backendShaderId == 0) {
|
||||
MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for "
|
||||
"program %u.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* sourceCStr = source.c_str();
|
||||
MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices "
|
||||
"%u):\n%s",
|
||||
stateProgramObject.GetExternalIndex(), patchVertices, sourceCStr);
|
||||
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
|
||||
g_GLESFuncs.glCompileShader(backendShaderId);
|
||||
|
||||
// GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten
|
||||
// out-param must read as "compile failed" and never as a silent success.
|
||||
GLint compileStatus = GL_FALSE;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
|
||||
if (compileStatus == GL_FALSE) {
|
||||
GLint logLength = 0;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
if (logLength < 0) logLength = 0;
|
||||
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
log.back() = '\0';
|
||||
MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for "
|
||||
"program %u. Driver log: %s\nSource:\n%s",
|
||||
stateProgramObject.GetExternalIndex(), log.data(), sourceCStr);
|
||||
m_backendProgramUsable = false;
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
return;
|
||||
}
|
||||
|
||||
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
||||
// Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is
|
||||
// what makes the program own it and what keeps a relink from leaking it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -5605,6 +6043,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reading it afterwards - resolves the same slot for the same GL binding.
|
||||
m_atomicCounterGlBindings.clear();
|
||||
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
|
||||
// Re-established by AttachPassthroughTessControlStage below when this program needs
|
||||
// one; cleared first so a program that stops needing one (a relink that now attaches
|
||||
// a real control stage) does not keep comparing against a stale patch size.
|
||||
m_passthroughTessControlPatchVertices = -1;
|
||||
// The same shape again for image FORMATS: what a format-less image declaration
|
||||
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||
@@ -5617,6 +6059,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"different bound formats; left format-less.",
|
||||
conflicted.c_str(), stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
// ...and once more for image ARRAYS whose per-element units are not consecutive, which
|
||||
// ESSL has no way to express in one declaration. Program-wide, like the bake, and read
|
||||
// from the same snapshot of the reflection; the per-stage rewrite happens below.
|
||||
const Vector<ImageArrayUnitPlan> nonConsecutiveImageArrays =
|
||||
CollectNonConsecutiveImageArrayPlans(*stateProgramObject);
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -5738,6 +6185,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return candidate;
|
||||
};
|
||||
|
||||
// Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a
|
||||
// program that has an evaluation stage without one, with an empty info log. When that
|
||||
// is this program's shape, one is synthesized below - and it has to be spelled to
|
||||
// MATCH the two stages it sits between, so their emitted ESSL is kept here as it is
|
||||
// produced. Empty for every program that has a control stage of its own, which is
|
||||
// all but a handful.
|
||||
Bool hasTessEvalStage = false;
|
||||
Bool hasTessControlStage = false;
|
||||
Int tessEvalShaderIndex = -1;
|
||||
String vertexStageEssl;
|
||||
String tessEvalStageEssl;
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
const auto stage = attachedShaders[index]->GetShaderStage();
|
||||
if (stage == ShaderStage::TessControl) hasTessControlStage = true;
|
||||
if (stage == ShaderStage::TessEval) {
|
||||
hasTessEvalStage = true;
|
||||
tessEvalShaderIndex = index;
|
||||
}
|
||||
}
|
||||
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -5781,6 +6249,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray;
|
||||
esslKeyInputs.supportsNoperspectiveInterpolation =
|
||||
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
|
||||
esslKeyInputs.supportsExtendedImageFormats =
|
||||
g_GLESCapabilities.SupportsExtendedImageFormats;
|
||||
esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples;
|
||||
esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples;
|
||||
esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples;
|
||||
@@ -5945,6 +6415,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// strip, so both halves of a split image inherit the format.
|
||||
source = BakeImageFormatQualifiers(std::move(source),
|
||||
imageFormatBake.esslFormatQualifierByUniformName);
|
||||
// An image ARRAY whose elements do not sit on consecutive units cannot be spelled
|
||||
// by the single layout(binding=N) the rebind above stamped: ESSL gives element k
|
||||
// the unit N+k and there is no glUniform1i to correct it with. Split the array
|
||||
// into one scalar declaration per element, each with its own binding. AFTER the
|
||||
// rebind and the format bake, both of which look the array up by its GL uniform
|
||||
// name and need the binding already there; BEFORE the read+write split, so an
|
||||
// element that is both read and written is split with its own binding on it.
|
||||
if (!nonConsecutiveImageArrays.empty()) {
|
||||
Vector<String> declinedImageArrays;
|
||||
source = RemapImageArrayElementUnits(source, nonConsecutiveImageArrays,
|
||||
&declinedImageArrays);
|
||||
for (const auto& declined : declinedImageArrays) {
|
||||
// MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics
|
||||
// around it: this is the "linked, drew, produced wrong numbers, said
|
||||
// nothing" shape that cost earlier waves whole days, and one line per
|
||||
// declined array is bounded by program count. There is no honest GL answer
|
||||
// to give instead - the frontend has already reported LINK_STATUS = true.
|
||||
MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach "
|
||||
"from one declaration, so this stage will read and write the WRONG units. State "
|
||||
"program ID: %u, stage: %s.",
|
||||
declined.c_str(), stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
}
|
||||
// Wedged between those two on purpose:
|
||||
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
|
||||
// both halves of a split image is already the frontend texture unit (and so
|
||||
@@ -5953,6 +6447,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// declaration and preserves its binding - an image unit cannot be set from
|
||||
// the API in ES, so the qualifier is the only binding mechanism there is,
|
||||
// and both halves of the pair have to still be carrying theirs when it runs.
|
||||
// Takes no stage: the qualifier it adds is a decision about THIS text's accesses
|
||||
// and the rename that keeps two stages from declaring one image uniform
|
||||
// differently is keyed on that same decision, so two stages that agree still
|
||||
// share one uniform (see the location-budget note on the pass).
|
||||
Uint splitImageUniformCount = 0;
|
||||
source = SplitReadWriteImageUniforms(source, &splitImageUniformCount);
|
||||
if (splitImageUniformCount != 0) {
|
||||
@@ -6035,9 +6533,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// MobileGL creates without an owning wrapper to destroy it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
|
||||
// Kept AFTER every text-level pass, so what the synthesized control stage mirrors
|
||||
// is the text the driver actually sees, not an intermediate form.
|
||||
if (needsPassthroughTessControl) {
|
||||
if (glShaderType == GL_VERTEX_SHADER) {
|
||||
vertexStageEssl = source;
|
||||
} else if (glShaderType == GL_TESS_EVALUATION_SHADER) {
|
||||
tessEvalStageEssl = source;
|
||||
}
|
||||
}
|
||||
|
||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||
}
|
||||
|
||||
if (needsPassthroughTessControl) {
|
||||
AttachPassthroughTessControlStage(*stateProgramObject, tessEvalShaderIndex, shaderSpirvs,
|
||||
vertexStageEssl, tessEvalStageEssl);
|
||||
}
|
||||
|
||||
// A counter buffer declared by several stages was recorded once per stage; the draw
|
||||
// path binds per GL binding point, so collapse the duplicates here rather than
|
||||
// re-issuing the same glBindBufferBase two or three times every draw.
|
||||
|
||||
@@ -1222,6 +1222,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// counter sync at one empty-vector test.
|
||||
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
||||
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
||||
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
|
||||
// for, or -1 when this program needed no such stage. Another of the same shape as the
|
||||
// signatures above: the value is compiled INTO the synthesized stage as
|
||||
// `layout(vertices = N) out`, so a program built for one patch size is stale for
|
||||
// another and the draw path has to say so. -1 compares equal to itself for every
|
||||
// program that has a control stage of its own, i.e. for all but a handful.
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1270,6 +1279,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
||||
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
||||
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
|
||||
// stage has been attached and before the link; see the definition for why it cannot
|
||||
// regress a program that works today.
|
||||
void AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl);
|
||||
|
||||
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||
@@ -1307,6 +1326,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||
Vector<Int> m_atomicCounterGlBindings;
|
||||
Int m_atomicCounterEsslBindingTop = -1;
|
||||
// -1 for every program that has a tessellation control stage of its own (or none at
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
@@ -1398,6 +1421,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||
Bool needsExtendedImageFormats = false;
|
||||
// Some DECLARED format in play is one WidenImageFormatsForEssl will re-declare in a
|
||||
// core carrier. Answered from the uniform reflection rather than from a module parse
|
||||
// on purpose: the widening is armed on every driver, so a per-stage BuildModule to
|
||||
// find out would land on every stage of every program - which is the cost
|
||||
// SpirvGateFeatures exists to avoid. Program-wide, so it can over-arm a stage that
|
||||
// declares no image; the pass then finds nothing, reports no change, and the caller
|
||||
// keeps the module it already had.
|
||||
Bool declaresWidenableImageFormat = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
@@ -233,6 +234,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) {
|
||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const auto carrier = static_cast<GLenum>(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested));
|
||||
if (carrier == 0) {
|
||||
return {};
|
||||
}
|
||||
// EXACTLY the arming WidenImageFormatsForEssl uses, and it has to be: the shader, the
|
||||
// storage and the bind must all widen or none of them may, or the shader addresses a
|
||||
// texel size the storage does not have (which every driver tested accepts silently,
|
||||
// reading and writing out of bounds).
|
||||
//
|
||||
// A driver WITH GL_NV_image_formats can spell the narrow format - but only for the
|
||||
// formats SPIRV-Cross will actually print. It throws for its is_desktop_only_format
|
||||
// set instead of emitting a token, and the throw loses the stage whatever the driver
|
||||
// would have accepted: on Mesa, which advertises the extension, `layout(r8ui)
|
||||
// uimage2D` still lost its whole program until the widening ran for it too.
|
||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
||||
return {};
|
||||
}
|
||||
ImageBindableStorageWidening widening;
|
||||
widening.InternalFormat = carrier;
|
||||
widening.SourceChannels =
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ImageFormatChannelCount(requested);
|
||||
switch (carrier) {
|
||||
case GL_RGBA32UI:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RGBA32I:
|
||||
case GL_RGBA16I:
|
||||
case GL_RGBA8I:
|
||||
widening.IntegerData = true;
|
||||
break;
|
||||
default:
|
||||
widening.IntegerData = false;
|
||||
break;
|
||||
}
|
||||
// The carrier is a core ES format in every case, so it needs no fallback options of
|
||||
// its own; this call is only here to spell the transfer pair that describes it.
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
|
||||
nullptr, &widening.Format, &widening.Type);
|
||||
return widening;
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode) {
|
||||
@@ -700,6 +746,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, const Bool input) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Deliberately a scan for the DECLARATION rather than a regex over the whole text:
|
||||
// "gl_PerVertex" also appears inside the block's own body in some emissions, and the
|
||||
// direction keyword has to be the one immediately preceding the name for the match to
|
||||
// mean what this needs it to mean.
|
||||
const auto isIdentifierChar = [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
||||
};
|
||||
const String keyword = input ? String("in") : String("out");
|
||||
SizeT pos = 0;
|
||||
while ((pos = essl.find("gl_PerVertex", pos)) != String::npos) {
|
||||
// Walk back over whitespace to the direction keyword.
|
||||
SizeT before = pos;
|
||||
while (before > 0 && std::isspace(static_cast<unsigned char>(essl[before - 1]))) --before;
|
||||
const Bool matches = before >= keyword.size() &&
|
||||
essl.compare(before - keyword.size(), keyword.size(), keyword) == 0 &&
|
||||
(before == keyword.size() ||
|
||||
!isIdentifierChar(essl[before - keyword.size() - 1]));
|
||||
if (!matches) {
|
||||
pos += 1;
|
||||
continue;
|
||||
}
|
||||
const SizeT open = essl.find('{', pos);
|
||||
if (open == String::npos) return std::nullopt;
|
||||
const SizeT close = essl.find('}', open);
|
||||
if (close == String::npos) return std::nullopt;
|
||||
return essl.substr(open + 1, close - open - 1);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Tessellation is core in ES 3.2 and reachable in 3.1 only through
|
||||
// GL_EXT_tessellation_shader. The caller has already established that the driver runs
|
||||
// the evaluation stage at all, so the only question here is which spelling to use.
|
||||
const Bool core = esslVersion >= 320;
|
||||
String source = "#version " + std::to_string(core ? 320u : 310u) + " es\n";
|
||||
if (!core) {
|
||||
source += "#extension GL_EXT_tessellation_shader : require\n";
|
||||
}
|
||||
source += "precision highp float;\n";
|
||||
source += "precision highp int;\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// Mirrored, never invented. An empty member list means the neighbouring stage did not
|
||||
// redeclare the block either, and the driver's own built-in declaration is then what
|
||||
// both sides agree on - redeclaring here would be the thing that broke the match.
|
||||
if (!inPerVertexMembers.empty()) {
|
||||
source += "in gl_PerVertex {" + inPerVertexMembers + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
}
|
||||
if (!outPerVertexMembers.empty()) {
|
||||
source += "out gl_PerVertex {" + outPerVertexMembers + "} gl_out[];\n";
|
||||
}
|
||||
source += "void main() {\n";
|
||||
// Only gl_Position is forwarded. That is the whole of what the pass-through owes the
|
||||
// evaluation stage: a program whose evaluation stage reads anything else per-vertex
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsImagePassIdentifierChar(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
@@ -811,6 +933,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
struct ImageUniformDecl {
|
||||
String name;
|
||||
String aliasName; // the repair-tagged name the rewritten declaration takes; empty
|
||||
// for a declaration this pass leaves alone
|
||||
String writeName; // the writeonly half's name, when split
|
||||
String layout; // raw contents of layout(...)
|
||||
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
||||
@@ -855,11 +979,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return out;
|
||||
}
|
||||
|
||||
// A name for the writeonly half that no identifier in the shader (and no other
|
||||
// half already minted) can collide with.
|
||||
String MakeImageWriteAliasName(const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
|
||||
// A name for a rewritten declaration that no identifier in the shader (and no other
|
||||
// alias already minted for this stage) can collide with.
|
||||
String MakeImageAliasName(const String& prefix, const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = prefix + name;
|
||||
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
||||
// that already starts with '_' would otherwise produce.
|
||||
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
||||
@@ -905,6 +1029,236 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
// The digits of an array extent or of an element subscript, or -1 for "not a plain
|
||||
// decimal literal".
|
||||
//
|
||||
// One trailing `u`/`U` is PART of the literal rather than grounds for rejection.
|
||||
// SPIRV-Cross prints an index in the type SPIR-V gave it, and
|
||||
// LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
||||
// index it replaced (ConstantLikeIndex reads that index's own type_id), so an image
|
||||
// array reached through anything unsigned - `for (uint i = 0u; i < 4u; ++i)`, or any
|
||||
// expression on gl_LocalInvocationIndex, which is uint by definition - arrives here
|
||||
// spelled `g_image[0u]`. Reading that as "not a literal" declined the array and left
|
||||
// it on one layout(binding = N), which hands its elements the consecutive units
|
||||
// N, N+1, ... - exactly the silently-wrong-units defect the split exists to remove.
|
||||
Int ParseNonNegativeIntLiteral(const String& text) {
|
||||
if (text.empty()) return -1;
|
||||
SizeT digitCount = text.size();
|
||||
if (text[digitCount - 1] == 'u' || text[digitCount - 1] == 'U') --digitCount;
|
||||
if (digitCount == 0) return -1;
|
||||
Int value = 0;
|
||||
for (SizeT i = 0; i < digitCount; ++i) {
|
||||
const char c = text[i];
|
||||
if (c < '0' || c > '9') return -1;
|
||||
value = value * 10 + (c - '0');
|
||||
if (value > 4096) return -1; // no image array is anywhere near this
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (outDeclined != nullptr) outDeclined->clear();
|
||||
if (plans.empty() || glslCode.find("image") == String::npos) return glslCode;
|
||||
|
||||
// Same declaration shape as the split pass reads, with the array extent captured.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]*)\s*\])?\s*;)");
|
||||
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
|
||||
|
||||
struct StageImageDecl {
|
||||
String name;
|
||||
String layout;
|
||||
String qualifiers;
|
||||
String type;
|
||||
Int elementCount = 1;
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
};
|
||||
// Every image declaration in the stage; the plans are program-wide and name arrays
|
||||
// this stage may not declare at all.
|
||||
Vector<StageImageDecl> decls;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
StageImageDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
decl.qualifiers = NormalizeDeclarationSpacing(match[2].str());
|
||||
decl.type = match[3].str();
|
||||
decl.name = match[4].str();
|
||||
decl.elementCount = match[5].matched ? ParseNonNegativeIntLiteral(match[5].str()) : 1;
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenNames;
|
||||
for (const ImageArrayUnitPlan& plan : plans) {
|
||||
const auto decline = [&](const char* why) {
|
||||
if (outDeclined != nullptr) outDeclined->push_back(plan.name + ": " + why);
|
||||
};
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
const StageImageDecl* decl = nullptr;
|
||||
for (const auto& candidate : decls) {
|
||||
if (candidate.name == plan.name) {
|
||||
decl = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (decl == nullptr) {
|
||||
// Absent from this stage entirely is the normal outcome - the reflection is
|
||||
// program-wide and this pass runs per stage. Named but not RECOGNIZED is not:
|
||||
// it means the declaration is spelled in some shape the regex above does not
|
||||
// read, and staying quiet about that is how the wrong units got shipped.
|
||||
if (ContainsIdentifier(glslCode, plan.name)) {
|
||||
decline("the stage names it but declares it in a shape this pass cannot read");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (decl->elementCount < 0 || static_cast<SizeT>(decl->elementCount) != plan.units.size()) {
|
||||
decline("the emitted array extent disagrees with the reflected element count");
|
||||
continue;
|
||||
}
|
||||
|
||||
Bool consecutive = true;
|
||||
Bool everyElementHasAUnit = true;
|
||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
||||
const Int unit = plan.units[element];
|
||||
if (unit < 0) {
|
||||
everyElementHasAUnit = false;
|
||||
break;
|
||||
}
|
||||
if (unit != plan.units[0] + static_cast<Int>(element)) consecutive = false;
|
||||
}
|
||||
if (!everyElementHasAUnit) {
|
||||
decline("an element has no image unit");
|
||||
continue;
|
||||
}
|
||||
// Already exactly what ESSL would do on its own. The caller filters these out;
|
||||
// repeating the test here keeps the pass correct on its own terms.
|
||||
if (consecutive) continue;
|
||||
|
||||
// Every use has to be `name[<literal>]`. The literal is what the split turns
|
||||
// into a name, and by the time this runs there is always one:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every
|
||||
// dynamic image-array subscript in the module, because ESSL forbids one
|
||||
// outright ("image arrays indexed with non-constant expressions are forbidden
|
||||
// in GLSL ES"). A subscript that is still an expression here is therefore a
|
||||
// stage that was never going to compile, and guessing which element it meant
|
||||
// would only change which unit it addressed wrongly.
|
||||
struct ElementUse {
|
||||
SizeT start; // the first character of the name
|
||||
SizeT length; // through the closing ']'
|
||||
SizeT element;
|
||||
};
|
||||
Vector<ElementUse> uses;
|
||||
const char* refusal = nullptr;
|
||||
for (SizeT pos = glslCode.find(plan.name); pos != String::npos;
|
||||
pos = glslCode.find(plan.name, pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue;
|
||||
const SizeT after = pos + plan.name.size();
|
||||
if (after < glslCode.size() && IsImagePassIdentifierChar(glslCode[after])) continue;
|
||||
if (pos >= decl->declStart && pos < decl->declStart + decl->declLength) {
|
||||
continue; // the declaration's own name
|
||||
}
|
||||
const SizeT open = glslCode.find_first_not_of(" \t\r\n", after);
|
||||
if (open == String::npos || glslCode[open] != '[') {
|
||||
refusal = "it is reached by something other than a subscript, so there is no "
|
||||
"element index to rewrite";
|
||||
break;
|
||||
}
|
||||
Int depth = 0;
|
||||
SizeT scan = open;
|
||||
for (; scan < glslCode.size(); ++scan) {
|
||||
if (glslCode[scan] == '[') {
|
||||
++depth;
|
||||
} else if (glslCode[scan] == ']' && --depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scan >= glslCode.size() || open + 1 >= scan) {
|
||||
refusal = "it is reached by something other than a subscript, so there is no "
|
||||
"element index to rewrite";
|
||||
break;
|
||||
}
|
||||
const Int element = ParseNonNegativeIntLiteral(
|
||||
NormalizeDeclarationSpacing(glslCode.substr(open + 1, scan - open - 1)));
|
||||
if (element < 0 || element >= decl->elementCount) {
|
||||
refusal = "its subscript is not a literal element index, so which unit the "
|
||||
"access reaches cannot be decided here";
|
||||
break;
|
||||
}
|
||||
uses.push_back({pos, scan + 1 - pos, static_cast<SizeT>(element)});
|
||||
}
|
||||
if (refusal != nullptr) {
|
||||
decline(refusal);
|
||||
continue;
|
||||
}
|
||||
|
||||
// One SCALAR declaration per element, each carrying its own binding. ESSL nails
|
||||
// an ARRAY's elements to consecutive units and offers no way to move them, so
|
||||
// the only spelling that reaches an arbitrary set of units is one declaration
|
||||
// per unit - and with every subscript a literal, every use has exactly one of
|
||||
// them to be rewritten to.
|
||||
//
|
||||
// It costs precisely the image uniforms the application declared, which is why
|
||||
// there is no budget test here: an array of four elements becomes four scalars
|
||||
// however far apart their units are.
|
||||
const SizeT elementCount = plan.units.size();
|
||||
Vector<String> elementNames;
|
||||
String replacement;
|
||||
for (SizeT element = 0; element < elementCount; ++element) {
|
||||
const String elementName =
|
||||
MakeImageAliasName(IMAGE_ARRAY_ELEMENT_PREFIX,
|
||||
plan.name + "_" + std::to_string(element), glslCode, takenNames);
|
||||
takenNames.push_back(elementName);
|
||||
elementNames.push_back(elementName);
|
||||
|
||||
String layout = decl->layout;
|
||||
const String bindingText = "binding = " + std::to_string(plan.units[element]);
|
||||
if (std::regex_search(layout, bindingValueRegex)) {
|
||||
layout = std::regex_replace(layout, bindingValueRegex, bindingText);
|
||||
} else {
|
||||
layout = bindingText + (layout.empty() ? String() : ", " + layout);
|
||||
}
|
||||
if (element != 0) replacement += '\n';
|
||||
replacement += "layout(" + layout + ") uniform ";
|
||||
if (!decl->qualifiers.empty()) {
|
||||
replacement += decl->qualifiers;
|
||||
replacement += ' ';
|
||||
}
|
||||
replacement += decl->type + " " + elementName + ";";
|
||||
}
|
||||
edits.push_back({decl->declStart, decl->declLength, Move(replacement)});
|
||||
|
||||
// `name[k]` -> the scalar declared for element k, subscript and all.
|
||||
for (const ElementUse& use : uses) {
|
||||
edits.push_back({use.start, use.length, elementNames[use.element]});
|
||||
}
|
||||
}
|
||||
if (edits.empty()) return glslCode;
|
||||
|
||||
// Back to front, so an earlier edit's offsets stay valid. No two edits overlap: each
|
||||
// one covers either a whole declaration or a whole `name[k]`, the declaration's own
|
||||
// name is skipped when the uses are collected, and one occurrence of a name yields at
|
||||
// most one edit.
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
||||
String result = glslCode;
|
||||
for (const ImageSourceEdit& edit : edits) {
|
||||
result.replace(edit.start, edit.length, edit.text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -968,13 +1322,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
|
||||
// Walk every `image*(` call and attribute its first argument to a declaration.
|
||||
struct StoreSite {
|
||||
// EVERY recognized use is recorded, not only the stores: a declaration this pass
|
||||
// renames has to take all of its uses with it, and the "every occurrence was one I
|
||||
// saw" check below is what makes the recorded set provably the complete set.
|
||||
struct ImageUseSite {
|
||||
SizeT declIndex;
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
SizeT callOpen; // the '(' of the call this argument belongs to
|
||||
Bool stores; // an imageStore, i.e. the use a split redirects to the write half
|
||||
};
|
||||
Vector<StoreSite> storeSites;
|
||||
Vector<ImageUseSite> useSites;
|
||||
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
||||
SizeT tokenEnd = pos;
|
||||
@@ -1019,12 +1377,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
switch (ClassifyImageBuiltin(builtin)) {
|
||||
case ImageBuiltinAccess::Load:
|
||||
decl.loaded = true;
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
||||
break;
|
||||
case ImageBuiltinAccess::Store:
|
||||
decl.stored = true;
|
||||
storeSites.push_back({declIndex, argStart, argEnd - argStart, openParen});
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, true});
|
||||
break;
|
||||
case ImageBuiltinAccess::None:
|
||||
// imageSize/imageSamples touch nothing, but they still NAME the variable, so
|
||||
// a rename has to reach them.
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
||||
break;
|
||||
default:
|
||||
decl.unknownUse = true;
|
||||
@@ -1041,12 +1403,54 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenAliases;
|
||||
Vector<String> takenNames;
|
||||
for (auto& decl : decls) {
|
||||
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
||||
// EVERY declaration this pass rewrites is also RENAMED, under the prefix of the
|
||||
// repair it is about to receive - the qualifier below is a decision about ONE
|
||||
// STAGE's accesses, and GLSL requires a uniform declared in two stages to be
|
||||
// declared IDENTICALLY (GLSL 4.3 4.3.9 / GLSL ES 3.20 4.3.9). A shader that
|
||||
// stores to an image in the vertex stage and loads it in the fragment stage gets
|
||||
// `writeonly` on one and `readonly` on the other, and on Adreno the linker merges
|
||||
// the two same-named declarations and SILENTLY DISCARDS the vertex-stage stores:
|
||||
// no GL error, no link log, LINK_STATUS = 1, and the image still holding its
|
||||
// initial contents afterwards
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation, and any
|
||||
// shader pack that writes an image in one stage to read it in another).
|
||||
//
|
||||
// Keyed on the REPAIR and not on the stage, which is what makes the rename
|
||||
// exactly as wide as the problem. Two stages that use the image the same way
|
||||
// reach the same prefix and emit byte-identical declarations, so they keep ONE
|
||||
// shared uniform and there is nothing mismatched to merge; two that use it
|
||||
// differently reach different prefixes and cannot be merged at all. Tagging by
|
||||
// stage instead also broke the merge - but it broke it for the agreeing stages
|
||||
// too, turning one image uniform into one PER STAGE that names it, and Adreno
|
||||
// allocates image locations per distinct uniform: the five stages of
|
||||
// KHR-GL43.shading_language_420pack.binding_images_texture_type_* went from 6
|
||||
// image uniforms to 30 and the link failed outright with "Error: Image Image
|
||||
// location or component exceeds max allowed." on an Adreno 830, where Mali and
|
||||
// Mesa both accept the same text.
|
||||
//
|
||||
// Nothing downstream reads these names: the two passes that key on the GL uniform
|
||||
// name (RebindImageUniformsToFrontendUnits, BakeImageFormatQualifiers) both run
|
||||
// BEFORE this one, RemoveLayoutBinding recognises an image declaration by its TYPE
|
||||
// token, and CacheResourceLocations skips image uniforms outright because ES image
|
||||
// units come only from layout(binding=N). The declarations this pass LEAVES ALONE -
|
||||
// already readonly/writeonly in the source, or r32f/r32i/r32ui, which need no
|
||||
// qualifier - keep their names, and they are exactly the ones that already match
|
||||
// across stages.
|
||||
const char* aliasPrefix = decl.loaded && decl.stored ? IMAGE_SPLIT_READ_ALIAS_PREFIX
|
||||
: decl.stored ? IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
: IMAGE_READONLY_ALIAS_PREFIX;
|
||||
decl.aliasName = MakeImageAliasName(aliasPrefix, decl.name, glslCode, takenNames);
|
||||
takenNames.push_back(decl.aliasName);
|
||||
if (decl.loaded && decl.stored) {
|
||||
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||
takenAliases.push_back(decl.writeName);
|
||||
// Minted from the ALREADY access-tagged name, so the write half of a split
|
||||
// can never collide with the single declaration another stage's repair mints
|
||||
// for the same image.
|
||||
decl.writeName =
|
||||
MakeImageAliasName(IMAGE_WRITE_ALIAS_PREFIX, decl.aliasName, glslCode, takenNames);
|
||||
takenNames.push_back(decl.writeName);
|
||||
decl.split = true;
|
||||
if (outSplitCount != nullptr) ++*outSplitCount;
|
||||
// Both halves carry `coherent`; see BuildImageDeclaration. The
|
||||
@@ -1054,24 +1458,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// there is no visibility to restore and no reason to pay for the cache
|
||||
// behaviour.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
|
||||
BuildImageDeclaration(decl, "readonly", decl.aliasName,
|
||||
/*forceCoherent=*/true) +
|
||||
"\n" +
|
||||
BuildImageDeclaration(decl, "writeonly", decl.writeName,
|
||||
/*forceCoherent=*/true)});
|
||||
} else if (decl.stored) {
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||
BuildImageDeclaration(decl, "writeonly", decl.aliasName)});
|
||||
} else {
|
||||
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
||||
// the qualifier that keeps every one of those legal.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name)});
|
||||
BuildImageDeclaration(decl, "readonly", decl.aliasName)});
|
||||
}
|
||||
}
|
||||
for (const StoreSite& site : storeSites) {
|
||||
for (const ImageUseSite& site : useSites) {
|
||||
const ImageUniformDecl& decl = decls[site.declIndex];
|
||||
if (!decl.split) continue;
|
||||
edits.push_back({site.start, site.length, decl.writeName});
|
||||
// Empty exactly when the declaration was poisoned above and left untouched; its
|
||||
// uses must keep naming the variable that is still called that.
|
||||
if (decl.aliasName.empty()) continue;
|
||||
edits.push_back(
|
||||
{site.start, site.length, decl.split && site.stores ? decl.writeName : decl.aliasName});
|
||||
if (!decl.split || !site.stores) continue;
|
||||
// ...and an explicit barrier behind it. `coherent` on both halves is what makes
|
||||
// the store VISIBLE to a load through the other variable, but it says nothing
|
||||
// about ORDER within one invocation - and the whole reason a declaration is split
|
||||
|
||||
@@ -60,6 +60,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
|
||||
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
||||
// GLSL ES cannot spell as an image is carried by one it can.
|
||||
//
|
||||
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
||||
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
||||
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
||||
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
||||
// Seventeen have a core format of the SAME per-channel width and component type,
|
||||
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
||||
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so the carrier's surplus channels
|
||||
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
||||
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
||||
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
||||
//
|
||||
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
||||
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
||||
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
||||
// diagnostic rather than a silent approximation), and on a driver that HAS
|
||||
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
||||
// happen behind it.
|
||||
//
|
||||
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
||||
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
||||
// attachment behaviour, so the image constraint wins. In practice that only bites
|
||||
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
||||
// signed-normalized instead of becoming the half float that fallback would have picked -
|
||||
// so an image-bound texture in one of those two formats is no longer attachable, and
|
||||
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
||||
// does not update. Accepted deliberately: before the widening, an image binding in either
|
||||
// format was refused outright by every driver tested and the stage that declared it never
|
||||
// compiled at all, so nothing that works today is being given up.
|
||||
//
|
||||
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
||||
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
||||
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
||||
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
||||
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
||||
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
||||
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
||||
// from "alpha" to a channel count, which is its own change.
|
||||
struct ImageBindableStorageWidening {
|
||||
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
||||
GLenum Format = GL_UNKNOWN_MGL;
|
||||
GLenum Type = GL_UNKNOWN_MGL;
|
||||
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
||||
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
||||
Uint SourceChannels = 0;
|
||||
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||
// RG8UI), so the carrier decides.
|
||||
Bool IntegerData = false;
|
||||
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
@@ -178,9 +235,113 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
||||
// image array into; the suffix is the array's own name and the element's index.
|
||||
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
||||
// One image ARRAY whose elements the application pointed at units that are not
|
||||
// consecutive-from-element-zero.
|
||||
struct ImageArrayUnitPlan {
|
||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||
Vector<Int> units; // the frontend image unit element k has to reach
|
||||
};
|
||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||
// wrong value and three were never written, with no GL error and no link log. The same
|
||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
||||
// INVALID_OPERATION.
|
||||
//
|
||||
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
||||
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
||||
// element k. One declaration carries one binding, so one declaration per unit is the
|
||||
// only spelling that reaches an arbitrary set of them.
|
||||
//
|
||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||
// widening the array to cover the whole span of units and routing each subscript through
|
||||
// a `const highp int` offset table - was written before that pass covered images, and
|
||||
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
||||
// The split also costs exactly the image uniforms the application declared, where the
|
||||
// widening cost the whole SPAN (seven for the four elements of
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
||||
// fail to fit in.
|
||||
//
|
||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||
// caller to report - when the emitted extent disagrees with the reflection, when the
|
||||
// array is reached by anything other than a subscript, or when a subscript is not a
|
||||
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
||||
//
|
||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
||||
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
||||
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
||||
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined = nullptr);
|
||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||
// `out` one.
|
||||
//
|
||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||
// which members, depends on what the application's shader touched; a synthesized stage
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||
// bound in its place, and every draw silently renders nothing.
|
||||
//
|
||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
||||
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
||||
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
||||
// two that use it differently arrive at different ones - which is exactly the property
|
||||
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
||||
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
||||
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
||||
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||
@@ -192,18 +353,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||
// up as a device compile failure - and then as a silently no-op draw.
|
||||
//
|
||||
// Restores a legal declaration:
|
||||
// * loaded only -> add `readonly`
|
||||
// * stored only -> add `writeonly`
|
||||
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
||||
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
||||
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `coherent readonly <name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, point
|
||||
// same type, `coherent readonly
|
||||
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
||||
// every imageStore at the second one, and follow each of
|
||||
// those stores with `memoryBarrierImage();`. Several image
|
||||
// variables may share an image unit as long as they have
|
||||
// the same type and format, which is exactly what the pair
|
||||
// is.
|
||||
//
|
||||
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
||||
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
||||
// declared in two stages to be declared identically - so a shader that stores an image from
|
||||
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
||||
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
||||
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
||||
// still reads back its initial contents
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
||||
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
||||
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
||||
//
|
||||
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
||||
// point: two stages that use an image the same way emit byte-identical declarations, so
|
||||
// letting them keep one shared name costs nothing and merging them is correct, while two
|
||||
// stages that use it differently land on different prefixes and cannot be merged at all.
|
||||
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
||||
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
||||
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
||||
// binding_images_texture_type_* declares three read+write images in each of its five
|
||||
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// The declarations this pass leaves untouched keep their names, and those are exactly the
|
||||
// ones that already agree across stages.
|
||||
//
|
||||
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||
// one when both are coherent, and the split is what makes a same-variable
|
||||
@@ -227,7 +418,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||
// already rewritten to and must run before those bindings are stripped - see the call
|
||||
// site in Managers.cpp.
|
||||
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
||||
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
||||
//
|
||||
// `outSplitCount`, when given, receives the number of declarations that were actually
|
||||
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
||||
|
||||
@@ -2094,7 +2094,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2.
|
||||
// This value becomes the storage image VIEW's format while the image itself was created
|
||||
// from the texture's internal format, so the two must name the same bit layout or the
|
||||
// shader reads the texel through a different component order than the host wrote it.
|
||||
// GL_RGB10_A2 with GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, G in 10-19, B in
|
||||
// 20-29 and A in 30-31, which is Vulkan's A2B10G10R10; A2R10G10B10 transposes R and B.
|
||||
// KHR-GL43.shader_image_load_store.basic-allFormats-store read back [2,1,0,3] for an
|
||||
// rgb10_a2ui image stored as [0,1,2,3] while these two converters disagreed.
|
||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||
@@ -2117,7 +2125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2B10G10R10_UINT_PACK32; // see Rgb10A2 above
|
||||
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||
|
||||
@@ -230,8 +230,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||
// converted, so it is not constrained here.
|
||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||
//
|
||||
// "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
|
||||
// primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
|
||||
// is indistinguishable from no geometry shader by its reflected input type alone. The
|
||||
// sentinel test this replaces therefore skipped the whole rule for exactly the geometry
|
||||
// shaders whose input is the most restrictive one - every mode but GL_POINTS was
|
||||
// accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
|
||||
// program with GL_LINES and requires INVALID_OPERATION).
|
||||
const Bool geometryActive =
|
||||
currentProgram && currentProgram->GetShaderIndexByStage(ShaderStage::Geometry) >= 0;
|
||||
const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||
if (geometryActive && mode != GL_PATCHES) {
|
||||
Bool compatible = false;
|
||||
switch (gsInput) {
|
||||
case GL_POINTS:
|
||||
|
||||
@@ -81,11 +81,14 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/ImageFormatQualifierScenario.cpp
|
||||
Scenarios/NonCoreImageFormatScenario.cpp
|
||||
Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||
Scenarios/TessellationDrawModeScenario.cpp
|
||||
Scenarios/GeometryDrawModeScenario.cpp
|
||||
Scenarios/FormatlessImageBakeScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FormatlessImageBakeScenario.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 - A FORMAT-LESS IMAGE UNIFORM WHOSE UNIT HOLDS A NON-CORE FORMAT.
|
||||
//
|
||||
// GLSL 4.20 lets a write-only image uniform omit its layout format; GLSL ES demands one, so
|
||||
// DirectGLES BAKES the format of whatever glBindImageTexture put on the unit into the
|
||||
// declaration. When that format is outside the GLSL ES core thirteen, the bake alone is not
|
||||
// enough - the baked declaration then has to go through the same channel-widening
|
||||
// WidenImageFormatsForEssl gives a DECLARED non-core format (see NonCoreImageFormatScenario for
|
||||
// the widening itself).
|
||||
//
|
||||
// The two routes had different arming. The declared route armed the widening on the format
|
||||
// alone; the baked route armed it only when the driver lacked GL_NV_image_formats. That reads
|
||||
// like an optimisation and is not one: SPIRV-Cross throws for its is_desktop_only_format set the
|
||||
// moment it targets ESSL, whatever the driver would have accepted, so on a driver that HAS the
|
||||
// extension the shader half of the widening stayed switched off while TextureImpl's storage/bind
|
||||
// half - which keys on SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran. The
|
||||
// stage threw, the program linked without it, and every dispatch silently did nothing.
|
||||
//
|
||||
// KHR-GL43.stencil_texturing.functional is where it surfaced: its compute half writes through a
|
||||
// format-less `uimage2D` bound to an R8UI texture, and returned zeros for every texel.
|
||||
//
|
||||
// DISCRIMINATING ONLY WHERE THE DRIVER ADVERTISES GL_NV_image_formats - Mesa does, which is what
|
||||
// the software lanes run and where this was found. On Adreno 830 and both Malis the extension is
|
||||
// absent, the old code already armed the widening, and these cases pass before and after; they
|
||||
// are kept running there as a guard against the opposite mistake.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#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 {
|
||||
|
||||
constexpr int kExtent = 8;
|
||||
|
||||
// No layout format on uni_image on purpose: that is the whole subject. uni_source is a
|
||||
// plain integer texture so nothing but the image declaration is in play.
|
||||
const char* const kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
uniform usampler2D uni_source;
|
||||
void main()
|
||||
{
|
||||
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(uni_image, at, uvec4(texelFetch(uni_source, at, 0).r, 0u, 0u, 0u));
|
||||
}
|
||||
)";
|
||||
|
||||
class FormatlessImageBakeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsCompute()) {
|
||||
GTEST_SKIP() << "no compute stage on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsCompute() {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
DrainErrors();
|
||||
return maxImageUnits >= 2;
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint BuildCompute(const char* source, std::string& log) {
|
||||
const GLuint cs = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(cs, 1, &source, nullptr);
|
||||
glCompileShader(cs);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(cs, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char buffer[2048] = "";
|
||||
glGetShaderInfoLog(cs, sizeof(buffer), nullptr, buffer);
|
||||
log = buffer;
|
||||
glDeleteShader(cs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, cs);
|
||||
glLinkProgram(program);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
glDeleteShader(cs);
|
||||
if (!ok) {
|
||||
char buffer[2048] = "";
|
||||
glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer);
|
||||
log = buffer;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// internalFormat is the NON-CORE image format under test; the destination texture and
|
||||
// the glBindImageTexture argument both use it, and the shader declares nothing.
|
||||
void RunCopy(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType) {
|
||||
std::vector<GLuint> expected(kExtent * kExtent);
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
expected[i] = static_cast<GLuint>(1 + i);
|
||||
}
|
||||
|
||||
// Source: a core-format integer texture holding 1..64.
|
||||
std::vector<GLubyte> sourceBytes(kExtent * kExtent);
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
sourceBytes[i] = static_cast<GLubyte>(expected[i]);
|
||||
}
|
||||
GLuint sourceTexture = 0;
|
||||
glGenTextures(1, &sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, kExtent, kExtent);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_BYTE,
|
||||
sourceBytes.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Destination: the format under test, zero-filled so "the dispatch did nothing"
|
||||
// and "the dispatch wrote zeros" are the same observation the CTS made.
|
||||
GLuint destTexture = 0;
|
||||
glGenTextures(1, &destTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
const std::vector<GLubyte> zeros(static_cast<std::size_t>(kExtent) * kExtent * 8, 0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, zeros.data());
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "destination storage";
|
||||
|
||||
std::string log;
|
||||
const GLuint program = BuildCompute(kComputeSource, log);
|
||||
ASSERT_NE(program, 0u) << "the format-less image program did not build: " << log;
|
||||
|
||||
glUseProgram(program);
|
||||
glBindImageTexture(1, destTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, internalFormat);
|
||||
glUniform1i(glGetUniformLocation(program, "uni_image"), 1);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "uni_source"), 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "binding";
|
||||
|
||||
glDispatchCompute(kExtent, kExtent, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "dispatch";
|
||||
|
||||
std::vector<GLuint> readback(kExtent * kExtent, 0xFFFFFFFFu);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, readback.data());
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "readback";
|
||||
|
||||
int offenders = 0;
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
if (readback[i] != expected[i]) ++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0) << "the dispatch wrote " << offenders << " of "
|
||||
<< (kExtent * kExtent) << " texels wrongly; texel 0 was "
|
||||
<< readback[0] << ", expected " << expected[0]
|
||||
<< ". A whole stage lost to the ESSL emitter looks exactly like this.";
|
||||
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteTextures(1, &sourceTexture);
|
||||
glDeleteTextures(1, &destTexture);
|
||||
DrainErrors();
|
||||
}
|
||||
};
|
||||
|
||||
// R8UI: one of the seven formats GLSL ES reaches only through GL_NV_image_formats AND one
|
||||
// SPIRV-Cross refuses to print for ESSL, so it needs the widening in both driver modes.
|
||||
TEST_F(FormatlessImageBakeScenario, R8uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE);
|
||||
}
|
||||
|
||||
// R16UI, from the same set, carried in RGBA16UI: the fix must not be R8UI-shaped.
|
||||
TEST_F(FormatlessImageBakeScenario, R16uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
||||
}
|
||||
|
||||
// The control: R32UI is in the GLSL ES core thirteen, so it is baked and never widened.
|
||||
// It passed before the fix and has to keep passing.
|
||||
TEST_F(FormatlessImageBakeScenario, CoreFormatBakedFromTheBoundUnitIsUnaffected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,298 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.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 - A GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
|
||||
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
|
||||
//
|
||||
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
|
||||
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
|
||||
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
|
||||
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
|
||||
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
|
||||
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
|
||||
//
|
||||
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
|
||||
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
|
||||
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
|
||||
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
|
||||
// program is the control that proves the rule was not simply widened.
|
||||
//
|
||||
// Needs a real context: the validator returns before this rule when no backend object is
|
||||
// active, so the GPU-free negative-API suite cannot reach it.
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#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 {
|
||||
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
|
||||
// `result` is here so the same program can be captured with transform feedback.
|
||||
const char* const kPointsInGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float result;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
result = 1.0;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kLinesInGeometrySource = R"(#version 420 core
|
||||
layout(lines) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class GeometryDrawModeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "); there is no input primitive to validate";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
|
||||
// driver without GL_EXT_geometry_shader and on a DirectVulkan device without the
|
||||
// geometryShader feature.
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
|
||||
const std::vector<std::pair<GLenum, const char*>> stages = {
|
||||
{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (!compiled) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (capturedVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
if (!linked) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// GL_POINTS is the only mode that decomposes into a points input primitive.
|
||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kPointsInGeometrySource);
|
||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
for (const GLenum mode :
|
||||
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
|
||||
glDrawArrays(mode, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "mode " << mode << " does not decompose into the geometry shader's points input";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The one mode that IS compatible still draws.
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
|
||||
// case asks about. The capture span is really completed first, so GL_POINTS comes back
|
||||
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
|
||||
// never-ended-a-span reason instead and the case could not tell the two apart.
|
||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
|
||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||
|
||||
GLuint feedback = 0;
|
||||
glGenTransformFeedbacks(1, &feedback);
|
||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
|
||||
|
||||
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
|
||||
DrainErrors();
|
||||
|
||||
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
|
||||
DrainErrors();
|
||||
|
||||
// The compatible mode replays the captured span with no error at all, which is what
|
||||
// makes the two assertions above about the MODE and not about the span.
|
||||
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "a compatible mode must still replay the captured span";
|
||||
DrainErrors();
|
||||
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
|
||||
glDeleteTransformFeedbacks(1, &feedback);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
|
||||
// the rule even before the fix. It must still accept the line modes and still reject the
|
||||
// others - a fix that widened the rule instead of repairing its guard breaks this.
|
||||
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kLinesInGeometrySource);
|
||||
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||
static_cast<GLenum>(GL_LINE_LOOP)}) {
|
||||
glDrawArrays(mode, 0, 2);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "mode " << mode << " decomposes into lines and must be accepted";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
|
||||
glDrawArrays(mode, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "mode " << mode << " does not decompose into lines";
|
||||
DrainErrors();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -127,14 +127,23 @@ void main()
|
||||
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||
// spelling in a single declaration and cannot be expressed at all without splitting
|
||||
// the array into one declaration per element and rewriting every use of it.
|
||||
// spelling in a single declaration.
|
||||
//
|
||||
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
||||
// storage-block rebinding cases: the defect is per-backend and the frontend
|
||||
// mechanism these cases exist for - per-element units surviving the trip to the
|
||||
// pipeline composite - is fully exercised on Magma.
|
||||
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
// RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
|
||||
// image uniform per element, each carrying its own binding, which costs exactly the
|
||||
// four image uniforms the application declared. (It used to WIDEN the array to cover
|
||||
// the whole span instead, which cost seven for those four elements and had to be
|
||||
// declined on a stage that could not afford them - hence the budget gate that used
|
||||
// to be here.) DirectVulkan needs no rewrite at all.
|
||||
bool PerElementImageUnitsAreHonoured() const {
|
||||
if (Gl().BackendName() == "DirectVulkan") return true;
|
||||
GLint maxFragmentImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
// One per element of the four-element array either fragment program declares.
|
||||
return maxFragmentImageUniforms >= 4;
|
||||
}
|
||||
|
||||
// The scenarios below need image load/store at all; a driver without it should skip
|
||||
// rather than fail.
|
||||
@@ -164,7 +173,7 @@ void main()
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
GTEST_SKIP() << "fewer than 4 fragment image uniforms: the array under test does not fit";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
@@ -283,8 +292,12 @@ void main()
|
||||
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
|
||||
// here are consecutive on purpose, so on Espryt this would exercise nothing the case
|
||||
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
|
||||
// it deliberately never triggers.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.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 - AN IMAGE FORMAT GLSL ES CANNOT SPELL.
|
||||
//
|
||||
// GL 4.2 has forty image formats; GLSL ES core has thirteen, and GL_NV_image_formats - the only
|
||||
// thing that adds the rest - is advertised by none of Adreno 830, Mali-G1-Ultra MC12 or
|
||||
// Mali-G925-Immortalis MC12. A shader that declares one of the other twenty-six therefore has no
|
||||
// legal ESSL at all: SPIRV-Cross throws for some of them and the driver rejects the token for the
|
||||
// rest ("'rg32f' : not a legal layout qualifier id"), and dropping the qualifier is refused too
|
||||
// ("all images have to define layout format"). glBindImageTexture will not take the narrow format
|
||||
// either - GL_INVALID_VALUE for nineteen of the twenty-six on Adreno, twenty-five on both Malis.
|
||||
// The stage is lost, the program is "linked but not drawable", and every dispatch silently does
|
||||
// nothing: KHR-GL43.shader_image_load_store.basic-allFormats-*, single-byte_data_alignment and
|
||||
// multiple-uniforms are all that one defect.
|
||||
//
|
||||
// Espryt emulates the seventeen formats that have a core format of the SAME per-channel width by
|
||||
// CHANNEL WIDENING - rg32f is carried in an rgba32f, r8ui in an rgba8ui - moving all three layers
|
||||
// together (ES texture storage, the glBindImageTexture argument, and the shader declaration plus a
|
||||
// mask on every access). What makes the emulation EXACT rather than approximate is that GL already
|
||||
// defines the channels a narrow format does not have:
|
||||
//
|
||||
// * imageLoad on a one-channel format returns (r, 0, 0, 1), on a two-channel one (r, g, 0, 1);
|
||||
// * imageStore drops the components the format does not have;
|
||||
// * a sampler reads the same (r, g, 0, 1).
|
||||
//
|
||||
// so the carrier's surplus channels are not free storage - they hold values GL has already named.
|
||||
//
|
||||
// EVERY CASE HERE IS PHRASED IN THOSE GL RULES AND NOTHING ELSE, which is what makes it a
|
||||
// falsifiable net rather than a restatement of the implementation. Magma needs none of the
|
||||
// machinery (Vulkan takes the declared format natively), a driver that DOES advertise
|
||||
// GL_NV_image_formats - Mesa's, which is what the software lanes run - keeps the narrow format and
|
||||
// widens nothing, and all of them must produce the same numbers. A widening that forgot to mask a
|
||||
// store, or masked it with the wrong constants, or widened the storage without widening the bind,
|
||||
// fails these on the device while the software lanes stay green.
|
||||
|
||||
#include <cstring>
|
||||
#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 {
|
||||
|
||||
constexpr int kExtent = 4;
|
||||
// The narrow image is unit 0 and the wide one unit 1; both declare their binding, so this
|
||||
// scenario turns on the FORMAT alone and shares nothing with the unit bake
|
||||
// ImageFormatQualifierScenario covers.
|
||||
constexpr GLuint kNarrowUnit = 0;
|
||||
constexpr GLuint kWideUnit = 1;
|
||||
|
||||
class NonCoreImageFormatScenario : 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);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
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);
|
||||
}
|
||||
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 > static_cast<GLint>(kWideUnit) && maxComputeImageUniforms >= 2;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Immutable storage, NEAREST filtering and a single level, so the texture is complete
|
||||
// for texelFetch as well as image-bindable. `seed` fills every texel of every channel
|
||||
// with a value no dispatch writes, so "the store never happened" and "the store wrote
|
||||
// the right thing" cannot be confused - which matters here more than usual, because
|
||||
// the failure this scenario exists for is a dispatch that silently does nothing.
|
||||
GLuint MakeTexture(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType,
|
||||
const void* seed) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (seed != nullptr) {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, seed);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindImage(GLuint unit, GLuint texture, GLenum internalFormat, GLenum access) {
|
||||
glBindImageTexture(unit, texture, 0, GL_FALSE, 0, access, internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u)
|
||||
<< "glBindImageTexture refused format " << std::hex << internalFormat;
|
||||
}
|
||||
|
||||
void Dispatch(GLuint program) {
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(kExtent, kExtent, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
|
||||
std::vector<float> ReadFloats(GLuint texture, GLenum format, int componentsPerTexel) {
|
||||
std::vector<float> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
|
||||
-12345.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, format, GL_FLOAT, texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<GLuint> ReadUints(GLuint texture, GLenum format, int componentsPerTexel) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
|
||||
0xFFFFFFFFu);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, format, GL_UNSIGNED_INT, texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
};
|
||||
|
||||
// GL_RG32F, the format all four allFormats walkers abort on (it is entry 2 of the
|
||||
// thirty-nine they step through, and none of them ever reached entry 3 on this backend).
|
||||
//
|
||||
// Two channels are written and two are not, and the shader asks for four back: what the
|
||||
// dispatch stores in b and a has to be dropped, and what the load returns for them has to
|
||||
// be GL's 0 and 1, not whatever the storage happens to hold. A widening that forgot the
|
||||
// store mask hands back (1, 2, 3, 4); one that widened the storage but not the bind reads
|
||||
// out of bounds and hands back anything at all; one that did not widen at all leaves the
|
||||
// seed, because the program never compiled.
|
||||
TEST_F(NonCoreImageFormatScenario, TwoChannelFloatImageDropsSurplusStoresAndLoadsZeroOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const std::vector<float> seed(static_cast<std::size_t>(kExtent) * kExtent * 2u, -1.0f);
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kExtent) * kExtent * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RG32F, GL_RG, GL_FLOAT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg32f, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
}
|
||||
)");
|
||||
// A SEPARATE program and a separate dispatch, so the load is ordered after the store
|
||||
// by glMemoryBarrier rather than by an in-shader barrier whose scope drivers disagree
|
||||
// about. It also means the loading program is built with its own image bindings, which
|
||||
// is the shape a rebuild bug would show up in.
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg32f, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
if (storeProgram == 0 || loadProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG32F, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
// The store reached the texture at all, read through the channels it really has.
|
||||
const std::vector<float> narrowTexels = ReadFloats(narrow, GL_RG, 2);
|
||||
for (int texel = 0; texel < kExtent * kExtent; ++texel) {
|
||||
EXPECT_FLOAT_EQ(narrowTexels[texel * 2 + 0], 1.0f) << "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(narrowTexels[texel * 2 + 1], 2.0f) << "texel " << texel << " green";
|
||||
}
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG32F, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kExtent * kExtent; ++texel) {
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 0], 1.0f) << "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 2.0f) << "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f)
|
||||
<< "texel " << texel << ": imageLoad on a two-channel format must report 0 for blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
|
||||
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
|
||||
// no text was produced for the stage and the dispatch could not run.
|
||||
//
|
||||
// THREE added channels rather than one, and an INTEGER 1 rather than a saturated field -
|
||||
// GL_UNSIGNED_BYTE serves both GL_R8 and GL_R8UI, so a widening that decided the missing
|
||||
// alpha from the transfer type instead of from the format hands back 255 here.
|
||||
TEST_F(NonCoreImageFormatScenario, SingleChannelUnsignedImageDropsSurplusStoresAndLoadsZeroOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kExtent, 200u);
|
||||
const std::vector<GLuint> wideSeed(static_cast<std::size_t>(kExtent) * kExtent * 4u, 999u);
|
||||
const GLuint narrow = MakeTexture(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r8ui, binding = 0) writeonly uniform uimage2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), uvec4(7u, 8u, 9u, 10u));
|
||||
}
|
||||
)");
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r8ui, binding = 0) readonly uniform uimage2D narrow;
|
||||
layout (rgba32ui, binding = 1) writeonly uniform uimage2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
if (storeProgram == 0 || loadProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_R8UI, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
const std::vector<GLuint> narrowTexels = ReadUints(narrow, GL_RED_INTEGER, 1);
|
||||
for (int texel = 0; texel < kExtent * kExtent; ++texel) {
|
||||
EXPECT_EQ(narrowTexels[texel], 7u) << "texel " << texel << " red";
|
||||
}
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_R8UI, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32UI, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<GLuint> loaded = ReadUints(wide, GL_RGBA_INTEGER, 4);
|
||||
for (int texel = 0; texel < kExtent * kExtent; ++texel) {
|
||||
EXPECT_EQ(loaded[texel * 4 + 0], 7u) << "texel " << texel << " red";
|
||||
EXPECT_EQ(loaded[texel * 4 + 1], 0u)
|
||||
<< "texel " << texel << ": imageLoad on a one-channel format must report 0 for green";
|
||||
EXPECT_EQ(loaded[texel * 4 + 2], 0u)
|
||||
<< "texel " << texel << ": imageLoad on a one-channel format must report 0 for blue";
|
||||
EXPECT_EQ(loaded[texel * 4 + 3], 1u)
|
||||
<< "texel " << texel
|
||||
<< ": imageLoad on an INTEGER format without alpha must report the integer 1";
|
||||
}
|
||||
}
|
||||
|
||||
// The other consumer of the same texture. A widened texture's ES storage really does have
|
||||
// four channels, so a sampler reading it raw would see whatever the carrier holds; the
|
||||
// logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges
|
||||
// with GL_TEXTURE_SWIZZLE_B/A composed under the application's own swizzle). texelFetch
|
||||
// rather than a draw, so the case stays a compute dispatch and turns on nothing but the
|
||||
// sampled result.
|
||||
TEST_F(NonCoreImageFormatScenario, ATwoChannelImageTextureStillSamplesAsRGZeroOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const std::vector<float> seed(static_cast<std::size_t>(kExtent) * kExtent * 2u, -1.0f);
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kExtent) * kExtent * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RG32F, GL_RG, GL_FLOAT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg32f, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
}
|
||||
)");
|
||||
const GLuint sampleProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
uniform sampler2D narrowSampler;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, texelFetch(narrowSampler, coord, 0));
|
||||
}
|
||||
)");
|
||||
if (storeProgram == 0 || sampleProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG32F, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, narrow);
|
||||
glUseProgram(sampleProgram);
|
||||
const GLint samplerLocation = glGetUniformLocation(sampleProgram, "narrowSampler");
|
||||
ASSERT_GE(samplerLocation, 0) << "the sampler uniform was not reflected";
|
||||
glUniform1i(samplerLocation, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the texture unit errored";
|
||||
glUseProgram(0);
|
||||
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(sampleProgram);
|
||||
|
||||
const std::vector<float> sampled = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kExtent * kExtent; ++texel) {
|
||||
EXPECT_FLOAT_EQ(sampled[texel * 4 + 0], 1.0f) << "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(sampled[texel * 4 + 1], 2.0f) << "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(sampled[texel * 4 + 2], 0.0f)
|
||||
<< "texel " << texel << ": sampling a two-channel format must report 0 for blue";
|
||||
EXPECT_FLOAT_EQ(sampled[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": sampling a format without alpha must report 1";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -42,10 +42,10 @@
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The eight vertex shaders of the conformance sweep, verbatim in shape. Each reads three
|
||||
// vec4 positions out of a storage block on binding 0 and emits them as a triangle that
|
||||
// covers the whole viewport.
|
||||
constexpr const char* kFormVS[8] = {
|
||||
// The eight vertex shaders of the conformance sweep, verbatim in shape, plus a ninth that
|
||||
// is not from the sweep (see form 8). Each reads three vec4 positions out of a storage
|
||||
// block on binding 0 and emits them as a triangle that covers the whole viewport.
|
||||
constexpr const char* kFormVS[9] = {
|
||||
// 0 - instance name, no binding qualifier, sized array member
|
||||
R"(#version 430 core
|
||||
layout(std430) buffer Buffer {
|
||||
@@ -127,6 +127,38 @@ void main() {
|
||||
case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break;
|
||||
}
|
||||
}
|
||||
)",
|
||||
// 8 - NOT from the conformance sweep. An unqualified storage block with a UNIFORM
|
||||
// BLOCK beside it, which is what makes the block's DEFAULT binding observable at all.
|
||||
//
|
||||
// GL 4.3 core 7.8 gives a storage block with no layout(binding = N) a buffer binding
|
||||
// of zero. Forms 0, 1, 3, 4 and 5 above are all unqualified and all pass, but they
|
||||
// cannot prove that rule holds: they are the only resource in their shader, so the
|
||||
// binding glslang's IO mapper invents for them happens to BE zero and the right answer
|
||||
// arrives for the wrong reason.
|
||||
//
|
||||
// Every shader here is parsed as a Vulkan client, so that mapper allocates out of ONE
|
||||
// flat space shared by samplers, images, uniform blocks, storage blocks and the
|
||||
// synthesized global-uniform block (iomapper.cpp resolveBinding takes the `ent.newSet`
|
||||
// branch, and every resource resolves to set 0), and then writes the result back into
|
||||
// the type's qualifier - so the reflection cannot tell an invented binding from a
|
||||
// declared one. Put anything live next to the block and it is pushed off zero, the
|
||||
// draw reads a binding point nothing was ever bound to, and the triangle collapses
|
||||
// with no GL error anywhere. That is
|
||||
// KHR-GL43.compute_shader.resource-ubo's whole failure, in a vertex stage.
|
||||
//
|
||||
// The uniform block is REBOUND explicitly with glUniformBlockBinding, exactly as that
|
||||
// conformance case does. That keeps this case about the storage block's default and
|
||||
// not about the uniform block's - the rebinding path has always worked, and the
|
||||
// uniform-block default is a separate (still open) question.
|
||||
R"(#version 430 core
|
||||
layout(std140) uniform ScaleBlock {
|
||||
vec4 factor;
|
||||
} g_scale;
|
||||
layout(std430) buffer Buffer {
|
||||
vec4 position[3];
|
||||
} g_input_buffer;
|
||||
void main() { gl_Position = g_input_buffer.position[gl_VertexID] * g_scale.factor; }
|
||||
)",
|
||||
};
|
||||
|
||||
@@ -197,6 +229,26 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error);
|
||||
ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error;
|
||||
|
||||
// Form 8 alone declares a uniform block, and it exists only to occupy a slot the
|
||||
// storage block must not be pushed onto. Bound to a buffer of ones so it scales
|
||||
// the positions by exactly 1 - the block's contribution to the IMAGE is nothing,
|
||||
// and its contribution to the TEST is that it is there at all.
|
||||
GLuint uniformBuffer = 0;
|
||||
if (form == 8) {
|
||||
const float ones[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenBuffers(1, &uniformBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
|
||||
glBufferData(GL_UNIFORM_BUFFER, sizeof(ones), ones, GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniformBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
const GLuint blockIndex = glGetUniformBlockIndex(program, "ScaleBlock");
|
||||
ASSERT_NE(blockIndex, GL_INVALID_INDEX) << "form 8: the uniform block is not active";
|
||||
// Explicit, so this case cannot fail on the uniform block's own default
|
||||
// binding - which is a separate question from the storage block's.
|
||||
glUniformBlockBinding(program, blockIndex, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "form 8: uniform block setup errored";
|
||||
}
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
@@ -221,6 +273,7 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteProgram(program);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
if (uniformBuffer != 0) glDeleteBuffers(1, &uniformBuffer);
|
||||
gl.EndFrame();
|
||||
}
|
||||
};
|
||||
@@ -241,6 +294,10 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock)
|
||||
MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne)
|
||||
MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout)
|
||||
// The form that makes the DEFAULT binding observable rather than accidental: forms 0/1/3/4/5
|
||||
// are unqualified too, but nothing competes with them for glslang's flat slot 0, so they
|
||||
// would keep passing even with the default wrong. See the comment on kFormVS[8].
|
||||
MGL_SSBO_FORM_CASE(8, NoBindingQualifierBesideAUniformBlock)
|
||||
// ---- the two forms that do not work yet ----
|
||||
//
|
||||
// Both carry an UNSIZED array that is not the block's sole trailing member, and both fail
|
||||
|
||||
@@ -828,6 +828,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
|
||||
artifacts.explicitOpaqueUniformBindings[name] = binding;
|
||||
}
|
||||
// Storage blocks declared with NO layout(binding = N), which GL puts on binding 0.
|
||||
// A UNION across stages, unlike the maps above: a block that any stage declared
|
||||
// unqualified is unqualified, because GLSL requires every stage declaring the same
|
||||
// block to declare it identically - so the stages cannot disagree, and a stage whose
|
||||
// grammar the scanner did not recognise simply contributes nothing.
|
||||
artifacts.storageBlocksWithoutBinding.insert(compiled.storageBlocksWithoutBinding.begin(),
|
||||
compiled.storageBlocksWithoutBinding.end());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1016,6 +1023,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
return uniform.index >= 0 && uniform.index < static_cast<Int>(artifacts.tProgramBlockIndexToGl.size()) &&
|
||||
artifacts.tProgramBlockIndexToGl[uniform.index] < 0;
|
||||
};
|
||||
// Member of a block GL can see - a named uniform block, a buffer block, or the
|
||||
// synthesized atomic-counter block. GL locations are a property of the DEFAULT uniform
|
||||
// block alone (GL 4.6 core 7.6.1), so these take none.
|
||||
const auto isNamedBlockMember = [&isGlobalUboMember](const glslang::TObjectReflection& uniform) {
|
||||
return uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
};
|
||||
for (Int i = 0; i < tProgramUniformCount; i++) {
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
if (isGlobalUboMember(uniform) && uniform.stages == 0) {
|
||||
@@ -1062,8 +1075,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
if (inNamedBlock) continue; // block members never take glUniform locations
|
||||
if (isNamedBlockMember(uniform)) continue; // block members never take glUniform locations
|
||||
|
||||
if (const Int* explicitLocation = findExplicitLocation(uniform.name)) {
|
||||
effectiveLocation[i] = static_cast<Uint>(*explicitLocation);
|
||||
@@ -1138,20 +1150,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
|
||||
}
|
||||
|
||||
// Counts ONLY default-block uniforms, which is the whole of what a GL uniform location
|
||||
// is and the whole of what GL_MAX_UNIFORM_LOCATIONS bounds (GL 4.6 core 7.6.1). A
|
||||
// named-block member used to be counted here too and used to be handed a location by the
|
||||
// first-fit pass below, which is a spec violation twice over: glGetUniformLocation must
|
||||
// answer -1 for it (glGetProgramResourceLocation already did), and every slot it took
|
||||
// pushed a real default-block uniform one location further up. On a program with a
|
||||
// buffer block that is exactly how a location EQUAL to the advertised maximum got minted
|
||||
// - the table's ceiling is raised to hold this count, so one extra block member raised it
|
||||
// to MAX and the first-fit pass then filled the last slot
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit-max, whose compute
|
||||
// program carries an SSBO; its -max-array sibling ran the pool out and failed to link).
|
||||
Int requiredUniformLocations = deadReservedLocationCount;
|
||||
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
|
||||
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
|
||||
// named-block members, which take a slot in this allocator's table (an implementation
|
||||
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
|
||||
// fail a link the spec allows.
|
||||
Int defaultBlockLocationDemand = deadReservedLocationCount;
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
auto& uniform = artifacts.program->getUniform(i);
|
||||
const Uint location = effectiveLocation[i];
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
requiredUniformLocations += locationSpan;
|
||||
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
|
||||
if (!isNamedBlockMember(uniform)) requiredUniformLocations += locationSpan;
|
||||
if (location != kNoLocation) {
|
||||
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||
}
|
||||
@@ -1170,11 +1185,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
|
||||
// A single uniform whose own span passes the ceiling was already rejected above; this is
|
||||
// the aggregate half of the same rule.
|
||||
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
|
||||
if (requiredUniformLocations > static_cast<Int>(kMaxUniformLocations)) {
|
||||
artifacts.infoLog =
|
||||
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
|
||||
"GL_MAX_UNIFORM_LOCATIONS is {}.",
|
||||
defaultBlockLocationDemand, kMaxUniformLocations);
|
||||
requiredUniformLocations, kMaxUniformLocations);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
@@ -1247,6 +1262,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// is demoted to the first-fit pass below instead of failing the link.
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
auto& uniform = artifacts.program->getUniform(i);
|
||||
// Same rule the effective-location loop applies: a block member has no GL location,
|
||||
// so it must not reach the first-fit pass either. Its uniformLocations entry stays
|
||||
// at kNoLocation, which glGetUniformLocation reads back as the -1 the spec wants.
|
||||
if (isNamedBlockMember(uniform)) continue;
|
||||
if (locationIsSourceExplicit[i]) continue;
|
||||
const Uint location = effectiveLocation[i];
|
||||
if (location == kNoLocation) {
|
||||
@@ -1505,6 +1524,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (Int i = 0; i < blockCount; ++i) {
|
||||
artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i)));
|
||||
}
|
||||
SeedDefaultStorageBlockBindings();
|
||||
|
||||
const Int uniformCount = program.getNumUniformVariables();
|
||||
artifacts.uniformReflection.clear();
|
||||
@@ -1553,6 +1573,63 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size());
|
||||
}
|
||||
|
||||
// GL 4.3 core 7.8: a shader storage block declared without a layout(binding = N) qualifier
|
||||
// has a buffer binding of ZERO. MobileGL could not report that, because by the time this
|
||||
// reflection is built the number in the block's qualifier is one glslang INVENTED.
|
||||
//
|
||||
// Every shader is parsed as a Vulkan client, so glslang's IO mapper takes the `set = openGl
|
||||
// ? resource : ent.newSet` branch with openGl == 0 (iomapper.cpp resolveBinding) - i.e. it
|
||||
// allocates out of ONE flat binding space shared by every sampler, image, uniform block,
|
||||
// storage block and the synthesized MGL_GLOBAL_UBO - and then writes the result back into
|
||||
// the type's qualifier (iomapper.cpp, `base->getWritableType().getQualifier().layoutBinding =
|
||||
// at->second.newBinding`). getBinding() therefore answers with the auto-assigned slot and
|
||||
// cannot be distinguished from a declared one. An unqualified block lands on 0 only when
|
||||
// nothing else in the program claimed 0 first, which is why a lone storage block in a
|
||||
// trivial shader looked correct and KHR-GL43.compute_shader.resource-ubo - whose shader also
|
||||
// declares twelve uniform blocks - wrote everything to a binding nothing was bound at.
|
||||
//
|
||||
// THE FLAT SPACE IS LEFT ALONE. It is load-bearing: DirectVulkan indexes bindingKinds[],
|
||||
// uniformBlockIndexByBinding[] and storageBlockIndexByBinding[] by that one number and
|
||||
// asserts when two resources collide on it, so forcing the SPIR-V decoration to 0 would
|
||||
// collide an unqualified block with the global UBO and take working programs down. What is
|
||||
// repaired is the GL-VISIBLE binding, through the record GL already has for exactly this -
|
||||
// the same per-name map glShaderStorageBlockBinding writes, which both backends already
|
||||
// consult (ProgramInterface's GL_BUFFER_BINDING, DirectGLES's SPIRV-Cross binding rewrite,
|
||||
// DirectVulkan's GetShaderStorageBlockBinding). Seeding it here means the default and a
|
||||
// later rebind travel the same path, and basic-noBindingLayout - which rebinds all three of
|
||||
// its unqualified blocks - keeps working because a rebind simply overwrites the seed.
|
||||
//
|
||||
// Seeded INSIDE `artifacts`, so an L1 translation-cache hit that republishes the artifacts
|
||||
// wholesale carries it too; a seed applied outside them would silently vanish on a hit.
|
||||
//
|
||||
// Only blocks the lexical scanner recognised in full AND recognised as unqualified are
|
||||
// seeded. A block whose grammar it did not understand keeps today's behaviour rather than
|
||||
// being defaulted on a guess - the shape of the input decides, never an assumption about it.
|
||||
//
|
||||
// THE COLLISION IS DELIBERATE, and it is GL's. Several unqualified blocks all default to 0
|
||||
// and alias there until the application rebinds them; a real GL driver does the same, which
|
||||
// is why every program that has more than one either rebinds or uses one of them.
|
||||
// basic-noBindingLayout is that regression test - it rebinds all three of its blocks
|
||||
// immediately after linking, and the DirectGLES transpile is lazy (first use, not link), so
|
||||
// the ESSL it eventually emits already carries the rebound 0/1/2 and never the aliased seed.
|
||||
// What this replaces was not a safer arrangement, only an accidental one: the three blocks
|
||||
// got glslang's 0/1/2 and an application that rebound them to anything else still wrote to
|
||||
// the wrong buffers.
|
||||
void ProgramLinkTask::SeedDefaultStorageBlockBindings() {
|
||||
if (artifacts.storageBlocksWithoutBinding.empty()) return;
|
||||
for (const ProgramObject::BlockReflection& block : artifacts.blockReflection) {
|
||||
if (!block.type.isBuffer) continue;
|
||||
// An instance array reflects as "B[0]", "B[1]", ... and each element is its own GL
|
||||
// resource with its own binding; the scanner keys on the block TYPE name, so the
|
||||
// subscript is stripped before the lookup. GL gives element k of an unqualified
|
||||
// array binding 0 + k, the same base + element rule a declared binding follows.
|
||||
const String base = StripArrayElementSuffix(block.name);
|
||||
if (!artifacts.storageBlocksWithoutBinding.contains(base)) continue;
|
||||
// First writer wins: never overwrite a binding the application has already chosen.
|
||||
artifacts.shaderStorageBlockBinding.emplace(block.name, BlockArrayElement(block.name));
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||
if (!artifacts.program) return false;
|
||||
// The pipe-output list is the output interface of the program's LAST stage. Only a
|
||||
|
||||
@@ -189,6 +189,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Copies every reflection record the GL query surface reads out of the glslang
|
||||
// TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection.
|
||||
void SnapshotGlslangReflection();
|
||||
// Gives every storage block whose shader declared no layout(binding = N) the binding
|
||||
// GL 4.3 core 7.8 says it has - zero - because glslang's IO mapper has by then invented
|
||||
// one and overwritten the qualifier. See the definition for why the invented binding is
|
||||
// deliberately left in place for the backends' own use.
|
||||
void SeedDefaultStorageBlockBindings();
|
||||
Bool ValidateFragmentOutputLocations();
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||
|
||||
@@ -344,6 +344,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.uniformBlockIndexByName.clear();
|
||||
artifacts.uniformBlockBinding.clear();
|
||||
artifacts.shaderStorageBlockBinding.clear();
|
||||
// Cleared with it: the seed above is re-derived from the newly attached shaders on every
|
||||
// link, so a stale set would otherwise default a block the new sources do declare a
|
||||
// binding for.
|
||||
artifacts.storageBlocksWithoutBinding.clear();
|
||||
artifacts.attribs.clear();
|
||||
artifacts.attribTypes.clear();
|
||||
artifacts.activeUniformCount = 0;
|
||||
|
||||
@@ -373,6 +373,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||
if (!uniform.type.isArray) return 0;
|
||||
// An atomic counter reaches the std140 branch below only because the transpiler
|
||||
// lowered it onto a synthesized block; the buffer it actually addresses is an
|
||||
// ATOMIC COUNTER buffer, whose elements are tightly packed uints (GL 4.6 core 7.6:
|
||||
// "each counter is a single 4-byte value"). Its array stride is therefore 4, not the
|
||||
// vec4 round-up std140 would apply
|
||||
// (KHR-GL43.shader_atomic_counters.basic-program-query wants 4 for ac_counter67[0]).
|
||||
if (IsActiveUniformAtomicCounter(index)) return 4;
|
||||
if (uniform.type.isMatrix) {
|
||||
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
||||
const int vectors = rowMajor ? uniform.type.matrixRows : uniform.type.matrixCols;
|
||||
@@ -1125,7 +1132,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N), merged across stages by MergeShaderSideChannels. Input to the
|
||||
// seeding above; see ExtractStorageBlocksWithoutExplicitBinding for why it has to be
|
||||
// captured lexically.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
Uint maxUniformLocation = 0;
|
||||
|
||||
@@ -211,6 +211,12 @@ namespace {
|
||||
// ProgramObject::DoReflection.
|
||||
result.explicitUniformLocations = ExtractExplicitUniformLocations(result.preprocessedSource);
|
||||
result.explicitOpaqueBindings = ExtractExplicitOpaqueBindings(result.preprocessedSource);
|
||||
// The third side-channel, and the one that restores a GL DEFAULT rather than an
|
||||
// application-declared value: a storage block with no layout(binding = N) has buffer
|
||||
// binding 0 in GL, and by the time the reflection is built glslang's IO mapper has
|
||||
// already invented one and written it into the qualifier.
|
||||
result.storageBlocksWithoutBinding =
|
||||
ExtractStorageBlocksWithoutExplicitBinding(result.preprocessedSource);
|
||||
result.outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||
return result;
|
||||
}
|
||||
@@ -355,6 +361,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.preprocessedSource = shared.preprocessedSource;
|
||||
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
||||
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
||||
artifacts.storageBlocksWithoutBinding = shared.storageBlocksWithoutBinding;
|
||||
artifacts.infoLog.clear();
|
||||
if (shouldPopulateCache) {
|
||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||
@@ -381,6 +388,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
fresh->infoLog = artifacts.infoLog;
|
||||
fresh->explicitUniformLocations.clear();
|
||||
fresh->explicitOpaqueBindings.clear();
|
||||
fresh->storageBlocksWithoutBinding.clear();
|
||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
String preprocessedSource;
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||
// Block type names of this stage's shader storage blocks that declared no
|
||||
// layout(binding = N), i.e. the ones GL puts on binding 0.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
String infoLog;
|
||||
Bool compileStatus = false;
|
||||
};
|
||||
|
||||
@@ -112,6 +112,13 @@ namespace MobileGL {
|
||||
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
|
||||
return Compiled().explicitOpaqueBindings;
|
||||
}
|
||||
// Block type names of this shader's storage blocks that declared NO
|
||||
// layout(binding = N), captured lexically because glslang's IO mapper invents one
|
||||
// and overwrites the qualifier before anything can ask (see
|
||||
// ExtractStorageBlocksWithoutExplicitBinding).
|
||||
const std::set<String>& GetStorageBlocksWithoutBinding() const {
|
||||
return Compiled().storageBlocksWithoutBinding;
|
||||
}
|
||||
Bool GetCompileStatus() const { return Compiled().compileStatus; }
|
||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||
|
||||
|
||||
@@ -46,6 +46,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
String preprocessedSource;
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||
// Block type names of the shader storage blocks declared here with NO
|
||||
// layout(binding = N). GL gives such a block binding 0; nothing downstream can still
|
||||
// tell, because glslang's IO mapper auto-assigns one and writes it into the qualifier.
|
||||
// See ExtractStorageBlocksWithoutExplicitBinding.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The compile info log to publish; empty when outcome == Preprocessed.
|
||||
String infoLog;
|
||||
|
||||
|
||||
@@ -17,8 +17,16 @@
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_ARRAY_ELEMENT_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_READONLY_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_SPLIT_READ_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
||||
@@ -37,7 +45,18 @@ namespace {
|
||||
return count;
|
||||
}
|
||||
|
||||
// The pass tags the name of every declaration it rewrites with the REPAIR it applied, so the
|
||||
// expectations have to spell the tag that matches how the fixture uses the image.
|
||||
String RoAlias(const String& name) { return String(IMAGE_READONLY_ALIAS_PREFIX) + name; }
|
||||
String WoAlias(const String& name) { return String(IMAGE_WRITEONLY_ALIAS_PREFIX) + name; }
|
||||
String RwAlias(const String& name) { return String(IMAGE_SPLIT_READ_ALIAS_PREFIX) + name; }
|
||||
// The writeonly half is minted from the ALREADY access-tagged name, so it carries both.
|
||||
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
||||
String SplitWriteAlias(const String& name) { return WriteAlias(RwAlias(name)); }
|
||||
// The scalar RemapImageArrayElementUnits declares for one element of a split image array.
|
||||
String Elem(const String& name, Int element) {
|
||||
return String(IMAGE_ARRAY_ELEMENT_PREFIX) + name + "_" + std::to_string(element);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
||||
@@ -61,14 +80,19 @@ void main()
|
||||
// Both halves: same binding, same format, same type - which is what makes two image
|
||||
// variables on one image unit legal - and both `coherent`, which is what makes the store
|
||||
// through one of them visible to the load through the other.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
|
||||
// The load keeps the original name, the store moves to the writeonly half.
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ","));
|
||||
// The load goes to the readonly half, the store to the writeonly one, and neither is called
|
||||
// what the application called it any more.
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ","));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ","));
|
||||
EXPECT_FALSE(Contains(out, "imageStore(goku,"));
|
||||
EXPECT_FALSE(Contains(out, "imageLoad(goku,"));
|
||||
}
|
||||
|
||||
// The split has to survive RemoveLayoutBinding, which runs straight after it: an ES image
|
||||
@@ -98,7 +122,10 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray trunks;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray " +
|
||||
RoAlias("trunks") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("trunks") + ","));
|
||||
EXPECT_FALSE(Contains(out, "writeonly"));
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
EXPECT_EQ(CountOf(out, "image2DArray"), 1u);
|
||||
@@ -113,7 +140,10 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D gohan;"));
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D " + WoAlias("gohan") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("gohan") + ","));
|
||||
EXPECT_FALSE(Contains(out, "readonly"));
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
}
|
||||
@@ -140,6 +170,9 @@ void main()
|
||||
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
|
||||
}
|
||||
)";
|
||||
// Untouched means UNRENAMED too: a declaration that already carries its qualifier in the
|
||||
// source carries the SAME one in every stage, so there is no cross-stage mismatch to break up
|
||||
// and renaming it would only churn the text.
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
@@ -154,11 +187,14 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("gohan") + "[3];"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("gohan") + "[3];"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("gohan") + "[1],"));
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("gohan") + "[2],"));
|
||||
}
|
||||
|
||||
// The rewrite is by identifier, not by substring: "goku" must not reach into "goku_hd", and
|
||||
@@ -177,14 +213,19 @@ void main()
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
|
||||
// place, not split, and left non-coherent because nothing aliases it.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
||||
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
||||
EXPECT_FALSE(Contains(out, WriteAlias("goku_hd")));
|
||||
// place, not split, and left non-coherent because nothing aliases it. Both are renamed.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " +
|
||||
WoAlias("goku_hd") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("goku_hd") + ","));
|
||||
EXPECT_FALSE(Contains(out, SplitWriteAlias("goku") + "_hd"));
|
||||
EXPECT_FALSE(Contains(out, SplitWriteAlias("goku_hd")));
|
||||
}
|
||||
|
||||
// Other qualifiers belong to both halves, and the memory qualifier goes where SPIRV-Cross
|
||||
@@ -198,9 +239,11 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D " + RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
|
||||
// compile error in ESSL, so the source's own has to be recognized.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
@@ -225,12 +268,13 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
|
||||
// place, has no alias to stay visible to, and must not pay for uncached access.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D " + WoAlias("storeOnly") + ";")) << out;
|
||||
}
|
||||
|
||||
// The ORDERING half of the split, which `coherent` alone does not buy. Coherent makes the store
|
||||
@@ -253,9 +297,11 @@ void main()
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "imageStore(" + SplitWriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "imageStore(" + SplitWriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
|
||||
<< out;
|
||||
// One per store, not one per shader and not one per load.
|
||||
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 2u) << out;
|
||||
@@ -273,7 +319,7 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D " + WoAlias("storeOnly") + ";")) << out;
|
||||
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
|
||||
}
|
||||
|
||||
@@ -289,7 +335,9 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "max(imageLoad(gohan[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();")) << out;
|
||||
EXPECT_TRUE(Contains(out, "max(imageLoad(" + RwAlias("gohan") +
|
||||
"[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();"))
|
||||
<< out;
|
||||
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
|
||||
}
|
||||
|
||||
@@ -340,25 +388,38 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D sizeOnly;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D " +
|
||||
RoAlias("sizeOnly") + ";"))
|
||||
<< out;
|
||||
// The rename has to reach imageSize too, or the declaration and its only use stop agreeing.
|
||||
EXPECT_TRUE(Contains(out, "imageSize(" + RoAlias("sizeOnly") + ")")) << out;
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
}
|
||||
|
||||
// The alias must not land on an identifier the shader already uses.
|
||||
TEST(SplitReadWriteImageUniformsTest, AliasNameAvoidsAnExistingIdentifier) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 6, rgba8) uniform highp image2D taken;
|
||||
highp vec4 mg_imageWrite_taken;
|
||||
void main()
|
||||
{
|
||||
imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + mg_imageWrite_taken);
|
||||
}
|
||||
)";
|
||||
// Neither minted name may land on an identifier the shader already uses - and there are two of
|
||||
// them now, the access-tagged name of the repaired declaration and the writeonly half built on
|
||||
// top of it. Both collisions are exercised at once.
|
||||
TEST(SplitReadWriteImageUniformsTest, AliasNamesAvoidExistingIdentifiers) {
|
||||
const String stageCollision = RwAlias("taken");
|
||||
const String writeCollision = SplitWriteAlias("taken");
|
||||
const String source = "#version 320 es\n"
|
||||
"layout(binding = 6, rgba8) uniform highp image2D taken;\n"
|
||||
"highp vec4 " +
|
||||
stageCollision + ";\nhighp vec4 " + writeCollision +
|
||||
";\nvoid main()\n{\n"
|
||||
" imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + " +
|
||||
stageCollision + " + " + writeCollision + ");\n}\n";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_FALSE(Contains(out, "image2D " + WriteAlias("taken") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "image2D " + WriteAlias("taken") + "X;"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("taken") + "X,"));
|
||||
EXPECT_TRUE(Contains(out, "+ mg_imageWrite_taken)"));
|
||||
|
||||
EXPECT_FALSE(Contains(out, "image2D " + stageCollision + ";")) << out;
|
||||
EXPECT_FALSE(Contains(out, "image2D " + writeCollision + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + stageCollision + "X;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + writeCollision + "X;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + writeCollision + "X,")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + stageCollision + "X,")) << out;
|
||||
// ...and the globals that forced the suffix are still themselves.
|
||||
EXPECT_TRUE(Contains(out, "highp vec4 " + stageCollision + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "highp vec4 " + writeCollision + ";")) << out;
|
||||
}
|
||||
|
||||
// A use the pass cannot account for (here: the image handed to a user function) means it
|
||||
@@ -372,6 +433,9 @@ void main()
|
||||
imageStore(passed, ivec2(0), helper(passed));
|
||||
}
|
||||
)";
|
||||
// Declining means declining EVERYTHING: no qualifier, and no rename either. A rename that
|
||||
// moved the declaration but not the use inside helper() would be a compile error rather than
|
||||
// the wrong-but-compiling shader this pass refuses to guess at.
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
@@ -387,6 +451,407 @@ void main()
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
// The defect the rename exists for. The pass sees ONE stage at a time and picks the memory
|
||||
// qualifier from the accesses in THAT stage, so a vertex shader that only stores and a fragment
|
||||
// shader that only loads the same image came out `writeonly g_image` and `readonly g_image` -
|
||||
// two declarations of one uniform name that GLSL requires to be identical. Adreno merges them
|
||||
// and silently discards the vertex-stage stores (advanced-memory-dependentInvocation reads back
|
||||
// the untouched zeros, with LINK_STATUS = 1 and an empty driver log). Tagging by the repair
|
||||
// leaves nothing to merge.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageDifferentlyGetDifferentNames) {
|
||||
const String vertexSource = R"(#version 320 es
|
||||
layout(binding = 0, rgba32f) uniform coherent highp image2D g_image;
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image, ivec2(0), vec4(1.0));
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String fragmentSource = R"(#version 320 es
|
||||
layout(binding = 0, rgba32f) uniform coherent highp image2D g_image;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
mg_FragColor = imageLoad(g_image, ivec2(0));
|
||||
}
|
||||
)";
|
||||
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
||||
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
||||
|
||||
const String vsName = WoAlias("g_image");
|
||||
const String fsName = RoAlias("g_image");
|
||||
EXPECT_NE(vsName, fsName);
|
||||
EXPECT_TRUE(Contains(vsOut, "uniform writeonly coherent highp image2D " + vsName + ";")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "uniform readonly coherent highp image2D " + fsName + ";")) << fsOut;
|
||||
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
|
||||
// The whole point: after the rewrite the two stages no longer declare a common name, so
|
||||
// there is nothing for a linker to merge and mis-qualify.
|
||||
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
|
||||
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
|
||||
// Both bindings are untouched - the image unit is still the same one.
|
||||
EXPECT_TRUE(Contains(vsOut, "binding = 0"));
|
||||
EXPECT_TRUE(Contains(fsOut, "binding = 0"));
|
||||
}
|
||||
|
||||
// The other side of that coin, and the one a per-STAGE tag got wrong. Two stages that use the
|
||||
// image the same way emit byte-identical declarations, so they must arrive at ONE shared name:
|
||||
// Adreno allocates an image LOCATION per distinct uniform, and giving each stage its own name
|
||||
// multiplied a program's image-uniform count by the number of stages that mention it - which is
|
||||
// how the five stages of KHR-GL43.shading_language_420pack.binding_images_texture_type_* went
|
||||
// from 6 image uniforms to 30 and drew "Error: Image Image location or component exceeds max
|
||||
// allowed." out of the Adreno 830 linker, with LINK_STATUS = TRUE already published by the
|
||||
// frontend and every draw silently doing nothing.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageAlikeShareOneName) {
|
||||
const String vertexSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba8) uniform highp image2D goku;
|
||||
void main()
|
||||
{
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String fragmentSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba8) uniform highp image2D goku;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
mg_FragColor = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
||||
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
||||
|
||||
// One name, arrived at independently by two different stages, so the linker merges them
|
||||
// back into the single image uniform the application declared.
|
||||
for (const String& out : {vsOut, fsOut}) {
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ",")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ",")) << out;
|
||||
}
|
||||
}
|
||||
|
||||
// One tag per repair, all three distinct, and each a legal identifier stem.
|
||||
TEST(SplitReadWriteImageUniformsTest, EveryAccessTagIsDistinct) {
|
||||
const String prefixes[] = {String(IMAGE_READONLY_ALIAS_PREFIX), String(IMAGE_WRITEONLY_ALIAS_PREFIX),
|
||||
String(IMAGE_SPLIT_READ_ALIAS_PREFIX), String(IMAGE_WRITE_ALIAS_PREFIX)};
|
||||
Vector<String> seenPrefixes;
|
||||
for (const String& prefix : prefixes) {
|
||||
// A GLSL identifier may not contain "__" (GLSL ES 3.20 3.7), and the prefix is glued
|
||||
// straight onto a name that may itself start with '_'.
|
||||
EXPECT_EQ(prefix.find("__"), String::npos) << prefix;
|
||||
for (const String& seen : seenPrefixes) {
|
||||
EXPECT_NE(seen, prefix) << prefix;
|
||||
// Nor may one be a prefix of another: the write half is minted on top of an
|
||||
// already-tagged name, so a shared stem would let two repairs collide.
|
||||
EXPECT_NE(prefix.rfind(seen, 0), 0u) << prefix << " vs " << seen;
|
||||
}
|
||||
seenPrefixes.push_back(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// RemapImageArrayElementUnits
|
||||
//
|
||||
// ES takes an image unit only from layout(binding=N), and one declaration carries one of them,
|
||||
// so an image array's elements land on N, N+1, N+2, ... Desktop GL lets an application point
|
||||
// each element wherever it likes with glUniform1i, which ES makes an INVALID_OPERATION on an
|
||||
// image uniform - there is no API side to fix, so the emitted text has to carry it.
|
||||
|
||||
namespace {
|
||||
// The advanced-sso-simple shape: a four-element image array on units 0, 2, 4, 6. The
|
||||
// subscripts are literals because LegalizeResourceArrayIndexingForEssl has already folded
|
||||
// the conformance case's `for (int i = 0; i < g_image.length(); ++i)` - ESSL forbids a
|
||||
// non-constant image-array subscript outright, so a loop counter never reaches this pass.
|
||||
const char* const kSsoImageArrayFS = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[1], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[2], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
ImageArrayUnitPlan Plan(const String& name, const Vector<Int>& units) {
|
||||
ImageArrayUnitPlan plan;
|
||||
plan.name = name;
|
||||
plan.units = units;
|
||||
return plan;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The defect, end to end. Elements 0..3 need units 0, 2, 4, 6, so the array becomes four scalars
|
||||
// carrying those four bindings. Before this, the single stamped binding sent the four elements to
|
||||
// units 0, 1, 2, 3.
|
||||
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsSplitIntoOneScalarPerElement) {
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, &declined);
|
||||
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
const Int units[4] = {0, 2, 4, 6};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", element) + ", ivec2(gl_FragCoord.xy)"))
|
||||
<< out;
|
||||
}
|
||||
// The array is gone entirely; nothing may still address units 0,1,2,3 through it.
|
||||
EXPECT_FALSE(Contains(out, "image2D g_image[4];")) << out;
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
// Exactly the four image uniforms the application declared - what the earlier widening cost
|
||||
// was the whole SPAN, seven here, which is the budget failure mode this shape removes.
|
||||
EXPECT_EQ(CountOf(out, "image2D "), 4u) << out;
|
||||
}
|
||||
|
||||
// The other program of the same conformance case: units 1, 3, 5, 7 in the application's own
|
||||
// element ORDER, which is what carries the assignment, so it must NOT be sorted or rebased.
|
||||
TEST(RemapImageArrayElementUnitsTest, EachElementCarriesTheUnitTheApplicationGaveIt) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 3) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(2.0));
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {3, 1, 7, 5})});
|
||||
const Int units[4] = {3, 1, 7, 5};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
}
|
||||
// Only elements 0 and 3 are ever accessed; elements 1 and 2 are declared and unused, because
|
||||
// the reflection says the array has four of them.
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0)")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 3) + ", ivec2(0)")) << out;
|
||||
}
|
||||
|
||||
// Consecutive-from-element-zero is exactly what ESSL does unaided, so the emitted text of an
|
||||
// ordinary image shader must come out byte-identical. The caller filters these; the pass must
|
||||
// not depend on that.
|
||||
TEST(RemapImageArrayElementUnitsTest, ConsecutiveUnitsAreLeftCompletelyAlone) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 2) uniform writeonly highp image2D g_image[3];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {2, 3, 4})}), source);
|
||||
// ...and so is a plan for an array this stage does not declare at all: the reflection is
|
||||
// program-wide, the pass runs per stage.
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("other_image", {0, 4})}), source);
|
||||
}
|
||||
|
||||
// A subscript that is not a literal names no element, so there is no scalar to rewrite it to.
|
||||
// It should never arrive - LegalizeResourceArrayIndexingForEssl runs first and ESSL rejects the
|
||||
// shape outright - but if one does, guessing an element would only change WHICH unit the access
|
||||
// reaches wrongly. Decline, loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, ANonLiteralSubscriptIsDeclinedAndNamed) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
imageStore(g_image[i], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 2, 4, 6})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
|
||||
// A literal that is out of the reflected range is the same class of mismatch.
|
||||
const String outOfRange = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[5], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> outOfRangeDeclined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(outOfRange, {Plan("g_image", {0, 5})}, &outOfRangeDeclined),
|
||||
outOfRange);
|
||||
ASSERT_EQ(outOfRangeDeclined.size(), 1u);
|
||||
}
|
||||
|
||||
// A uint subscript IS a literal element index. SPIRV-Cross prints an index in the type SPIR-V
|
||||
// gave it and LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
||||
// index it replaced, so an array walked by anything unsigned - a `uint` loop counter, or
|
||||
// anything derived from gl_LocalInvocationIndex, which is uint by definition - reaches this pass
|
||||
// spelled `g_image[0u]`. Refusing the `u` declined the array and left every element on the
|
||||
// consecutive units one binding hands out, silently.
|
||||
TEST(RemapImageArrayElementUnitsTest, AUintSubscriptIsStillALiteralElementIndex) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[3];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0u], ivec2(0), vec4(1.0));
|
||||
imageStore(g_image[2U], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined);
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
|
||||
EXPECT_TRUE(Contains(out, "binding = 0) uniform writeonly highp image2D " + Elem("g_image", 0) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 4) uniform writeonly highp image2D " + Elem("g_image", 1) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 8) uniform writeonly highp image2D " + Elem("g_image", 2) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0), vec4(1.0))")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 2) + ", ivec2(0), vec4(2.0))")) << out;
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
}
|
||||
|
||||
// ...and the suffix is not a licence to accept anything else that ends in one: `iu` is not a
|
||||
// literal, and neither is a bare `u`.
|
||||
TEST(RemapImageArrayElementUnitsTest, ASuffixAloneDoesNotMakeAnExpressionALiteral) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
highp int iu = 1;
|
||||
imageStore(g_image[iu], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// A use the pass cannot see a subscript on has no element index to rewrite, so splitting the
|
||||
// array out from under it would leave it naming a declaration that no longer exists. Decline,
|
||||
// loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, AUseWithoutASubscriptIsDeclined) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void helper();
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
||||
helper(g_image);
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 5})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
}
|
||||
|
||||
// The reflection and the emitted text have to be talking about the same array. If they are not,
|
||||
// the pass has misidentified something and must not rewrite on a guess.
|
||||
TEST(RemapImageArrayElementUnitsTest, AnExtentThatDisagreesWithTheReflectionIsDeclined) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// The two passes that run after it have to see the split declarations and keep their bindings: an
|
||||
// ES image unit cannot be assigned through the API, so the qualifier is the only mechanism there
|
||||
// is, and an element that is both read and written is split again into a pair that must BOTH
|
||||
// carry that element's own unit.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheSplitElementsSurviveTheLaterImagePasses) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 6})});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Element 0 is only ever loaded and element 1 only ever stored, so neither is split into a
|
||||
// pair - but each keeps the unit the application gave it, which the array could not express.
|
||||
EXPECT_TRUE(Contains(out, "binding = 4")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 6")) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RoAlias(Elem("g_image", 0)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WoAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias(Elem("g_image", 1)) + ", ivec2(0), imageLoad(" +
|
||||
RoAlias(Elem("g_image", 0)) + ", ivec2(0)))"))
|
||||
<< out;
|
||||
// Nothing is left addressing the array.
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
}
|
||||
|
||||
// The same element both read and written IS split into a coherent pair, and both halves have to
|
||||
// inherit that element's binding - the shape the widening used to have to carry on an array.
|
||||
TEST(RemapImageArrayElementUnitsTest, AnElementThatIsBothReadAndWrittenIsSplitWithItsOwnBinding) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[1], ivec2(0)));
|
||||
imageStore(g_image[0], ivec2(0), vec4(0.0));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Element 1 sits on unit 9, and both halves of its split pair say so.
|
||||
EXPECT_EQ(CountOf(out, "binding = 9"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WriteAlias(RwAlias(Elem("g_image", 1))) + ";"))
|
||||
<< out;
|
||||
EXPECT_EQ(CountOf(out, "binding = 4"), 1u) << out;
|
||||
}
|
||||
|
||||
// The gap that let a per-STAGE image rename reach production: every fixture above declares an
|
||||
// image ARRAY, and the regression it caused was in the SCALAR images sitting next to one. A
|
||||
// scalar with an explicit binding has to come out of the whole chain still on ITS OWN unit,
|
||||
// still spelled once, and named the same thing every stage would name it - it is the array that
|
||||
// needs repairing, not its neighbour.
|
||||
TEST(RemapImageArrayElementUnitsTest, AScalarImageWithItsOwnBindingIsUntouchedByTheArrayRepair) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba8, binding = 7) uniform highp image2D goku;
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})}, &declined);
|
||||
EXPECT_TRUE(declined.empty());
|
||||
// The array pass may only ever touch the arrays it was handed a plan for.
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba8, binding = 7) uniform highp image2D goku;")) << out;
|
||||
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Unit 7 exactly twice - the two halves of the scalar's own split pair - and nothing has
|
||||
// moved it onto one of the array's units.
|
||||
EXPECT_EQ(CountOf(out, "binding = 7"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + SplitWriteAlias("goku") + ";")) << out;
|
||||
// ...and no per-stage tag anywhere: the name a scalar gets is a function of how this text
|
||||
// uses it, so every stage that uses it the same way keeps ONE shared uniform (Adreno spends
|
||||
// an image location per distinct one).
|
||||
EXPECT_FALSE(Contains(out, "mg_imageVs_")) << out;
|
||||
EXPECT_FALSE(Contains(out, "mg_imageFs_")) << out;
|
||||
EXPECT_FALSE(Contains(out, "mg_imageCs_")) << out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// RetargetTextureBufferExtension
|
||||
//
|
||||
@@ -732,3 +1197,84 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
||||
}
|
||||
|
||||
// --- pass-through tessellation control stage --------------------------------------------------
|
||||
//
|
||||
// Desktop GL makes the tessellation control stage optional and takes the levels from
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL; ES 3.2 has neither, and rejects a
|
||||
// program that has an evaluation stage without a control stage - with an EMPTY info log. The
|
||||
// synthesized stage is what stands in, and it has to MIRROR its two neighbours' gl_PerVertex
|
||||
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
|
||||
// ES link error against a program that has nothing else wrong with it.
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "");
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
|
||||
EXPECT_TRUE(Contains(out,
|
||||
"gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;"))
|
||||
<< out;
|
||||
// All six, unconditionally: writing a level the evaluation stage's domain does not use is
|
||||
// legal and ignored, and it saves the generator from having to know the domain.
|
||||
for (const char* level : {"gl_TessLevelOuter[0]", "gl_TessLevelOuter[1]", "gl_TessLevelOuter[2]",
|
||||
"gl_TessLevelOuter[3]", "gl_TessLevelInner[0]", "gl_TessLevelInner[1]"}) {
|
||||
EXPECT_TRUE(Contains(out, String(level) + " = 1.0;")) << level << "\n" << out;
|
||||
}
|
||||
// Nothing redeclared when the neighbours redeclared nothing - the driver's own built-in
|
||||
// gl_in/gl_out is then what both sides agree on, and redeclaring is what would break it.
|
||||
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
|
||||
}
|
||||
|
||||
// ES 3.1 reaches tessellation only through the extension; the caller has already established
|
||||
// that the driver runs the evaluation stage at all, so the only question is the spelling.
|
||||
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
||||
const String out = BuildPassthroughTessControlEssl(310, 3, "", "");
|
||||
EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
|
||||
}
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
|
||||
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
|
||||
const String outMembers = " highp vec4 gl_Position; ";
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers);
|
||||
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
|
||||
}
|
||||
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReadsEitherDirectionAndOnlyThatDirection) {
|
||||
const String essl = R"(#version 320 es
|
||||
in gl_PerVertex { highp vec4 gl_Position; } gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex { highp vec4 gl_Position; highp float gl_PointSize; } gl_out[];
|
||||
void main() {}
|
||||
)";
|
||||
const auto inMembers = ExtractPerVertexBlockMembers(essl, true);
|
||||
ASSERT_TRUE(inMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*inMembers, "gl_Position")) << *inMembers;
|
||||
EXPECT_FALSE(Contains(*inMembers, "gl_PointSize"))
|
||||
<< "the `in` block must not pick up the `out` block's members: " << *inMembers;
|
||||
|
||||
const auto outMembers = ExtractPerVertexBlockMembers(essl, false);
|
||||
ASSERT_TRUE(outMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*outMembers, "gl_PointSize")) << *outMembers;
|
||||
}
|
||||
|
||||
// A shader that does not redeclare the block must report nothing, so the generator leaves the
|
||||
// driver's built-in declaration alone rather than inventing one.
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReportsNothingWhenTheBlockIsNotRedeclared) {
|
||||
const String essl = R"(#version 320 es
|
||||
layout(quads) in;
|
||||
void main() { gl_Position = gl_in[0].gl_Position; }
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, false).has_value()) << essl;
|
||||
}
|
||||
|
||||
// "min" ends in "in" and "layout" ends in "out": the direction keyword has to be a whole token
|
||||
// immediately before the block name, or an unrelated identifier would be read as a redeclaration.
|
||||
TEST(ExtractPerVertexBlockMembersTest, DoesNotMatchAnIdentifierEndingInTheKeyword) {
|
||||
const String essl = R"(#version 320 es
|
||||
struct fin gl_PerVertex { highp vec4 gl_Position; };
|
||||
void main() {}
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
}
|
||||
|
||||
@@ -3401,3 +3401,182 @@ void main() { fragColor = vec4(1.0); }
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6: an atomic counter is a default-block uniform that addresses an ATOMIC COUNTER
|
||||
// buffer, where every counter is a tightly packed 4-byte value. MobileGL lowers each atomic_uint
|
||||
// onto a synthesized block, which used to drag the whole array-stride query onto the std140 rule
|
||||
// that rounds an element stride up to a vec4 - so an atomic counter array reported 16
|
||||
// (KHR-GL43.shader_atomic_counters.basic-program-query: "GL_UNIFORM_ARRAY_STRIDE is 16 should be
|
||||
// 4"). The offsets, matrix stride and row-major flag are pinned alongside it because the same
|
||||
// synthesized block feeds all four queries.
|
||||
TEST_F(ProgramTest, AtomicCounterArrayReportsThePackedFourByteStride) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
void main() { gl_Position = vec4(1.0); }
|
||||
)";
|
||||
const char* fsSource = R"(#version 430 core
|
||||
layout(location = 0) out vec4 o_color;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint ac_counter0;
|
||||
layout(binding = 0, offset = 4) uniform atomic_uint ac_counter1;
|
||||
layout(binding = 0) uniform atomic_uint ac_counter2;
|
||||
layout(binding = 0) uniform atomic_uint ac_counter67[2];
|
||||
layout(binding = 0) uniform atomic_uint ac_counter3;
|
||||
void main() {
|
||||
uint c = 0u;
|
||||
c += atomicCounterIncrement(ac_counter0);
|
||||
c += atomicCounterIncrement(ac_counter1);
|
||||
c += atomicCounterIncrement(ac_counter2);
|
||||
c += atomicCounterIncrement(ac_counter3);
|
||||
c += atomicCounterIncrement(ac_counter67[0]);
|
||||
c += atomicCounterIncrement(ac_counter67[1]);
|
||||
o_color = vec4(float(c));
|
||||
}
|
||||
)";
|
||||
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
|
||||
GLint activeUniforms = 0;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_EQ(activeUniforms, 5);
|
||||
|
||||
// Declared offset -> expected {array size, array stride}. layout(offset=) pins the first two;
|
||||
// the rest are packed after them in declaration order, the array taking two 4-byte slots.
|
||||
struct Expectation {
|
||||
const char* name;
|
||||
GLint size;
|
||||
GLint offset;
|
||||
GLint arrayStride;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{"ac_counter0", 1, 0, 0}, {"ac_counter1", 1, 4, 0}, {"ac_counter2", 1, 8, 0},
|
||||
{"ac_counter67[0]", 2, 12, 4}, {"ac_counter3", 1, 20, 0},
|
||||
};
|
||||
|
||||
for (const auto& expected : expectations) {
|
||||
const char* queryName = expected.name;
|
||||
GLuint index = GL_INVALID_INDEX;
|
||||
GetUniformIndices(program, 1, &queryName, &index);
|
||||
ASSERT_NE(index, GL_INVALID_INDEX) << expected.name << " is not an active uniform";
|
||||
|
||||
GLint value = -2;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_TYPE, &value);
|
||||
EXPECT_EQ(value, static_cast<GLint>(GL_UNSIGNED_INT_ATOMIC_COUNTER)) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_SIZE, &value);
|
||||
EXPECT_EQ(value, expected.size) << expected.name;
|
||||
// An atomic counter is a default-block uniform however it was lowered.
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_BLOCK_INDEX, &value);
|
||||
EXPECT_EQ(value, -1) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_OFFSET, &value);
|
||||
EXPECT_EQ(value, expected.offset) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_ARRAY_STRIDE, &value);
|
||||
EXPECT_EQ(value, expected.arrayStride) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_MATRIX_STRIDE, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_IS_ROW_MAJOR, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6.1: a uniform LOCATION is a property of the default uniform block. A member of
|
||||
// a named uniform block or a buffer block has none, and glGetUniformLocation must answer -1 for
|
||||
// it - which is what glGetProgramResourceLocation(GL_UNIFORM, ...) already did, so the two used
|
||||
// to disagree. The location such a member was handed was not merely reported, it was CONSUMED:
|
||||
// it came out of the same first-fit table the default-block uniforms draw from.
|
||||
TEST_F(ProgramTest, BlockMembersConsumeNoUniformLocation) {
|
||||
const char* csSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 1) buffer ResultBuffer { vec4 bufferMember; };
|
||||
layout(std140, binding = 2) uniform SettingsBlock { vec4 blockMember; };
|
||||
layout(location = 0) uniform float uDead[3];
|
||||
uniform float uImplicit;
|
||||
void main() { bufferMember = blockMember * uImplicit; }
|
||||
)";
|
||||
const GLuint cs = CompileShaderChecked(GL_COMPUTE_SHADER, csSource);
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, cs);
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
char infoLog[1024] = "";
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
|
||||
|
||||
for (const char* member : {"bufferMember", "blockMember"}) {
|
||||
EXPECT_EQ(GetUniformLocation(program, member), -1) << member << " is a block member, not a GL uniform";
|
||||
EXPECT_EQ(GetProgramResourceLocation(program, GL_UNIFORM, member), -1)
|
||||
<< member << ": the two location queries must agree";
|
||||
}
|
||||
|
||||
// uDead[3] reserves 0..2 without becoming visible, so the first location left for the one
|
||||
// default-block uniform is 3. It used to be 4, because a block member took 3 first.
|
||||
EXPECT_EQ(GetUniformLocation(program, "uImplicit"), 3)
|
||||
<< "a block member consumed a location the default-block uniform was entitled to";
|
||||
EXPECT_EQ(GetUniformLocation(program, "uDead"), -1);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same defect at the boundary, which is where the conformance suite catches it. The location
|
||||
// table's ceiling is raised to hold every uniform it must place; counting block members into that
|
||||
// raise pushed the ceiling to GL_MAX_UNIFORM_LOCATIONS itself, and the first-fit pass then handed
|
||||
// out the one location past the legal 0..MAX-1 range
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit-max, whose compute program
|
||||
// carries an SSBO: "Uniform u2 returned location (4095) is greater than implementation dependent
|
||||
// limit (4095)"). Its -array sibling shares the root cause and failed one step further along, with
|
||||
// the pool reported exhausted and no link at all.
|
||||
TEST_F(ProgramTest, ImplicitLocationStaysInRangeWhenABufferBlockSharesTheProgram) {
|
||||
GLint maxLocations = 0;
|
||||
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
|
||||
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
|
||||
|
||||
// The CTS shape: explicit unused arrays fill the pool except for a hole of `implicitCount`
|
||||
// locations at `holeBase`, and the one implicit uniform must land exactly in that hole.
|
||||
const auto runCase = [&](int holeBase, int implicitCount) {
|
||||
String decls;
|
||||
int nextName = 0;
|
||||
if (holeBase > 0) {
|
||||
decls += "layout(location = 0) uniform float u" + std::to_string(nextName++) + "[" +
|
||||
std::to_string(holeBase) + "];\n";
|
||||
}
|
||||
const int tailBase = holeBase + implicitCount;
|
||||
if (tailBase < maxLocations) {
|
||||
decls += "layout(location = " + std::to_string(tailBase) + ") uniform float u" +
|
||||
std::to_string(nextName++) + "[" + std::to_string(maxLocations - tailBase) + "];\n";
|
||||
}
|
||||
const String implicitName = "u" + std::to_string(nextName);
|
||||
decls += "uniform float " + implicitName + "[" + std::to_string(implicitCount) + "];\n";
|
||||
|
||||
// The buffer block is the whole point: it is one more uniform the table has to seat, and
|
||||
// seating it inside the location space is what used to push the implicit uniform out.
|
||||
const String csSource = "#version 430 core\n"
|
||||
"layout(local_size_x = 1) in;\n"
|
||||
"layout(std430, binding = 1) buffer ResultBuffer { vec4 cs_result; };\n" +
|
||||
decls + "void main() { cs_result = vec4(" + implicitName + "[0]); }\n";
|
||||
const GLuint cs = CompileShaderChecked(GL_COMPUTE_SHADER, csSource.c_str());
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, cs);
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
char infoLog[1024] = "";
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
ASSERT_EQ(linkStatus, GL_TRUE) << "hole at " << holeBase << " x" << implicitCount << ": " << infoLog;
|
||||
|
||||
const GLint location = GetUniformLocation(program, implicitName.c_str());
|
||||
EXPECT_EQ(location, holeBase) << "the implicit uniform must take the one free span left";
|
||||
EXPECT_LT(location + implicitCount, maxLocations + 1)
|
||||
<< "locations " << location << ".." << (location + implicitCount - 1)
|
||||
<< " must stay inside 0.." << (maxLocations - 1);
|
||||
EXPECT_EQ(GetUniformLocation(program, "cs_result"), -1);
|
||||
};
|
||||
|
||||
// The three holes the CTS walks, for its single-uniform and its 3-element-array subcase.
|
||||
for (const int implicitCount : {1, 3}) {
|
||||
runCase(0, implicitCount);
|
||||
runCase(3, implicitCount);
|
||||
runCase(maxLocations - implicitCount, implicitCount);
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#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/Lower1DSampledImagesPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
|
||||
@@ -3041,6 +3042,47 @@ void main() {
|
||||
<< "the generated ESSL still indexes a fragment output with a non-constant:\n" << essl;
|
||||
}
|
||||
|
||||
// Marking a loop for unrolling means marking every loop enclosing it - SPIRV-Tools only unrolls
|
||||
// innermost loops - and the copies those levels produce MULTIPLY, so bounding each loop on its
|
||||
// own bounds nothing. This nest is the OIT shape wrapped in a tile walk: 64 x 64 x 2, every level
|
||||
// individually inside kMaxUnrolledIterations, and its product is not. Spending the budget as the
|
||||
// walk climbs stops at the innermost level; the switch lowering, whose cost is the output array's
|
||||
// length rather than the trip counts, legalizes whatever the unroll no longer reaches. The same
|
||||
// defect was measured first on LegalizeResourceArrayIndexPass, which the image half of that pass
|
||||
// made reachable; this walk is its twin and is fixed the same way.
|
||||
TEST_F(ProgramUtilTest, ALoopNestAroundAFragmentOutputIndexIsBoundedAsAWhole) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = CompileFragmentToRawSpirv(R"(#version 330 core
|
||||
out vec4 coeff[2];
|
||||
in vec4 vColor;
|
||||
void main() {
|
||||
for (int y = 0; y < 64; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
for (int attachmentIndex = 0; attachmentIndex < 2; ++attachmentIndex) {
|
||||
coeff[attachmentIndex] = vColor * float(x + y + attachmentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
// Still legalized - that is not what is being traded away.
|
||||
EXPECT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(legalized));
|
||||
// ...and the module the driver has to compile is still a module, not the nest's product.
|
||||
// Measured on this fixture: 318 words with the nest budget, 5112 without - so the bound is
|
||||
// loose enough not to pin spirv-opt's exact output (3x the real figure) and tight enough
|
||||
// that a nest-wide unroll cannot slip under it (5x below the unbounded one).
|
||||
EXPECT_LT(legalized.size(), 1024u) << "legalized module is " << legalized.size() << " words";
|
||||
}
|
||||
|
||||
// The fallback half: an index computed from a uniform cannot be folded by any amount of
|
||||
// unrolling, so the write becomes a switch over the array's range and the read becomes
|
||||
// constant-indexed loads combined with selects.
|
||||
@@ -3651,6 +3693,290 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
|
||||
<< "declining means the 1D-array type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- 1D SAMPLED images (Lower1DSampledImagesPass) ----------------------------------------------
|
||||
//
|
||||
// The other half of the 1D story. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - the
|
||||
// test above pins that - but it prints the OFFSET and the two GRADIENT operands with the arity the
|
||||
// desktop shader spelled, against a sampler it has just declared 2D. The result has no ESSL
|
||||
// overload, the driver says "no matching overloaded function found", and the stage is lost.
|
||||
|
||||
namespace {
|
||||
// Same word walk as the storage-image counters, for Sampled == 1.
|
||||
SizeT Count1DSampledImageTypes(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 + 7] == 1u) {
|
||||
++count;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// KHR-GL43.compute_shader.resource-texture's own sampler1DArray lookup, minus the other eight
|
||||
// samplers: a textureLodOffset whose offset is the scalar GL gives a 1D array.
|
||||
const char* k1DArraySamplerOffsetCompute = R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() { ssb.data = textureLodOffset(g_sampler4, vec2(0.5, 1.0), 0.0, 0); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// The negative control, and the whole reason the pass exists: SPIRV-Cross emits the sampler as 2D
|
||||
// and widens the coordinate, then hands the scalar offset straight through. 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, SpirvCrossEmitsAScalarOffsetFor1DSamplers) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(k1DArraySamplerOffsetCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 1u)
|
||||
<< "glslang no longer emits a Dim1D/Sampled=1 image for sampler1DArray";
|
||||
|
||||
const String essl = DecompileToEssl(spirv);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("sampler2DArray"), String::npos)
|
||||
<< "SPIRV-Cross declares the 1D array sampler as 2D on ES; that half it does do:\n" << essl;
|
||||
EXPECT_EQ(essl.find("ivec2"), String::npos)
|
||||
<< "SPIRV-Cross is expected to pass the SCALAR offset straight through, so nothing in this "
|
||||
"fixture builds an ivec2 - its absence IS the defect, because ESSL has no "
|
||||
"textureLodOffset(sampler2DArray, vec3, float, int). If this no longer happens, "
|
||||
"Lower1DSampledImagesForEssl may no longer be needed:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The fix: the type becomes a 2D array and the offset becomes two components, so the call
|
||||
// type-checks against the declaration SPIRV-Cross was already emitting.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesWidensTheOffsetOfA1DArrayLookup) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(k1DArraySamplerOffsetCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
// Through the shared chain first, exactly as the DirectGLES transpile path does - the same
|
||||
// reason the storage-image tests above do it: the pass runs on sanitized bytes, and validating
|
||||
// raw glslang output would latch pre-existing properties against this pass.
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u)
|
||||
<< "no 1D sampled image type may survive the pass:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
// The point of moving the TYPE rather than only the operand: an ivec2 offset against a type
|
||||
// still declared Dim1D is an invalid module, and the validator would say so.
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("sampler2DArray"), String::npos)
|
||||
<< "the sampler must still be declared as the 2D array the texture is stored as:\n" << essl;
|
||||
EXPECT_NE(essl.find("ivec2"), String::npos)
|
||||
<< "the offset must now be the two-component one ESSL's sampler2DArray overload takes:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The gradients take the identical repair, and through a different SPIRV-Cross branch - the offset
|
||||
// is emitted at `if (args.offset)` and the gradients at `if (args.grad_x || args.grad_y)`, so one
|
||||
// fixture cannot cover both.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesWidensTheGradientsOfA1DLookup) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() { ssb.data = textureGrad(g_sampler0, 0.5, 0.25, 0.125); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("textureGrad"), String::npos) << essl;
|
||||
// Both derivatives have to be widened, not just the first: ESSL's overload takes two vec2s.
|
||||
EXPECT_NE(essl.find("vec2(0.25, 0.0)"), String::npos)
|
||||
<< "dPdx must be widened to two components:\n" << essl;
|
||||
EXPECT_NE(essl.find("vec2(0.125, 0.0)"), String::npos)
|
||||
<< "dPdy must be widened too:\n" << essl;
|
||||
}
|
||||
|
||||
// Scope: a 1D sampler that is only SAMPLED or FETCHED 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 - the same rule the storage-image sibling applies to a 1D image
|
||||
// with no atomic on it. resource-texture's own sampler1D is exactly this shape (it only calls
|
||||
// texelFetch), so this is not a hypothetical.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesLeavesPlainLookupsToSpirvCross) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = texelFetch(g_sampler0, 2, 0) + texture(g_sampler4, vec2(0.5, 1.0));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 2u);
|
||||
EXPECT_FALSE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "no offset and no gradient here, so the probe must say there is nothing to do";
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a 1D sampler with no offset or gradient must pass through byte "
|
||||
"for byte";
|
||||
}
|
||||
|
||||
// The gate is per arrayed-ness, matching the two distinct OpTypeImage declarations glslang emits:
|
||||
// the sampler1DArray carries the offset and is rewritten, while the sampler1D in the same module
|
||||
// is left to SPIRV-Cross. This is resource-texture's own shape.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesRewritesOnlyTheArrayednessThatCarriesTheOffset) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = texelFetch(g_sampler0, 2, 0) +
|
||||
textureLodOffset(g_sampler4, vec2(0.5, 1.0), 0.0, 0);
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 2u);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 1u)
|
||||
<< "the arrayed sampler must be rewritten and the non-arrayed one left alone:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
// Both spellings coincide on ES, which is why a partial rewrite is safe here and is NOT safe
|
||||
// for the storage-image sibling: SPIRV-Cross prints Dim1D as "2D" already, so the stage that
|
||||
// was rewritten and the stage that was not declare the same ESSL type.
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("sampler1D"), String::npos)
|
||||
<< "nothing may reach the driver still spelled 1D:\n" << essl;
|
||||
}
|
||||
|
||||
// The shape that would emit INVALID SPIR-V without the deduplication, and the shape the
|
||||
// conformance case actually has: a 1D sampler and a real 2D sampler of the same sampled type in
|
||||
// one module. Rewriting the first one's Dim in place makes the two OpTypeImage declarations
|
||||
// structurally identical, and SPIR-V forbids duplicate non-aggregate types.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesDeduplicatesAgainstAnExisting2DSampler) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler2D g_sampler1;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = textureLodOffset(g_sampler0, 0.5, 0.0, 1) +
|
||||
textureLod(g_sampler1, vec2(0.5), 0.0);
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 1u);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the rewritten 1D sampler collided with the module's own 2D sampler and left a "
|
||||
"duplicate type declaration behind:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
}
|
||||
|
||||
// The declined shape, for the sibling's reason: textureSize(sampler1D) yields an int and
|
||||
// textureSize(sampler2D) an ivec2, so rewriting the type while leaving the query would hand the
|
||||
// shader a value of the wrong shape. The module is returned untouched rather than half-translated.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesDeclinesAModuleThatQueriesTheTextureSize) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = textureLodOffset(g_sampler0, 0.5, 0.0, 1) + float(textureSize(g_sampler0, 0));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "the fixture must still carry the offset that arms the pass, so that the decline is "
|
||||
"what leaves the module alone rather than the gate:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv)
|
||||
<< "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 1u)
|
||||
<< "declining means the 1D type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
|
||||
@@ -3674,6 +4000,7 @@ namespace {
|
||||
constexpr Uint kGlRgba32ui = 0x8D70;
|
||||
constexpr Uint kGlR8ui = 0x8232;
|
||||
constexpr Uint kGlR32f = 0x822E;
|
||||
constexpr Uint kGlRgb10A2ui = 0x906F;
|
||||
} // namespace
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
|
||||
@@ -3716,14 +4043,23 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
|
||||
|
||||
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
|
||||
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
|
||||
// into the module would trade a missing qualifier for a missing shader. They are left format-less
|
||||
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
|
||||
// which the stencil half of the packed_depth_stencil case binds, is one of them.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
|
||||
// into the module would trade a missing qualifier for a missing shader.
|
||||
//
|
||||
// That still holds for the formats NOTHING can rescue, which are left format-less here and
|
||||
// completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). It stopped
|
||||
// holding for the ones that widen EXACTLY: WidenImageFormatsForEssl runs immediately after this
|
||||
// pass on the ESSL chain and re-declares them in a core carrier SPIRV-Cross does print, so for
|
||||
// those the module is the right place and the text completion would put back the narrow token no
|
||||
// ES driver accepts. r8ui - which the stencil half of the packed_depth_stencil case binds - is
|
||||
// one of the rescued ones; rgb10_a2ui, whose 10/10/10/2 channel widths no core format has, is not.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesOnlyTheFormatsNoCoreCarrierRescues) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, this route can go";
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlR8ui), 0u);
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2ui));
|
||||
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
|
||||
@@ -3736,11 +4072,29 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
|
||||
// ...and the stage still transpiles, which is the whole point of declining.
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
{ // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles.
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb10A2ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format nothing can carry must leave the module untouched";
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
}
|
||||
{ // Unprintable but carriable: baked narrow here, then widened into the carrier, which is
|
||||
// what finally gives the declaration a qualifier ES accepts.
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_NE(baked, spirv) << "a format the widening carries must reach the module";
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(baked));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(baked, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
const String essl = DecompileToEssl(widened);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("rgba8ui"), String::npos)
|
||||
<< "the baked r8ui must come out as the core carrier:\n" << essl;
|
||||
}
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
|
||||
@@ -4225,3 +4579,102 @@ void main() {}
|
||||
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
|
||||
}
|
||||
|
||||
// GL 4.3 core 7.8 puts a storage block with no layout(binding = N) on binding ZERO. Nothing
|
||||
// downstream can still tell which blocks those are, because glslang's IO mapper auto-assigns a
|
||||
// binding out of one flat space and writes it into the qualifier - so the reflection reports the
|
||||
// invention. This scanner is the only surviving record, and it reports POSITIVELY: a block is
|
||||
// named only when it was recognised in full AND recognised as unqualified.
|
||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingNamesOnlyUnqualifiedBlocks) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// KHR-GL43.compute_shader.resource-ubo's own shape: an unqualified storage block alongside
|
||||
// the uniform blocks whose presence is what pushes it off binding 0 today.
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std140) uniform InputBuffer { vec4 data[4]; } g_in_buffer[12];
|
||||
layout(std430) buffer OutputBuffer { vec4 data0[4]; } g_out_buffer;
|
||||
layout(std430, binding = 3) buffer BoundBlock { vec4 data1[4]; } g_bound;
|
||||
layout(binding = 5, std430) buffer BoundFirst { vec4 data2[4]; } g_bound_first;
|
||||
void main() { g_out_buffer.data0[0] = g_in_buffer[0].data[0]; }
|
||||
)";
|
||||
|
||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
||||
EXPECT_EQ(unqualified.count("OutputBuffer"), 1u)
|
||||
<< "the block the test binds at 0 with glBindBufferBase must be recognised";
|
||||
EXPECT_EQ(unqualified.count("BoundBlock"), 0u)
|
||||
<< "a declared binding must never be defaulted away";
|
||||
EXPECT_EQ(unqualified.count("BoundFirst"), 0u)
|
||||
<< "the binding may appear anywhere in the layout list, not only last";
|
||||
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, and
|
||||
// its default is already handled where uniformBlockBinding is seeded. Naming it here would
|
||||
// make the seeder default a resource it does not own.
|
||||
EXPECT_EQ(unqualified.count("InputBuffer"), 0u) << "uniform blocks are out of scope";
|
||||
}
|
||||
|
||||
// The scanner must not mistake a member qualifier, a buffer-typed sampler, or the
|
||||
// "layout(...) buffer;" default-qualifier form for a block declaration - and must record nothing
|
||||
// at all for grammar it does not fully recognise, so that anything surprising keeps today's
|
||||
// behaviour instead of being defaulted on a guess.
|
||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingIgnoresNonBlockBufferTokens) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform samplerBuffer texelSampler;
|
||||
layout(std430) buffer;
|
||||
layout(std430) buffer Real { vec4 v[4]; } realInstance;
|
||||
void main() { realInstance.v[0] = texelFetch(texelSampler, 0); }
|
||||
)";
|
||||
|
||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
||||
EXPECT_EQ(unqualified.count("Real"), 1u);
|
||||
// samplerBuffer is one identifier token, so it can never match the `buffer` keyword; and the
|
||||
// default-qualifier form declares no block, so there is no name to record.
|
||||
EXPECT_EQ(unqualified.size(), 1u)
|
||||
<< "only the one real block declaration may be recorded";
|
||||
}
|
||||
|
||||
// The dangerous direction, because a false positive here DEFAULTS AWAY a binding the shader
|
||||
// really declared. Memory qualifiers may sit between the layout list and the `buffer` keyword in
|
||||
// either order, and the binding has to survive them.
|
||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingKeepsBindingsAcrossMemoryQualifiers) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 1) coherent restrict buffer AfterLayout { uint a; } afterLayout;
|
||||
readonly layout(std430, binding = 2) buffer BeforeLayout { uint b; } beforeLayout;
|
||||
writeonly buffer NoBindingAtAll { uint c; } noBinding;
|
||||
void main() { noBinding.c = afterLayout.a + beforeLayout.b; }
|
||||
)";
|
||||
|
||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
||||
EXPECT_EQ(unqualified.count("AfterLayout"), 0u)
|
||||
<< "coherent/restrict must not break the qualifier run and lose the binding";
|
||||
EXPECT_EQ(unqualified.count("BeforeLayout"), 0u)
|
||||
<< "a qualifier may precede the layout list too";
|
||||
EXPECT_EQ(unqualified.count("NoBindingAtAll"), 1u)
|
||||
<< "a memory-qualified block with no binding is still an unqualified block";
|
||||
}
|
||||
|
||||
// This scans preprocessor-visible text, so a block can be declared twice - once with a binding
|
||||
// and once without. Reporting it as unqualified would default away a binding the active
|
||||
// declaration carries, so any name seen WITH a binding is dropped outright.
|
||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingDropsNamesSeenBothWays) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 4) buffer Ambiguous { uint a; } bound;
|
||||
layout(std430) buffer Ambiguous { uint a; } unbound;
|
||||
layout(std430) buffer Clear { uint b; } clearInstance;
|
||||
void main() { clearInstance.b = 0u; }
|
||||
)";
|
||||
|
||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
||||
EXPECT_EQ(unqualified.count("Ambiguous"), 0u)
|
||||
<< "seen both ways is a doubt, and a doubt must not become a default";
|
||||
EXPECT_EQ(unqualified.count("Clear"), 1u)
|
||||
<< "the unambiguous block alongside it is still recognised";
|
||||
}
|
||||
|
||||
@@ -1374,7 +1374,10 @@ TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
|
||||
{SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32},
|
||||
{SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT},
|
||||
{SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM},
|
||||
{SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32},
|
||||
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2:
|
||||
// the view format and the image format have to name the same bit layout, and
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV is A2B10G10R10. A2R10G10B10 transposes R and B.
|
||||
{SpvImageFormatRgb10A2, VK_FORMAT_A2B10G10R10_UNORM_PACK32},
|
||||
{SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM},
|
||||
{SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM},
|
||||
{SpvImageFormatR16, VK_FORMAT_R16_UNORM},
|
||||
@@ -1397,7 +1400,7 @@ TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
|
||||
{SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT},
|
||||
{SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT},
|
||||
{SpvImageFormatR32ui, VK_FORMAT_R32_UINT},
|
||||
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32},
|
||||
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2B10G10R10_UINT_PACK32},
|
||||
{SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT},
|
||||
{SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT},
|
||||
{SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT},
|
||||
|
||||
@@ -12,8 +12,9 @@ add_executable(
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeStorageBlockArrayIndexTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
WidenImageFormatsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeResourceArrayIndexTest.cpp
|
||||
// Copyright (c) 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 <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// Test-side reference walker, deliberately independent of the production detection so a
|
||||
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
|
||||
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
|
||||
// exactly what the Qualcomm ES compiler refuses.
|
||||
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
|
||||
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpDecorate:
|
||||
if (wordCount >= 3u) {
|
||||
const auto decoration = static_cast<spv::Decoration>(words[2]);
|
||||
if (decoration == spv::Decoration::Block ||
|
||||
decoration == spv::Decoration::BufferBlock) {
|
||||
blockStructs.insert(words[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
// OpTypeArray <result> <element type> <length>
|
||||
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
|
||||
blockArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
// OpTypePointer <result> <storage class> <pointee>
|
||||
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
|
||||
blockArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
// OpVariable <result type> <result> <storage class>
|
||||
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
|
||||
blockArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
// OpAccessChain <result type> <result> <base> <index 0> ...
|
||||
if (wordCount < 5u) return;
|
||||
if (blockArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// The image half of the same reference walker, and equally independent of the production
|
||||
// detection: true when some access chain rooted at an array-of-IMAGES variable carries a
|
||||
// non-constant FIRST index. A UniformConstant array whose element type is an OpTypeImage
|
||||
// with Sampled == 2 is what GLSL spells `image2D g_image[N]`; a sampler array is an
|
||||
// OpTypeSampledImage and is deliberately not matched here, because ESSL allows it a
|
||||
// dynamically-uniform index.
|
||||
bool HasDynamicImageArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> storageImages; // OpTypeImage ids with Sampled == 2
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> imageArrayTypes; // OpTypeArray ids whose element is such an image
|
||||
std::set<Uint32> imageArrayPointers; // OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> imageArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpTypeImage:
|
||||
// OpTypeImage <result> <sampled type> <dim> <depth> <arrayed> <ms> <sampled>
|
||||
if (wordCount >= 8u && words[7] == 2u) storageImages.insert(words[1]);
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
if (wordCount >= 4u && storageImages.count(words[2]) != 0u) {
|
||||
imageArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (wordCount >= 4u && imageArrayTypes.count(words[3]) != 0u) {
|
||||
imageArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
if (wordCount >= 4u && imageArrayPointers.count(words[1]) != 0u) {
|
||||
imageArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
if (wordCount < 5u) return;
|
||||
if (imageArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// The ESSL SPIRV-Cross prints for a module, or the error it refused with. This is where the
|
||||
// rule actually bites: the SPIR-V is legal Vulkan either way, and what a strict ES driver
|
||||
// reads is this text.
|
||||
struct EsslAttempt {
|
||||
Bool succeeded = false;
|
||||
String text;
|
||||
String error;
|
||||
};
|
||||
|
||||
EsslAttempt EmitEssl(const Vector<Uint32>& spirv) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
EsslAttempt attempt;
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) return attempt;
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
if (session.SetOptions(options) != SPVC_SUCCESS) return attempt;
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) {
|
||||
attempt.error = essl.error().log;
|
||||
return attempt;
|
||||
}
|
||||
attempt.succeeded = true;
|
||||
attempt.text = *essl;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
|
||||
// induction variable is a literal after unrolling.
|
||||
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
g_out.data[i] = g_blocks[i].data[0];
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced index - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
|
||||
// can fold it, so the switch/select lowering is what has to carry it.
|
||||
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_blocks[g_index].data[0] = 7u;
|
||||
g_out.value = g_blocks[g_index].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control from the device run: dynamic addressing through an array MEMBER of
|
||||
// ONE block is legal ES and must not be rewritten.
|
||||
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = g_block.data[g_index];
|
||||
}
|
||||
)";
|
||||
|
||||
// A block array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
void main() {
|
||||
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The IMAGE half, and the case that has always been broken independently of any per-element
|
||||
// unit remapping: a plain CONSECUTIVE image array subscripted by a loop variable. This is
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple's own fragment shader shape, and a raw
|
||||
// GLES probe on Mesa 26.1.4 at ES 3.2 refuses the ESSL it produces with "image arrays indexed
|
||||
// with non-constant expressions are forbidden in GLSL ES". Foldable: after unrolling every
|
||||
// subscript is a literal.
|
||||
constexpr const char* kLoopIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
imageStore(g_image[i], ivec2(0), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// The same fold, buried in the loop nest a real image-writing shader has: a tile walk with
|
||||
// the array walk innermost. Every level's trip count is inside the per-loop budget on its
|
||||
// own, so nothing but a NEST budget stops the three from multiplying.
|
||||
constexpr const char* kNestedLoopIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
for (int y = 0; y < 64; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
imageStore(g_image[i], ivec2(x, y), vec4(1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced image index: nothing can fold it, so the switch/select lowering is what
|
||||
// has to carry it. Both directions in one shader, as the block-array fixture does.
|
||||
constexpr const char* kUniformIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform image2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
imageStore(g_image[g_index], ivec2(0), vec4(7.0));
|
||||
g_out.value = imageLoad(g_image[g_index], ivec2(1));
|
||||
}
|
||||
)";
|
||||
|
||||
// An image array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control for the scope decision: ESSL 3.20 4.1.7 allows a SAMPLER array a
|
||||
// dynamically-uniform index, and the same raw GLES probe confirms it - both a loop-variable
|
||||
// subscript and a const-table lookup compile and link. Nothing here may be rewritten.
|
||||
constexpr const char* kUniformIndexedSamplerArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform sampler2D g_tex[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = texture(g_tex[g_index], vec2(0.5));
|
||||
}
|
||||
)";
|
||||
|
||||
// An imageAtomic* reaches the array through OpImageTexelPointer, and running one per element
|
||||
// would perform every other element's atomic as well. The pass has to decline rather than
|
||||
// lower this.
|
||||
constexpr const char* kUniformIndexedImageAtomic = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform uimage2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = imageAtomicAdd(g_image[g_index], ivec2(0), 1u);
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedBlockArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Either half of the legalization is an acceptable outcome here - what the ES driver
|
||||
// cares about is only that no dynamic subscript survives.
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
// One switch for the store, and one select per element past the first for the load.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
// The case no test covered before, and the one that has nothing to do with per-element unit
|
||||
// remapping: an ordinary consecutive image array written from a loop. Every emitted subscript has
|
||||
// to end up a literal, or the ES driver drops the stage and every draw with it.
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedImageArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
// ...and in the text the driver actually reads. Before: `g_image[i]`; after: four literals.
|
||||
const EsslAttempt before = EmitEssl(input);
|
||||
ASSERT_TRUE(before.succeeded) << before.error;
|
||||
EXPECT_NE(before.text.find("g_image[i]"), String::npos) << before.text;
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_EQ(after.text.find("g_image[i]"), String::npos) << after.text;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Marking a loop for unrolling means marking every loop enclosing it - SPIRV-Tools only unrolls
|
||||
// innermost loops, so an outer one is unrollable only once its children are gone - and the copies
|
||||
// those levels produce MULTIPLY. Bounding each loop on its own therefore bounds nothing: with the
|
||||
// per-loop cap alone this nest (64 x 64 x 4, every level inside it) folded to 16384 OpImageWrite,
|
||||
// a 3.68 MB module and 3.6 s of spirv-opt on desktop x86, from twelve lines of GLSL - before
|
||||
// SPIRV-Cross or the device compiler saw any of it. Spending the budget as the walk climbs stops
|
||||
// at the innermost level here, and the switch lowering - whose cost is the ARRAY LENGTH, not the
|
||||
// trip counts - is what legalizes anything the unroll no longer reaches.
|
||||
TEST(LegalizeResourceArrayIndexPass, BoundsTheWholeLoopNestAndNotEachLoopSeparately) {
|
||||
const Vector<Uint32> input = CompileCompute(kNestedLoopIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Still legalized - that is not what is being traded away.
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
// ...and paid for at the budget, not at its cube. 64 is kMaxUnrolledIterations.
|
||||
EXPECT_LE(CountOpcode(output, spv::Op::OpImageWrite), 64u);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedImageWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
// One switch for the imageStore, and one select per element past the first for the imageLoad.
|
||||
// The selection is on the loaded TEXEL, never on the image object - an opaque type cannot be
|
||||
// selected at all - so there is one OpImageRead per element behind those selects.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageRead), 4u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageWrite), 4u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedImageArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// The scope decision, asserted rather than assumed: a sampler array indexed by a uniform is legal
|
||||
// ESSL, so the module must come back untouched - not merely legal, byte for byte the same.
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesADynamicallyIndexedSamplerArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedSamplerArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// An imageAtomic* is the shape the lowering must refuse: its per-element rebuild would run every
|
||||
// other element's read-modify-write. Declining leaves the illegal subscript in place - which is
|
||||
// what the latched warning in LegalizeResourceArrayIndexingForEssl is for - but a half-transform
|
||||
// would corrupt four images instead of losing one stage.
|
||||
TEST(LegalizeResourceArrayIndexPass, DeclinesAUniformIndexedImageAtomic) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageAtomic);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 0u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotentOnImageArrays) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp
|
||||
// Copyright (c) 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 <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// Test-side reference walker, deliberately independent of the production detection so a
|
||||
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
|
||||
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
|
||||
// exactly what the Qualcomm ES compiler refuses.
|
||||
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
|
||||
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpDecorate:
|
||||
if (wordCount >= 3u) {
|
||||
const auto decoration = static_cast<spv::Decoration>(words[2]);
|
||||
if (decoration == spv::Decoration::Block ||
|
||||
decoration == spv::Decoration::BufferBlock) {
|
||||
blockStructs.insert(words[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
// OpTypeArray <result> <element type> <length>
|
||||
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
|
||||
blockArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
// OpTypePointer <result> <storage class> <pointee>
|
||||
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
|
||||
blockArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
// OpVariable <result type> <result> <storage class>
|
||||
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
|
||||
blockArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
// OpAccessChain <result type> <result> <base> <index 0> ...
|
||||
if (wordCount < 5u) return;
|
||||
if (blockArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
|
||||
// induction variable is a literal after unrolling.
|
||||
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
g_out.data[i] = g_blocks[i].data[0];
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced index - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
|
||||
// can fold it, so the switch/select lowering is what has to carry it.
|
||||
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_blocks[g_index].data[0] = 7u;
|
||||
g_out.value = g_blocks[g_index].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control from the device run: dynamic addressing through an array MEMBER of
|
||||
// ONE block is legal ES and must not be rewritten.
|
||||
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = g_block.data[g_index];
|
||||
}
|
||||
)";
|
||||
|
||||
// A block array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
void main() {
|
||||
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, FoldsALoopIndexedBlockArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Either half of the legalization is an acceptable outcome here - what the ES driver
|
||||
// cares about is only that no dynamic subscript survives.
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
// One switch for the store, and one select per element past the first for the load.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -863,6 +863,13 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
v.supportsNoperspectiveInterpolation = true;
|
||||
variants.emplace_back("supportsNoperspectiveInterpolation", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms WidenImageFormatsForEssl - a driver WITH GL_NV_image_formats keeps the declared
|
||||
// rg32f/r8ui/... image formats, one without has them re-declared in a core carrier and
|
||||
// every access to them masked, so the two get materially different ESSL from one module.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.supportsExtendedImageFormats = true;
|
||||
variants.emplace_back("supportsExtendedImageFormats", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms AND parameterizes ClampMultisampleFetchesForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.maxColorTextureSamples = 1;
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.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
|
||||
//
|
||||
// WidenImageFormatsPass exists because GL has forty image formats and GLSL ES core has thirteen,
|
||||
// and no device MobileGL runs on advertises GL_NV_image_formats - so a shader declaring one of the
|
||||
// other twenty-six has no legal ESSL spelling at all. SPIRV-Cross throws for some of them and the
|
||||
// driver rejects the token for the rest ("'rg32f' : not a legal layout qualifier id"), and dropping
|
||||
// the qualifier is refused too ("all images have to define layout format"), so the stage is lost
|
||||
// and every draw with the program silently renders nothing while GL_LINK_STATUS still says TRUE.
|
||||
//
|
||||
// What has to hold is the emulation's exactness, in three parts at once: the DECLARED format must
|
||||
// become the core carrier of the same per-channel width, every imageStore through it must have its
|
||||
// surplus components replaced by GL's own (0.., 1) so the carrier's extra channels never hold
|
||||
// anything GL has not defined, and every imageLoad must come back masked the same way. A module
|
||||
// that declares only core formats - or one of the nine formats with no exact carrier - must come
|
||||
// out untouched, because widening those would be an approximation rather than an emulation. Real
|
||||
// GLSL through the same glslang path the backends use, for the same reason
|
||||
// ClampMultisampleFetchTest.cpp does it: what matters is what glslang actually emits.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileFragment(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER},
|
||||
.program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
// OpTypeImage words: 0 opcode/count, 1 result id, 2 sampled type, 3 Dim, 4 Depth, 5 Arrayed,
|
||||
// 6 MS, 7 Sampled, 8 Format. Sampled == 2 is a storage image, the only kind with a format.
|
||||
struct StorageImageType {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 format = 0u;
|
||||
};
|
||||
|
||||
Vector<StorageImageType> CollectStorageImageTypes(const Vector<Uint32>& spirv) {
|
||||
Vector<StorageImageType> types;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpTypeImage || wordCount < 9u) return;
|
||||
if (words[7] != 2u) return;
|
||||
types.push_back(StorageImageType{words[1], words[8]});
|
||||
});
|
||||
return types;
|
||||
}
|
||||
|
||||
// OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2,
|
||||
// 5.. the component selectors.
|
||||
struct VectorShuffle {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 firstVectorId = 0u;
|
||||
Uint32 secondVectorId = 0u;
|
||||
Vector<Uint32> components;
|
||||
};
|
||||
|
||||
Vector<VectorShuffle> CollectVectorShuffles(const Vector<Uint32>& spirv) {
|
||||
Vector<VectorShuffle> shuffles;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpVectorShuffle || wordCount < 5u) return;
|
||||
VectorShuffle shuffle{};
|
||||
shuffle.resultId = words[2];
|
||||
shuffle.firstVectorId = words[3];
|
||||
shuffle.secondVectorId = words[4];
|
||||
for (Uint32 word = 5u; word < wordCount; ++word) {
|
||||
shuffle.components.push_back(words[word]);
|
||||
}
|
||||
shuffles.push_back(shuffle);
|
||||
});
|
||||
return shuffles;
|
||||
}
|
||||
|
||||
// OpImageWrite words: 0 opcode/count, 1 image, 2 coordinate, 3 texel.
|
||||
Vector<Uint32> CollectImageWriteTexelIds(const Vector<Uint32>& spirv) {
|
||||
Vector<Uint32> texels;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpImageWrite || wordCount < 4u) return;
|
||||
texels.push_back(words[3]);
|
||||
});
|
||||
return texels;
|
||||
}
|
||||
|
||||
// OpImageRead words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate.
|
||||
Vector<Uint32> CollectImageReadResultIds(const Vector<Uint32>& spirv) {
|
||||
Vector<Uint32> results;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpImageRead || wordCount < 5u) return;
|
||||
results.push_back(words[2]);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
Bool HasComponents(const VectorShuffle& shuffle, const Vector<Uint32>& expected) {
|
||||
return shuffle.components == expected;
|
||||
}
|
||||
|
||||
const VectorShuffle* FindShuffleWithResult(const Vector<VectorShuffle>& shuffles, Uint32 resultId) {
|
||||
for (const VectorShuffle& shuffle : shuffles) {
|
||||
if (shuffle.resultId == resultId) return &shuffle;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const VectorShuffle* FindShuffleOver(const Vector<VectorShuffle>& shuffles, Uint32 firstVectorId) {
|
||||
for (const VectorShuffle& shuffle : shuffles) {
|
||||
if (shuffle.firstVectorId == firstVectorId) return &shuffle;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The ESSL SPIRV-Cross emits for a module, or the error it refused with - which is the whole
|
||||
// point for the formats in its is_desktop_only_format set: it THROWS rather than printing a
|
||||
// token, and the throw takes the stage with it.
|
||||
struct EsslAttempt {
|
||||
Bool succeeded = false;
|
||||
String text;
|
||||
String error;
|
||||
};
|
||||
|
||||
EsslAttempt EmitEssl(const Vector<Uint32>& spirv) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
EsslAttempt attempt;
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) return attempt;
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
if (session.SetOptions(options) != SPVC_SUCCESS) return attempt;
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) {
|
||||
attempt.error = essl.error().log;
|
||||
return attempt;
|
||||
}
|
||||
attempt.succeeded = true;
|
||||
attempt.text = *essl;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
// rg32f: two float channels, and the entry the four CTS allFormats walkers abort on. Both an
|
||||
// imageLoad and an imageStore, so both masks are exercised on one image.
|
||||
const char* const kRg32fLoadStore = R"(#version 430 core
|
||||
layout(rg32f, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// r8ui: ONE unsigned-integer channel, and the only format
|
||||
// KHR-GL43.shader_image_load_store.single-byte_data_alignment declares. SPIRV-Cross refuses to
|
||||
// print this one for ESSL at all, so before the widening the stage produced no text whatsoever.
|
||||
const char* const kR8uiLoadStore = R"(#version 430 core
|
||||
layout(r8ui, binding = 0) uniform uimage2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
uvec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), uvec4(7u, 8u, 9u, 10u));
|
||||
fragColor = vec4(texel);
|
||||
}
|
||||
)";
|
||||
|
||||
// rgba32f is one of the thirteen GLSL ES already has; nothing may move.
|
||||
const char* const kCoreFormatLoadStore = R"(#version 430 core
|
||||
layout(rgba32f, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16 is one of the NINE with no core carrier of the same per-channel width. Widening it
|
||||
// would change the quantisation an application sees, so it must be left alone and keep the
|
||||
// honest "no GLSL ES spelling" diagnostic instead.
|
||||
const char* const kRg16LoadStore = R"(#version 430 core
|
||||
layout(rg16, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// The table itself, which is the single source of truth all three layers of the emulation ask -
|
||||
// the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts
|
||||
// the three stop agreeing, and a narrow texture read through a wide image goes out of bounds
|
||||
// silently on every driver tested.
|
||||
TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
|
||||
struct Case {
|
||||
Uint requested;
|
||||
Uint carrier;
|
||||
Uint channels;
|
||||
const char* name;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{0x8230, 0x8814, 2, "GL_RG32F -> GL_RGBA32F"},
|
||||
{0x822F, 0x881A, 2, "GL_RG16F -> GL_RGBA16F"},
|
||||
{0x822D, 0x881A, 1, "GL_R16F -> GL_RGBA16F"},
|
||||
{0x822B, 0x8058, 2, "GL_RG8 -> GL_RGBA8"},
|
||||
{0x8229, 0x8058, 1, "GL_R8 -> GL_RGBA8"},
|
||||
{0x8F95, 0x8F97, 2, "GL_RG8_SNORM -> GL_RGBA8_SNORM"},
|
||||
{0x8F94, 0x8F97, 1, "GL_R8_SNORM -> GL_RGBA8_SNORM"},
|
||||
{0x823B, 0x8D82, 2, "GL_RG32I -> GL_RGBA32I"},
|
||||
{0x8239, 0x8D88, 2, "GL_RG16I -> GL_RGBA16I"},
|
||||
{0x8233, 0x8D88, 1, "GL_R16I -> GL_RGBA16I"},
|
||||
{0x8237, 0x8D8E, 2, "GL_RG8I -> GL_RGBA8I"},
|
||||
{0x8231, 0x8D8E, 1, "GL_R8I -> GL_RGBA8I"},
|
||||
{0x823C, 0x8D70, 2, "GL_RG32UI -> GL_RGBA32UI"},
|
||||
{0x823A, 0x8D76, 2, "GL_RG16UI -> GL_RGBA16UI"},
|
||||
{0x8234, 0x8D76, 1, "GL_R16UI -> GL_RGBA16UI"},
|
||||
{0x8238, 0x8D7C, 2, "GL_RG8UI -> GL_RGBA8UI"},
|
||||
{0x8232, 0x8D7C, 1, "GL_R8UI -> GL_RGBA8UI"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier)
|
||||
<< testCase.name;
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.requested), testCase.channels)
|
||||
<< testCase.name;
|
||||
// Every carrier is one of the thirteen ES has in core, or the widening would have moved
|
||||
// the problem rather than solved it - and every carrier has four channels, or the mask
|
||||
// selectors would address components that are not there.
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.carrier))
|
||||
<< testCase.name;
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.carrier), 4u) << testCase.name;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) {
|
||||
// The thirteen GLSL ES already has: nothing to carry.
|
||||
for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/,
|
||||
0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/,
|
||||
0x8D88u /*RGBA16I*/, 0x8D8Eu /*RGBA8I*/, 0x8235u /*R32I*/,
|
||||
0x8D70u /*RGBA32UI*/, 0x8D76u /*RGBA16UI*/, 0x8D7Cu /*RGBA8UI*/,
|
||||
0x8236u /*R32UI*/}) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u)
|
||||
<< "core format 0x" << std::hex << coreFormat;
|
||||
}
|
||||
// The nine with no core format of the same per-channel width. Carrying these would be an
|
||||
// approximation - a different quantisation, or a different numeric domain for anything that
|
||||
// samples the same texture - so they are deliberately left to the honest diagnostic.
|
||||
for (const Uint hardFormat : {0x8C3Au /*R11F_G11F_B10F*/, 0x8059u /*RGB10_A2*/,
|
||||
0x906Fu /*RGB10_A2UI*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/,
|
||||
0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/,
|
||||
0x8F98u /*R16_SNORM*/}) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u)
|
||||
<< "format without an exact carrier 0x" << std::hex << hardFormat;
|
||||
}
|
||||
// Not an image format at all.
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u);
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
// ...and there is nothing left for a second run to do.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba32f));
|
||||
|
||||
const auto shuffles = CollectVectorShuffles(widened);
|
||||
|
||||
// The STORE. GL drops the components a two-channel format does not have, so the carrier's
|
||||
// blue and alpha must be written as its own 0 and 1, never as what the shader passed.
|
||||
const auto texelIds = CollectImageWriteTexelIds(widened);
|
||||
ASSERT_EQ(texelIds.size(), 1u);
|
||||
const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front());
|
||||
ASSERT_NE(storeMask, nullptr) << "the imageStore texel is not a masked value";
|
||||
EXPECT_TRUE(HasComponents(*storeMask, {0u, 1u, 6u, 7u}))
|
||||
<< "expected (r, g, 0, 1) - components 0 and 1 of the texel, then 2 and 3 of (0,0,0,1)";
|
||||
|
||||
// The LOAD. Same mask, on the other side: GL defines an imageLoad from a two-channel format
|
||||
// as (r, g, 0, 1) whatever the storage holds, which matters for storage this shader never
|
||||
// wrote (glTexStorage with no upload leaves the surplus channels undefined).
|
||||
const auto readIds = CollectImageReadResultIds(widened);
|
||||
ASSERT_EQ(readIds.size(), 1u);
|
||||
const VectorShuffle* loadMask = FindShuffleOver(shuffles, readIds.front());
|
||||
ASSERT_NE(loadMask, nullptr) << "the imageLoad result is consumed unmasked";
|
||||
EXPECT_TRUE(HasComponents(*loadMask, {0u, 1u, 6u, 7u}));
|
||||
EXPECT_NE(loadMask->resultId, readIds.front())
|
||||
<< "the mask must be a separate value, or it would feed itself";
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kR8uiLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba8ui));
|
||||
|
||||
const auto shuffles = CollectVectorShuffles(widened);
|
||||
const auto texelIds = CollectImageWriteTexelIds(widened);
|
||||
ASSERT_EQ(texelIds.size(), 1u);
|
||||
const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front());
|
||||
ASSERT_NE(storeMask, nullptr);
|
||||
// Only red survives; green and blue take the constant's zeroes and alpha its one - the
|
||||
// INTEGER one, not a saturated field, which is what makes the uvec4 constant's fourth
|
||||
// component 1 rather than 0xFF.
|
||||
EXPECT_TRUE(HasComponents(*storeMask, {0u, 5u, 6u, 7u}));
|
||||
|
||||
const auto readIds = CollectImageReadResultIds(widened);
|
||||
ASSERT_EQ(readIds.size(), 1u);
|
||||
const VectorShuffle* loadMask = FindShuffleOver(shuffles, readIds.front());
|
||||
ASSERT_NE(loadMask, nullptr);
|
||||
EXPECT_TRUE(HasComponents(*loadMask, {0u, 5u, 6u, 7u}));
|
||||
}
|
||||
|
||||
// The point of the whole exercise, end to end: what reaches the ES driver.
|
||||
//
|
||||
// r8ui is in SPIRV-Cross's is_desktop_only_format set, so for an ESSL target it THROWS instead of
|
||||
// printing a token and no text is produced at all - which is what
|
||||
// KHR-GL43.shader_image_load_store.single-byte_data_alignment hit ("Attempting to use image format
|
||||
// not supported in ES profile"), leaving a program that linked and drew nothing. rg32f is the
|
||||
// other failure mode: SPIRV-Cross prints it happily and the DRIVER rejects it ("'rg32f' : not a
|
||||
// legal layout qualifier id"). After the widening both come out naming a core format, which is the
|
||||
// only thing on either side that makes the stage compilable.
|
||||
TEST(WidenImageFormats, WidenedModulesEmitEsslNamingTheCoreCarrier) {
|
||||
{
|
||||
const Vector<Uint32> spirv = CompileFragment(kR8uiLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const EsslAttempt before = EmitEssl(spirv);
|
||||
EXPECT_FALSE(before.succeeded)
|
||||
<< "SPIRV-Cross printed r8ui for an ES target; the widening's premise has changed:\n"
|
||||
<< before.text;
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("rgba8ui"), String::npos) << after.text;
|
||||
// "rgba8ui" does not contain "r8ui", so this is a clean negative.
|
||||
EXPECT_EQ(after.text.find("r8ui"), String::npos) << after.text;
|
||||
// GL reads a one-channel image as (r, 0, 0, 1) and drops everything past r on a store, so
|
||||
// both accesses have to be spelled that way whatever the carrier holds.
|
||||
EXPECT_NE(after.text.find("uvec4(0u, 0u, 0u, 1u)"), String::npos) << after.text;
|
||||
}
|
||||
{
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
// This one SPIRV-Cross does print - the token is simply not one GLSL ES has.
|
||||
const EsslAttempt before = EmitEssl(spirv);
|
||||
ASSERT_TRUE(before.succeeded) << before.error;
|
||||
EXPECT_NE(before.text.find("rg32f"), String::npos) << before.text;
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("rgba32f"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rg32f"), String::npos)
|
||||
<< "the token no ES driver accepts is still in the emitted source:\n"
|
||||
<< after.text;
|
||||
}
|
||||
}
|
||||
|
||||
// The narrow mode, for a driver that HAS GL_NV_image_formats - Mesa, which every software lane
|
||||
// runs on. There the driver can spell rg32f, so widening it would spend two to four times the
|
||||
// texture memory to change nothing; but SPIRV-Cross STILL throws for r8ui rather than printing it,
|
||||
// and the throw loses the stage whatever the driver would have accepted. So the extension narrows
|
||||
// the emulation to its is_desktop_only_format set rather than switching it off.
|
||||
TEST(WidenImageFormats, TheExtensionNarrowsTheWideningToWhatSpirvCrossWillNotPrint) {
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(0x8230 /*GL_RG32F*/));
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(0x8232 /*GL_R8UI*/));
|
||||
|
||||
{ // rg32f: printable, so the narrow mode leaves it exactly as declared.
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv, false));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv, true));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened,
|
||||
/*onlyFormatsSpirvCrossRefusesToPrint=*/true, true);
|
||||
if (!widened.empty()) {
|
||||
const auto types = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f));
|
||||
EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size());
|
||||
}
|
||||
}
|
||||
{ // r8ui: unprintable, so the narrow mode still carries it - and must mask it exactly as
|
||||
// the wide mode does, because the storage and the bind widen with it either way.
|
||||
const Vector<Uint32> spirv = CompileFragment(kR8uiLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv, true));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(
|
||||
spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/true, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
const auto types = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba8ui));
|
||||
|
||||
const auto shuffles = CollectVectorShuffles(widened);
|
||||
const auto texelIds = CollectImageWriteTexelIds(widened);
|
||||
ASSERT_EQ(texelIds.size(), 1u);
|
||||
const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front());
|
||||
ASSERT_NE(storeMask, nullptr);
|
||||
EXPECT_TRUE(HasComponents(*storeMask, {0u, 5u, 6u, 7u}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kCoreFormatLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
// The cheap probe is what keeps every ordinary shader off the optimizer entirely.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
if (!widened.empty()) {
|
||||
EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size())
|
||||
<< "a core-format module must gain no masks";
|
||||
const auto types = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba32f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg16LoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
|
||||
// rg16 has no core format with 16-bit unsigned-normalized channels behind it. Anything wider
|
||||
// would requantize differently from what the application asked for, so the pass declines and
|
||||
// CollectImageFormatBakeInputs reports the format as unspellable instead.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
if (!widened.empty()) {
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
}
|
||||
}
|
||||
@@ -5212,3 +5212,77 @@ TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGet
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// The image-format widening's transfer half. GL has forty image formats and GLSL ES core has
|
||||
// thirteen, so an image-bindable GL_R8UI texture is stored as a GL_RGBA8UI and an image-bindable
|
||||
// GL_RG32F as a GL_RGBA32F (TextureImpl::GetImageBindableStorageWidening). The driver is then told
|
||||
// the transfer is four components wide, and one- or two-component client data has to be repacked to
|
||||
// match - with the SAME values GL gives the channels a narrow format does not have, so that a
|
||||
// later sample, imageLoad or glGetTexImage cannot tell the carrier from the real thing.
|
||||
TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingChannelValues) {
|
||||
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PrepareChannelWidenedUpload;
|
||||
|
||||
const IntVec3 texelSize(2, 1, 1);
|
||||
|
||||
// GL_RG32F -> GL_RGBA32F. Blue is 0 and alpha 1.0, which is exactly what GL answers for the
|
||||
// two channels an rg32f image does not have.
|
||||
{
|
||||
const Float source[] = {0.25f, -0.5f, 1.5f, -2.5f};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Float*>(
|
||||
PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_FLOAT, widened, false));
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
ASSERT_EQ(widened.size(), 8 * sizeof(Float));
|
||||
const Float expected[] = {0.25f, -0.5f, 0.0f, 1.0f, 1.5f, -2.5f, 0.0f, 1.0f};
|
||||
for (SizeT i = 0; i < 8; ++i) {
|
||||
EXPECT_FLOAT_EQ(result[i], expected[i]) << "component " << i;
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R8UI -> GL_RGBA8UI. Three added channels, and the one in alpha is the INTEGER one: an
|
||||
// integer format's missing alpha reads back as 1, not as the saturated field a normalized
|
||||
// format's does, and GL_UNSIGNED_BYTE serves both classes so the type alone cannot decide.
|
||||
{
|
||||
const Uint8 source[] = {7, 8};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Uint8*>(PrepareChannelWidenedUpload(
|
||||
1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/true));
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
const Uint8 expected[] = {7, 0, 0, 1, 8, 0, 0, 1};
|
||||
ASSERT_EQ(widened.size(), sizeof(expected));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// GL_R8 -> GL_RGBA8, the normalized twin of the case above: same transfer type, saturated one.
|
||||
{
|
||||
const Uint8 source[] = {7, 8};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Uint8*>(PrepareChannelWidenedUpload(
|
||||
1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/false));
|
||||
const Uint8 expected[] = {7, 0, 0, 0xFF, 8, 0, 0, 0xFF};
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// GL_RG8_SNORM -> GL_RGBA8_SNORM keeps GL_BYTE, whose 1.0 is the positive maximum.
|
||||
{
|
||||
const Int8 source[] = {-1, 2, 3, -4};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Int8*>(
|
||||
PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_BYTE, widened, false));
|
||||
const Int8 expected[] = {-1, 2, 0, 0x7F, 3, -4, 0, 0x7F};
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// A four-component source is already the carrier's shape: nothing to repack, and the caller's
|
||||
// sub-rect upload fast path depends on the pointer coming back unchanged when that is so.
|
||||
{
|
||||
const Uint8 source[] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
Vector<Uint8> widened;
|
||||
EXPECT_EQ(PrepareChannelWidenedUpload(4, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened,
|
||||
false),
|
||||
static_cast<const void*>(source));
|
||||
EXPECT_TRUE(widened.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,9 +287,17 @@ namespace MobileGL {
|
||||
case TexturePixelDataType::UnsignedInt8888Rev:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TexturePixelDataType::UnsignedInt1010102:
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
// GL_UNSIGNED_INT_10_10_10_2 is R in bits 22-31, G 12-21, B 2-11, A 0-1 - an
|
||||
// R10G10B10A2 packing Vulkan has no format for at all. Reported as UNDEFINED
|
||||
// rather than as the A2*10*10*10 neighbours below, which are a different
|
||||
// packing: naming one of those would hand a caller a format whose components
|
||||
// sit in the wrong bits.
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
case TexturePixelDataType::UnsignedInt2101010Rev:
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
// A2**B**10G10R10, for the reason spelled out on
|
||||
// ConvertTextureInternalFormatToVkFormat's RGB10A2: _REV puts R in bits 0-9,
|
||||
// which is A2B10G10R10. A2R10G10B10 silently swaps R and B.
|
||||
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
|
||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||
return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/Lower1DSampledImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
#include "SpirvPasses/WidenImageFormatsPass.h"
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -41,7 +43,7 @@
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeStorageBlockArrayIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
@@ -596,6 +598,43 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleReadsLocatedInput"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
// A LOCATION is exactly the property that separates a user-defined varying (or a
|
||||
// per-patch input) from a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||
// gl_PrimitiveID and the tessellation levels carry none, and every one of them is
|
||||
// either forwarded by the pass-through or generated by the tessellator itself.
|
||||
//
|
||||
// Decided on the OpVariable's own Location decoration rather than on any
|
||||
// built-in classification, for the reason DirectVulkan's
|
||||
// ReflectPassthroughTessControlNeed records at length: gl_in is an ARRAY OF
|
||||
// INTERFACE BLOCKS, and a member walk of one reads back as BuiltIn::Position for
|
||||
// every member, so classifying by built-in would accept anything.
|
||||
for (auto& variable : context->module()->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
Bool located = false;
|
||||
context->get_decoration_mgr()->ForEachDecoration(
|
||||
variable.result_id(), static_cast<uint32_t>(spv::Decoration::Location),
|
||||
[&located](const spvtools::opt::Instruction&) { located = true; });
|
||||
if (located) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
@@ -771,6 +810,39 @@ namespace MobileGL {
|
||||
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
|
||||
}
|
||||
|
||||
bool ShaderCompiler::WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool onlyFormatsSpirvCrossRefusesToPrint,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
WidenImageFormatsPass::CreateWidenImageFormatsPass(onlyFormatsSpirvCrossRefusesToPrint));
|
||||
// Two image types that differed only in a format the widening collapses -
|
||||
// `layout(rg32f)` and `layout(rgba32f)` in one module - are one type afterwards,
|
||||
// and duplicate non-aggregate type declarations are invalid SPIR-V. This joins
|
||||
// them, and cascades to the pointer and array types that named them; the pass
|
||||
// itself deliberately does not carry a join of its own.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("WidenImageFormatsForEssl", optimizer, inputBinary, outputBinary,
|
||||
true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
const bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary,
|
||||
onlyFormatsSpirvCrossRefusesToPrint);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::WidenedCoreEsslImageFormat(glInternalFormat);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::ImageFormatChannelCount(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
@@ -943,16 +1015,16 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(
|
||||
bool ShaderCompiler::LegalizeResourceArrayIndexingForEssl(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Detection gates everything: a module that declares no array of storage
|
||||
// blocks, or indexes one only with constants - every shader but a handful -
|
||||
// pays one BuildModule and is handed back byte for byte, so the folding chain
|
||||
// can never perturb a shader that did not need it.
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
// Detection gates everything: a module that declares no array of storage blocks
|
||||
// and no array of images, or indexes one only with constants - every shader but
|
||||
// a handful - pays one BuildModule and is handed back byte for byte, so the
|
||||
// folding chain can never perturb a shader that did not need it.
|
||||
if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
@@ -968,7 +1040,7 @@ namespace MobileGL {
|
||||
// induction variable as an OpPhi, and glslang emits it as loads and stores of
|
||||
// a Function variable.
|
||||
folder.RegisterPass(CreateLocalMultiStoreElimPass());
|
||||
folder.RegisterPass(LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass());
|
||||
folder.RegisterPass(LegalizeResourceArrayIndexPass::CreateMarkLoopsForUnrollPass());
|
||||
folder.RegisterPass(CreateLoopUnrollPass(true));
|
||||
// Fold the unrolled induction values into the access chains, then clear out
|
||||
// what constant conditions leave behind.
|
||||
@@ -978,14 +1050,14 @@ namespace MobileGL {
|
||||
folder.RegisterPass(CreateBlockMergePass());
|
||||
|
||||
Vector<uint32_t> folded;
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.fold", folder,
|
||||
if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.fold", folder,
|
||||
inputBinary, folded, true, enableSpirvValidation) ||
|
||||
folded.empty()) {
|
||||
// Fail open onto the fallback rather than onto the illegal module.
|
||||
folded = inputBinary;
|
||||
}
|
||||
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
folded)) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
@@ -994,25 +1066,25 @@ namespace MobileGL {
|
||||
// Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it.
|
||||
Optimizer lowerer(SPV_ENV_VULKAN_1_1);
|
||||
lowerer.RegisterPass(
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass());
|
||||
LegalizeResourceArrayIndexPass::CreateLowerToConstantSwitchPass());
|
||||
// The chains the lowering replaced are dead now; remove_outputs must stay
|
||||
// false here for the same reason it does in SanitizeAndOptimizeBinary.
|
||||
lowerer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.lower", lowerer, folded,
|
||||
if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.lower", lowerer, folded,
|
||||
outputBinary, true, enableSpirvValidation) ||
|
||||
outputBinary.empty()) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
if (LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
outputBinary)) {
|
||||
// MGLOG_W, latched, for the same reason the fragment-output one is: this
|
||||
// runs per shader compile and shader packs compile lazily mid-session.
|
||||
MGLOG_W_ONCE("[spirv] LegalizeStorageBlockArrayIndexingForEssl: an array of storage "
|
||||
"blocks is still indexed dynamically; a strict ES driver will reject "
|
||||
"this shader");
|
||||
MGLOG_W_ONCE("[spirv] LegalizeResourceArrayIndexingForEssl: an array of storage "
|
||||
"blocks or of images is still indexed dynamically; a strict ES "
|
||||
"driver will reject this shader");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1094,6 +1166,39 @@ namespace MobileGL {
|
||||
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::Lower1DSampledImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// The overwhelmingly common answer, and the reason the probe exists: no 1D sampler
|
||||
// is reached by an offset or a gradient, so the module is handed back byte for
|
||||
// byte without an Optimizer ever being built. Every ESSL shader in the process
|
||||
// passes through here, so the cost of the case with nothing to do is the cost of
|
||||
// this pass. Note the probe is deliberately NARROWER than "declares a 1D sampler":
|
||||
// SPIRV-Cross emits the plain sample and fetch forms correctly, and taking those
|
||||
// over would be a regression looking for somewhere to happen.
|
||||
if (!Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(Lower1DSampledImagesPass::CreateLower1DSampledImagesPass());
|
||||
// Mandatory, not tidying - the same collision Lower1DArrayImagesForEssl documents
|
||||
// one screen up. Rewriting a 1D sampled image type to the 2D one makes it
|
||||
// structurally IDENTICAL to any real 2D sampled image of the same sampled type the
|
||||
// module already declared, and SPIR-V forbids duplicate non-aggregate type
|
||||
// declarations. That is not exotic here: it is the exact shape of the headline
|
||||
// case, whose compute shader declares sampler1D and sampler2D side by side. The
|
||||
// same applies to the OpTypeSampledImage and OpTypePointer instructions above
|
||||
// them, and to the Sampled1D capability the rewrite turns into a second Shader.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("Lower1DSampledImagesForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -71,6 +71,11 @@ namespace MobileGL {
|
||||
// GL_OES_viewport_array AND integer multisample squeezed to 1) the separate
|
||||
// probes made compile-heavy workloads measurably slower - ReservedNames-class
|
||||
// CTS cases paid ~10%. Callers with more than one armed gate use this instead.
|
||||
// The image-format widening deliberately does NOT ride this probe, even though it
|
||||
// is a module question of exactly the same shape. It is armed on every driver, so
|
||||
// a gate answered from the module would put a BuildModule on every stage of every
|
||||
// program - and the frontend's uniform reflection can answer it for free
|
||||
// (PrgramImpl::ImageFormatBakeInputs::declaresWidenableImageFormat).
|
||||
struct SpirvGateFeatures {
|
||||
Bool WritesViewportIndexOutput = false;
|
||||
Bool DeclaresMultisampledImage = false;
|
||||
@@ -156,18 +161,22 @@ namespace MobileGL {
|
||||
static bool LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Makes every index into an ARRAY OF SHADER STORAGE BLOCKS a constant integral
|
||||
// expression. GL 4.3 allows any dynamically-uniform index there; the Qualcomm
|
||||
// ES compiler enforces the ES 3.1 constant-expression rule and refuses the whole
|
||||
// stage ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted"), which loses the program while the frontend still reports
|
||||
// GL_LINK_STATUS = TRUE. Same two halves as the fragment-output legalization:
|
||||
// fold the loop-derived indices, then lower whatever is genuinely dynamic to a
|
||||
// switch over the array's range. DirectGLES transpile path only - Vulkan has no
|
||||
// such restriction and must keep seeing one descriptor array. Copies the input
|
||||
// through untouched when no block array is indexed dynamically, which is every
|
||||
// shader but a handful. See LegalizeStorageBlockArrayIndexPass.
|
||||
static bool LegalizeStorageBlockArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
// Makes every index into an ARRAY OF SHADER STORAGE BLOCKS or an ARRAY OF IMAGE
|
||||
// UNIFORMS a constant integral expression. Desktop GL allows any
|
||||
// dynamically-uniform index in either; ES keeps the ES 3.1
|
||||
// constant-expression rule for both and the drivers refuse the whole stage
|
||||
// ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted" on Qualcomm, "image arrays indexed with non-constant expressions
|
||||
// are forbidden in GLSL ES" on Mesa), which loses the program while the
|
||||
// frontend still reports GL_LINK_STATUS = TRUE. Same two halves as the
|
||||
// fragment-output legalization: fold the loop-derived indices, then lower
|
||||
// whatever is genuinely dynamic to a switch over the array's range. SAMPLER
|
||||
// arrays are out of scope - ESSL 3.20 4.1.7 permits them a dynamically-uniform
|
||||
// index. DirectGLES transpile path only - Vulkan has no such restriction and
|
||||
// must keep seeing one descriptor array. Copies the input through untouched
|
||||
// when no such array is indexed dynamically, which is every shader but a
|
||||
// handful. See LegalizeResourceArrayIndexPass.
|
||||
static bool LegalizeResourceArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Collapses each synthesized gl_AtomicCounterBlock_<N> into one uint array at
|
||||
@@ -204,6 +213,19 @@ namespace MobileGL {
|
||||
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// The SAMPLED-image counterpart. SPIRV-Cross widens a 1D sampler's COORDINATE for
|
||||
// ES and prints the OFFSET and GRADIENT operands with their original 1D arity, so
|
||||
// textureOffset / textureLodOffset / texelFetchOffset / textureGrad on a
|
||||
// sampler1D(Array) come out with no ESSL overload ("no matching overloaded
|
||||
// function found") and the stage is lost. Rewrites the type to 2D and widens
|
||||
// coordinate, offset and gradients together. DirectGLES transpile path only -
|
||||
// Vulkan has 1D images natively. Copies the input through untouched unless the
|
||||
// module actually carries such an operand on a 1D sampler, so a shader that only
|
||||
// samples or fetches keeps SPIRV-Cross's own correct emission. See
|
||||
// Lower1DSampledImagesPass for what it declines and why.
|
||||
static bool Lower1DSampledImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Gives each format-less storage image the format bound to its image unit, so
|
||||
// the emitted ESSL can carry the format layout qualifier GLSL ES requires of
|
||||
// every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
|
||||
@@ -231,6 +253,39 @@ namespace MobileGL {
|
||||
// must not ask BakeImageFormatsForEssl for those, and completes them in the
|
||||
// emitted text instead.
|
||||
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
|
||||
// Re-declares every storage image whose DECLARED format GLSL ES cannot spell in
|
||||
// the core format that carries it exactly, and masks each access back to the
|
||||
// channels the original format has. The 26 formats outside the ES core set have no
|
||||
// legal ESSL spelling on any tested driver (none exposes GL_NV_image_formats), and
|
||||
// a format-less declaration is rejected too, so the stage is otherwise lost
|
||||
// whatever this backend emits. DirectGLES transpile path only - Vulkan takes the
|
||||
// declared format natively. See WidenImageFormatsPass for the table, for the nine
|
||||
// formats it deliberately does NOT widen, and for why the texture storage and the
|
||||
// glBindImageTexture argument have to move with it.
|
||||
// `onlyFormatsSpirvCrossRefusesToPrint` narrows it to the formats that have no
|
||||
// ESSL route even WITH GL_NV_image_formats, because SPIRV-Cross throws for them
|
||||
// rather than printing a token - which is the whole set a driver that advertises
|
||||
// the extension still needs. See WidenImageFormatsPass.
|
||||
static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares a storage image WidenImageFormatsForEssl would
|
||||
// widen, under the same mode the run would use. Costs its own module parse, so
|
||||
// the transpile path does NOT gate on this - it answers the question from the
|
||||
// frontend's uniform reflection instead, for the reason on SpirvGateFeatures.
|
||||
// Here for tests and for callers that already hold nothing but the binary.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
|
||||
// when it needs no widening or cannot be widened exactly. The single source of
|
||||
// truth for all three layers of the emulation: this one answers the shader, and
|
||||
// DirectGLES asks it again for the texture storage and the image bind, so the two
|
||||
// sides cannot drift.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
// Channels a GL image internal format really has (1-4), 0 when it is not one of
|
||||
// the forty image formats.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
@@ -376,6 +431,19 @@ namespace MobileGL {
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
// what the backends report: no mobile driver can build such a module.
|
||||
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module declares an Input variable carrying a Location - i.e. a
|
||||
// user-defined varying or a per-patch input, as opposed to a built-in.
|
||||
//
|
||||
// Asked of a TESSELLATION EVALUATION stage that has no control stage, to decide
|
||||
// whether the pass-through control stage GL 4.6 core 11.2.2 describes can stand
|
||||
// in for the missing one. That stage forwards gl_Position and nothing else, so a
|
||||
// located input - which the vertex stage feeds today and which would stop
|
||||
// arriving once a control stage sat in between - means the program has to be
|
||||
// declined rather than fed an undefined varying. Same rule, same reasoning, as
|
||||
// DirectVulkan's ReflectPassthroughTessControlNeed, which asks SPIRV-Reflect the
|
||||
// identical question for the identical decision.
|
||||
static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -1548,6 +1548,89 @@ namespace MobileGL {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::set<String> ExtractStorageBlocksWithoutExplicitBinding(const String& source) {
|
||||
std::set<String> names;
|
||||
// Fast path: no storage block, nothing to record. `buffer` as a whole token is
|
||||
// what declares one; samplerBuffer/imageBuffer/textureBuffer tokenize as single
|
||||
// identifiers and so cannot match below, but this substring test is only a
|
||||
// cheap pre-filter and is allowed to be generous.
|
||||
if (source.find("buffer") == String::npos) return names;
|
||||
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
Int braceDepth = 0;
|
||||
// Block names seen WITH a binding. Subtracted at the end so a name that is
|
||||
// declared unqualified in one place and qualified in another is never reported:
|
||||
// this scans preprocessor-visible text, so both arms of a #if can be present,
|
||||
// and defaulting a block the active arm binds explicitly would be a regression.
|
||||
// A name is dropped whenever there is any doubt, never kept.
|
||||
std::set<String> qualified;
|
||||
// The binding the qualifier run currently being scanned declared, -1 for none.
|
||||
// Several layout(...) lists may precede one declaration and the later one wins -
|
||||
// the same accumulate-then-consume shape FindShaderStorageBindingViolation uses.
|
||||
long long binding = -1;
|
||||
long long literal = 0;
|
||||
for (SizeT pos = 0; pos < count; ++pos) {
|
||||
const String& text = tokens[pos].text;
|
||||
if (text == "{") {
|
||||
++braceDepth;
|
||||
binding = -1;
|
||||
continue;
|
||||
}
|
||||
if (text == "}") {
|
||||
if (braceDepth > 0) --braceDepth;
|
||||
continue;
|
||||
}
|
||||
// Only depth-0 declarations are block declarations; `buffer` inside a block
|
||||
// body or a function is a member qualifier or an identifier.
|
||||
if (braceDepth != 0) continue;
|
||||
|
||||
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
|
||||
SizeT j = pos + 2;
|
||||
Int parenDepth = 1;
|
||||
while (j < count && parenDepth > 0) {
|
||||
const String& layoutToken = tokens[j].text;
|
||||
if (layoutToken == "(") {
|
||||
++parenDepth;
|
||||
} else if (layoutToken == ")") {
|
||||
--parenDepth;
|
||||
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
|
||||
tokens[j + 1].text == "=" &&
|
||||
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||
j += 2;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
pos = j - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text == "buffer") {
|
||||
// Recorded ONLY for the fully recognised shape: a block type name
|
||||
// followed by the body's '{'. The "layout(...) buffer;"
|
||||
// default-qualifier form declares no block and has no name to key on,
|
||||
// and anything else here is grammar this scanner does not judge - both
|
||||
// fall through and keep today's behaviour.
|
||||
if (pos + 2 < count && IsIdentifierToken(tokens[pos + 1]) &&
|
||||
tokens[pos + 2].text == "{") {
|
||||
(binding < 0 ? names : qualified).insert(tokens[pos + 1].text);
|
||||
}
|
||||
binding = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Qualifiers may sit between the layout list and the `buffer` keyword;
|
||||
// anything else ends the run, so a binding never leaks onto an unrelated
|
||||
// declaration - and, just as importantly, the ABSENCE of one never does.
|
||||
if (!IsNonLayoutQualifierKeyword(text)) binding = -1;
|
||||
}
|
||||
for (const String& name : qualified) {
|
||||
names.erase(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
|
||||
// A backend that advertises nothing has no ceiling to enforce.
|
||||
if (maxBindings <= 0) return std::nullopt;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <set>
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/ProgramState/ShaderObject.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
@@ -65,6 +67,24 @@ namespace MobileGL {
|
||||
// grammar discipline as ExtractExplicitUniformLocations).
|
||||
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
||||
|
||||
// The BLOCK TYPE NAMES of the shader storage blocks this source declares WITHOUT a
|
||||
// layout(binding = N) qualifier. GL 4.3 core 7.8 gives such a block a buffer binding
|
||||
// of ZERO, which the application may then move with glShaderStorageBlockBinding.
|
||||
//
|
||||
// Exists because nothing downstream can still tell. Every shader is parsed as a
|
||||
// Vulkan client, so glslang's IO mapper allocates a binding for the block out of one
|
||||
// flat space shared with every sampler, image and uniform block in the program
|
||||
// (iomapper.cpp resolveBinding: `set = openGl ? resource : ent.newSet`, and openGl is
|
||||
// 0 here) - and then WRITES IT BACK into the type's qualifier, so the reflection
|
||||
// reports an auto-assigned number as if the shader had declared it. An unqualified
|
||||
// block therefore lands on 0 only when nothing else claimed 0 first.
|
||||
//
|
||||
// Reported POSITIVELY - only blocks the scanner recognised in full, and recognised as
|
||||
// carrying no binding - so anything outside its narrow grammar is left to the
|
||||
// existing behaviour rather than defaulted on a guess. Same discipline as
|
||||
// ExtractExplicitOpaqueBindings.
|
||||
std::set<String> ExtractStorageBlocksWithoutExplicitBinding(const String& source);
|
||||
|
||||
// A shader storage block whose layout(binding = N) reaches or passes
|
||||
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
|
||||
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "BakeImageFormatsPass.h"
|
||||
|
||||
#include "WidenImageFormatsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
@@ -418,7 +420,18 @@ namespace MobileGL {
|
||||
// Those formats are completed in the emitted text instead (see
|
||||
// PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for
|
||||
// them, which is exactly the state that pass looks for.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) continue;
|
||||
//
|
||||
// UNLESS the format widens exactly: WidenImageFormatsPass runs immediately
|
||||
// after this one on the ESSL chain and rewrites it to a core four-channel
|
||||
// carrier SPIRV-Cross does print, masking the accesses back to the channels
|
||||
// the baked format has. So for those the module IS the right place, and
|
||||
// routing them to the text completion instead would spell the narrow format
|
||||
// the driver rejects. The two lists are asked in this order because
|
||||
// printability is the cheaper and more common answer.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format)) &&
|
||||
WidenImageFormatsPass::WidenedCoreEsslImageFormat(formatIt->second) == 0) {
|
||||
continue;
|
||||
}
|
||||
// spirv-val: "Expected Image Format to match Sampled Type". A bind format
|
||||
// whose class disagrees with the declaration is an application error GL
|
||||
// leaves undefined; baking it would turn that into an invalid module, so it
|
||||
|
||||
@@ -54,18 +54,42 @@ namespace MobileGL {
|
||||
// cases fail on BOTH backends and on both an Adreno 830 and a Mali G925 - i.e. on every
|
||||
// device, because no device has shaderFloat64 and the demotion therefore always runs:
|
||||
//
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-cs
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-vs
|
||||
// KHR-GL43.compute_shader.fp64-case1
|
||||
// KHR-GL43.compute_shader.fp64-case3
|
||||
// ...and the std430 half of the same stdLayout case.
|
||||
//
|
||||
// They fail in the two ways this comment predicts and in no other. stdLayout-case3
|
||||
// copies a block byte for byte: the output matches the input for bytes [0, 76) and is
|
||||
// zero from there on, which is exactly the block's size once every double became a
|
||||
// float and the layout repacked tightly. fp64-case1 reports ceil(2.2) as 2: the
|
||||
// uniform's double 2.0 is 0x4000000000000000, the demoted read takes its low 32 bits
|
||||
// (0.0), ceil(0.0 + 0.2) = 1.0f = 0x3F800000 lands in the low half of the 8-byte
|
||||
// output slot and the whole thing prints as 2.
|
||||
// fp64-case3 is NOT this pass's to fix, and is listed only so it stops being counted
|
||||
// against it: it is blocked on GLSL subroutines ("FP64 support - subroutines"), which
|
||||
// glslang deletes when targeting SPIR-V, and is out of scope by standing instruction.
|
||||
//
|
||||
// The other three fail in the two ways this comment predicts and in no other.
|
||||
// stdLayout-case3 copies a block byte for byte: the output matches the input for
|
||||
// bytes [0, 76) and is zero from there on, which is exactly the block's size once
|
||||
// every double became a float and the layout repacked tightly. Re-derived byte-exactly
|
||||
// in 2026-08: the block is `int data0; float data1[5]; mat3x2 data2; double data3;
|
||||
// double data4[2]; int data5; dvec3 data6`, and demoting every double to float and
|
||||
// repacking std430 gives data0@0, data1@4..23, data2@24..47, data3@48, data4@52..59,
|
||||
// data5@60, data6@64..75 - 76 bytes. EVERY mismatching byte the QPA reports is >= 76
|
||||
// and every expected-non-zero byte below 76 matched, on both the std140 output and the
|
||||
// std430 one.
|
||||
//
|
||||
// ONE TRAP FOR THE NEXT READER, because it reads as evidence AGAINST demotion and is
|
||||
// not: in the std430 output the doubles below the boundary appear to have round-tripped
|
||||
// BIT-EXACTLY, which looks like fp64 surviving. It is an artifact. The shader reads and
|
||||
// writes through the SAME demoted offset, so those four bytes are copied verbatim
|
||||
// whatever they are interpreted as - the copy proves nothing about the width.
|
||||
//
|
||||
// fp64-case1 reports ceil(2.2) as 2: the uniform's double 2.0 is 0x4000000000000000,
|
||||
// the demoted read takes its low 32 bits (0.0), ceil(0.0 + 0.2) = 1.0f = 0x3F800000
|
||||
// lands in the low half of the 8-byte output slot and the whole thing prints as 2.
|
||||
// Index 0 of the same case PASSES by accident, for the same reason - writing 0.0f into
|
||||
// the low half of 1.0 leaves it unchanged - so a partial pass here is not progress.
|
||||
//
|
||||
// Both backends produce a CHARACTER-FOR-CHARACTER identical QPA byte list, which is
|
||||
// the cheapest available proof that the defect is in this shared pass and in neither
|
||||
// backend. A future wave that wants to re-open this should start by re-checking that
|
||||
// identity rather than by re-deriving the layout.
|
||||
//
|
||||
// Fixing them means NOT demoting a double that lives in a buffer block, and carrying
|
||||
// it as a uvec2 word pair instead - preserving the application's byte layout exactly,
|
||||
@@ -73,9 +97,10 @@ namespace MobileGL {
|
||||
// the same dmat problem the paragraph above describes (a uvec2 representation cannot
|
||||
// express a matrix stride either, so it would have to decline dmat types), and the
|
||||
// default-uniform routing above reflects the demoted module, so a representation
|
||||
// change there ripples into every glUniform*d. Four of 16085 cases; deliberately not
|
||||
// attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
|
||||
// green.
|
||||
// change there ripples into every glUniform*d. THREE actionable cases of 16085 (the
|
||||
// fourth, fp64-case3, is subroutine-blocked and unreachable from here); deliberately
|
||||
// not attempted, and re-confirmed as not worth attempting in the 2026-08 wave.
|
||||
// compute_shader.fp64-case2 passes today and any attempt has to keep it green.
|
||||
//
|
||||
// Declines (leaves the module byte-identical, so the caller's existing "this module
|
||||
// still declares Float64" failure path reports it) when the module contains an
|
||||
|
||||
@@ -52,6 +52,10 @@ namespace MobileGL {
|
||||
// lowering, whose cost is the array length rather than the trip count, takes
|
||||
// it instead. Real shaders of this shape (Minecraft 26.3's OIT coefficient
|
||||
// writer included) iterate a handful of times.
|
||||
//
|
||||
// This is a budget for the whole NEST, not for one loop: marking a loop for
|
||||
// unrolling means marking its ancestors too (see MarkLoopsForUnroll), and the
|
||||
// copies they produce multiply.
|
||||
constexpr size_t kMaxUnrolledIterations = 64;
|
||||
|
||||
struct DynamicIndexUse {
|
||||
@@ -228,12 +232,17 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether fully unrolling |loop| is bounded work. The trip count is read the
|
||||
// same way the stock unroller reads it, so a loop this declines to measure is
|
||||
// one CanPerformUnroll would refuse anyway - the hint would be inert on it,
|
||||
// and the fallback lowering is what handles it. Requires the induction
|
||||
// variable to already be an OpPhi, which is why this runs after ssa-rewrite.
|
||||
bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) {
|
||||
// |loop|'s trip count, when it has a measurable one, in *outIterations. The
|
||||
// count is read the same way the stock unroller reads it, so a loop this
|
||||
// declines to measure is one CanPerformUnroll would refuse anyway - the hint
|
||||
// would be inert on it, and the fallback lowering is what handles it. Requires
|
||||
// the induction variable to already be an OpPhi, which is why this runs after
|
||||
// ssa-rewrite.
|
||||
//
|
||||
// A count of zero is reported as unmeasurable: it means nothing this pass can
|
||||
// multiply a nest's budget by, and a loop that never runs is not one whose
|
||||
// subscript needs folding.
|
||||
bool TryGetUnrollTripCount(spvtools::opt::Loop* loop, size_t* outIterations) {
|
||||
const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock();
|
||||
if (condition == nullptr) {
|
||||
return false;
|
||||
@@ -243,10 +252,12 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
size_t iterations = 0;
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) {
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations) ||
|
||||
iterations == 0) {
|
||||
return false;
|
||||
}
|
||||
return iterations <= kMaxUnrolledIterations;
|
||||
*outIterations = iterations;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -287,25 +298,51 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The offending chain's own loop AND every loop enclosing it, because
|
||||
// SPIRV-Tools only ever unrolls an INNERMOST loop and because the index that
|
||||
// has to become a literal may be an outer loop's induction variable.
|
||||
//
|
||||
// Marking a whole nest means the unrolled body count is the PRODUCT of its
|
||||
// trip counts, so the budget is spent as the walk climbs rather than tested
|
||||
// loop by loop - a nest of three levels each individually inside the cap is
|
||||
// its CUBE, which is neither bounded nor anything the fold chain downstream
|
||||
// can absorb. Same defect, same shape, and the same reasoning as
|
||||
// LegalizeResourceArrayIndexPass::MarkLoopsForUnroll, which is where it was
|
||||
// first measured; the two walks are deliberately identical.
|
||||
//
|
||||
// Every exit is a BREAK rather than a skip-and-keep-climbing: a loop that
|
||||
// cannot be marked is a gap the unroller cannot cross, which makes every
|
||||
// mark above it dead weight. Falling out of the unroll path costs nothing
|
||||
// correctness-wise - LowerToConstantSwitch still legalizes the chain, at a
|
||||
// cost proportional to the output array's length.
|
||||
spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function);
|
||||
size_t nestIterations = 1;
|
||||
for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr;
|
||||
loop = loop->GetParent()) {
|
||||
if (!IsBoundedUnrollCandidate(loop)) {
|
||||
continue;
|
||||
size_t iterations = 0;
|
||||
if (!TryGetUnrollTripCount(loop, &iterations)) {
|
||||
break;
|
||||
}
|
||||
// Division, not multiplication, so the test itself cannot overflow.
|
||||
if (iterations > kMaxUnrolledIterations / nestIterations) {
|
||||
break;
|
||||
}
|
||||
Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst();
|
||||
// Only a bare `None` control is promoted, and only when no extra
|
||||
// literal (PartialCount, PeelCount, ...) follows it: the unroller
|
||||
// tests the control word for equality with Unroll, so ORing the bit
|
||||
// into a control that already carries something - DontUnroll above
|
||||
// all - would neither unroll nor mean what it says.
|
||||
// all - would neither unroll nor mean what it says. An `Unroll` this
|
||||
// pass itself already wrote for another chain in the same nest ends the
|
||||
// walk too: everything above it was considered on that pass through.
|
||||
if (mergeInst == nullptr || mergeInst->NumOperands() != 3 ||
|
||||
mergeInst->GetSingleWordOperand(2) !=
|
||||
static_cast<uint32_t>(spv::LoopControlMask::MaskNone)) {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
mergeInst->SetOperand(
|
||||
2, {static_cast<uint32_t>(spv::LoopControlMask::Unroll)});
|
||||
nestIterations *= iterations;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
+422
-72
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.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
|
||||
@@ -6,7 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "LegalizeStorageBlockArrayIndexPass.h"
|
||||
#include "LegalizeResourceArrayIndexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
@@ -39,25 +39,37 @@ namespace MobileGL {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS is 16 on the devices MobileGL targets, and
|
||||
// each lowered element costs one basic block per write, so a module claiming
|
||||
// more than this is refused rather than exploded. The largest array in the
|
||||
// conformance suite is 8.
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS and GL_MAX_*_IMAGE_UNIFORMS are both 16 or
|
||||
// fewer on the devices MobileGL targets, and each lowered element costs one
|
||||
// basic block per write, so a module claiming more than this is refused rather
|
||||
// than exploded. The largest array in the conformance suite is 8.
|
||||
constexpr uint32_t kMaxLoweredArrayLength = 32;
|
||||
// One CFG-changing rewrite per round (analyses are dropped after each), so
|
||||
// the round budget bounds the work on a pathological module.
|
||||
constexpr int kMaxLoweringRounds = 256;
|
||||
// Full unrolling copies the body once per iteration, and nothing in the stock
|
||||
// unroller bounds that. Past this count the loop is left alone and the switch
|
||||
// lowering, whose cost is the array length rather than the trip count, takes
|
||||
// it instead. A loop over an array of storage blocks iterates at most
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS times in any shader that is not already
|
||||
// broken.
|
||||
// unroller bounds that. Past this many copies the nest is left alone and the
|
||||
// switch lowering, whose cost is the array length rather than the trip count,
|
||||
// takes it instead. A loop over an array of storage blocks or of images iterates
|
||||
// at most GL_MAX_*_SHADER_STORAGE_BLOCKS / GL_MAX_*_IMAGE_UNIFORMS times in any
|
||||
// shader that is not already broken.
|
||||
//
|
||||
// This is a budget for the whole NEST, not for one loop: marking a loop for
|
||||
// unrolling means marking its ancestors too (see MarkLoopsForUnroll), and the
|
||||
// copies they produce multiply.
|
||||
constexpr size_t kMaxUnrolledIterations = 64;
|
||||
|
||||
struct ResourceArray {
|
||||
uint32_t length = 0;
|
||||
// Which lowering the chain's uses need; see the header. Detection and
|
||||
// loop-marking are identical for both.
|
||||
bool isImage = false;
|
||||
};
|
||||
|
||||
struct DynamicIndexUse {
|
||||
Instruction* accessChain = nullptr;
|
||||
uint32_t arrayLength = 0;
|
||||
bool isImageArray = false;
|
||||
};
|
||||
|
||||
bool HasDecoration(IRContext* context, uint32_t id, spv::Decoration kind) {
|
||||
@@ -73,16 +85,24 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every variable that is an ARRAY OF STORAGE BLOCKS, mapped to that array's
|
||||
// length. Two spellings are accepted because both reach here depending on the
|
||||
// SPIR-V version glslang targets: StorageBuffer + Block (1.3, what MobileGL
|
||||
// asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM block
|
||||
// array - Uniform + Block - is deliberately NOT collected; see the header.
|
||||
// Every variable that is an ARRAY OF STORAGE BLOCKS or an ARRAY OF IMAGE
|
||||
// UNIFORMS, mapped to that array's length and kind.
|
||||
//
|
||||
// Storage blocks: two spellings are accepted because both reach here depending
|
||||
// on the SPIR-V version glslang targets: StorageBuffer + Block (1.3, what
|
||||
// MobileGL asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM
|
||||
// block array - Uniform + Block - is deliberately NOT collected; see the header.
|
||||
//
|
||||
// Images: UniformConstant + OpTypeArray of OpTypeImage. Sampled == 2 is what
|
||||
// separates a storage image - what GLSL calls `image2D` and what the ES rule is
|
||||
// about - from the OpTypeImage that sits INSIDE an OpTypeSampledImage, which
|
||||
// never appears as an array element type on its own here and whose array ESSL
|
||||
// 3.20 4.1.7 explicitly permits a dynamically-uniform index.
|
||||
//
|
||||
// A length that is not a plain OpConstant (a spec constant) maps to 0: still
|
||||
// detected as illegal ESSL, never lowered.
|
||||
std::unordered_map<uint32_t, uint32_t> CollectStorageBlockArrays(IRContext* context) {
|
||||
std::unordered_map<uint32_t, uint32_t> blockArrays;
|
||||
std::unordered_map<uint32_t, ResourceArray> CollectResourceArrays(IRContext* context) {
|
||||
std::unordered_map<uint32_t, ResourceArray> resourceArrays;
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
|
||||
@@ -93,7 +113,8 @@ namespace MobileGL {
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::StorageBuffer &&
|
||||
storageClass != spv::StorageClass::Uniform) {
|
||||
storageClass != spv::StorageClass::Uniform &&
|
||||
storageClass != spv::StorageClass::UniformConstant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,17 +127,33 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
Instruction* elementType = defUseMgr->GetDef(pointeeType->GetSingleWordInOperand(0));
|
||||
if (elementType == nullptr || elementType->opcode() != spv::Op::OpTypeStruct) {
|
||||
if (elementType == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isStorageBlock =
|
||||
storageClass == spv::StorageClass::StorageBuffer
|
||||
? HasDecoration(context, elementType->result_id(), spv::Decoration::Block)
|
||||
: HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::BufferBlock);
|
||||
if (!isStorageBlock) {
|
||||
continue;
|
||||
bool isImage = false;
|
||||
if (storageClass == spv::StorageClass::UniformConstant) {
|
||||
// OpTypeImage <result> <sampled type> <dim> <depth> <arrayed> <ms>
|
||||
// <sampled> <format>
|
||||
if (elementType->opcode() != spv::Op::OpTypeImage ||
|
||||
elementType->NumInOperands() < 6 ||
|
||||
elementType->GetSingleWordInOperand(5) != 2u) {
|
||||
continue;
|
||||
}
|
||||
isImage = true;
|
||||
} else {
|
||||
if (elementType->opcode() != spv::Op::OpTypeStruct) {
|
||||
continue;
|
||||
}
|
||||
const bool isStorageBlock =
|
||||
storageClass == spv::StorageClass::StorageBuffer
|
||||
? HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::Block)
|
||||
: HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::BufferBlock);
|
||||
if (!isStorageBlock) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t arrayLength = 0;
|
||||
@@ -125,9 +162,9 @@ namespace MobileGL {
|
||||
if (lengthConstant != nullptr && lengthConstant->AsIntConstant() != nullptr) {
|
||||
arrayLength = lengthConstant->AsIntConstant()->GetU32BitValue();
|
||||
}
|
||||
blockArrays.emplace(inst.result_id(), arrayLength);
|
||||
resourceArrays.emplace(inst.result_id(), ResourceArray{arrayLength, isImage});
|
||||
}
|
||||
return blockArrays;
|
||||
return resourceArrays;
|
||||
}
|
||||
|
||||
// "Constant integral expression" in the ESSL sense: an OpConstant (or the
|
||||
@@ -140,17 +177,17 @@ namespace MobileGL {
|
||||
def->opcode() == spv::Op::OpConstantNull);
|
||||
}
|
||||
|
||||
// Access chains that index an array of storage blocks with a non-constant.
|
||||
// Only the FIRST index is considered: it is the one that selects the block,
|
||||
// and it is the only one ESSL constrains here. Indices inside the block - the
|
||||
// member selector and any array subscript below it - are legal however they
|
||||
// are computed, and chains rooted at another access chain are already inside
|
||||
// one element.
|
||||
// Access chains that index an array of storage blocks or of images with a
|
||||
// non-constant. Only the FIRST index is considered: it is the one that selects
|
||||
// the element, and it is the only one ESSL constrains here. Indices inside the
|
||||
// block - the member selector and any array subscript below it - are legal
|
||||
// however they are computed, and chains rooted at another access chain are
|
||||
// already inside one element.
|
||||
std::vector<DynamicIndexUse> CollectDynamicIndexUses(IRContext* context) {
|
||||
std::vector<DynamicIndexUse> uses;
|
||||
const std::unordered_map<uint32_t, uint32_t> blockArrays =
|
||||
CollectStorageBlockArrays(context);
|
||||
if (blockArrays.empty()) {
|
||||
const std::unordered_map<uint32_t, ResourceArray> resourceArrays =
|
||||
CollectResourceArrays(context);
|
||||
if (resourceArrays.empty()) {
|
||||
return uses;
|
||||
}
|
||||
|
||||
@@ -164,14 +201,15 @@ namespace MobileGL {
|
||||
if (inst.NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
const auto arrayIt = blockArrays.find(inst.GetSingleWordInOperand(0));
|
||||
if (arrayIt == blockArrays.end()) {
|
||||
const auto arrayIt = resourceArrays.find(inst.GetSingleWordInOperand(0));
|
||||
if (arrayIt == resourceArrays.end()) {
|
||||
continue;
|
||||
}
|
||||
if (IsConstantIndex(context, inst.GetSingleWordInOperand(1))) {
|
||||
continue;
|
||||
}
|
||||
uses.push_back({&inst, arrayIt->second});
|
||||
uses.push_back(
|
||||
{&inst, arrayIt->second.length, arrayIt->second.isImage});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,12 +290,17 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether fully unrolling |loop| is bounded work. The trip count is read the
|
||||
// same way the stock unroller reads it, so a loop this declines to measure is
|
||||
// one CanPerformUnroll would refuse anyway - the hint would be inert on it,
|
||||
// and the fallback lowering is what handles it. Requires the induction
|
||||
// variable to already be an OpPhi, which is why this runs after ssa-rewrite.
|
||||
bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) {
|
||||
// |loop|'s trip count, when it has a measurable one, in *outIterations. The
|
||||
// count is read the same way the stock unroller reads it, so a loop this
|
||||
// declines to measure is one CanPerformUnroll would refuse anyway - the hint
|
||||
// would be inert on it, and the fallback lowering is what handles it. Requires
|
||||
// the induction variable to already be an OpPhi, which is why this runs after
|
||||
// ssa-rewrite.
|
||||
//
|
||||
// A count of zero is reported as unmeasurable: it means nothing this pass can
|
||||
// multiply a nest's budget by, and a loop that never runs is not one whose
|
||||
// subscript needs folding.
|
||||
bool TryGetUnrollTripCount(spvtools::opt::Loop* loop, size_t* outIterations) {
|
||||
const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock();
|
||||
if (condition == nullptr) {
|
||||
return false;
|
||||
@@ -267,14 +310,16 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
size_t iterations = 0;
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) {
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations) ||
|
||||
iterations == 0) {
|
||||
return false;
|
||||
}
|
||||
return iterations <= kMaxUnrolledIterations;
|
||||
*outIterations = iterations;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
bool LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
const std::vector<uint32_t>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
@@ -289,11 +334,11 @@ namespace MobileGL {
|
||||
return !CollectDynamicIndexUses(context.get()).empty();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::Process() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::Process() {
|
||||
return m_mode == Mode::MarkLoopsForUnroll ? MarkLoopsForUnroll() : LowerToConstantSwitch();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::MarkLoopsForUnroll() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::MarkLoopsForUnroll() {
|
||||
auto* irContext = context();
|
||||
const std::vector<DynamicIndexUse> uses = CollectDynamicIndexUses(irContext);
|
||||
if (uses.empty()) {
|
||||
@@ -311,25 +356,58 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The offending chain's own loop AND every loop enclosing it, because
|
||||
// SPIRV-Tools only ever unrolls an INNERMOST loop - an outer one is
|
||||
// unrollable only once its children are gone - and because the index that
|
||||
// has to become a literal may be an outer loop's induction variable.
|
||||
//
|
||||
// Marking a whole nest means the unrolled body count is the PRODUCT of its
|
||||
// trip counts, not the largest of them, so the budget is spent as the walk
|
||||
// climbs rather than tested loop by loop. Measured through
|
||||
// LegalizeResourceArrayIndexingForEssl itself, on a twelve-line shader
|
||||
// writing image2D g_image[4] from a 64/64/4 nest - every level individually
|
||||
// inside the per-loop cap, which is all this used to test: 256 OpImageWrite
|
||||
// with the per-loop cap alone against 4 with the nest budget, and the
|
||||
// per-loop cap bounds nothing at all as the trip counts grow. The image half
|
||||
// of this pass is what made such a nest reachable; a storage-block array
|
||||
// rarely sits inside one. BoundsTheWholeLoopNestAndNotEachLoopSeparately is
|
||||
// that measurement.
|
||||
//
|
||||
// Every exit is a BREAK rather than a skip-and-keep-climbing. A loop that
|
||||
// cannot be marked - unmeasurable, out of budget, or carrying a control this
|
||||
// pass will not overwrite - is a gap the unroller cannot cross, which makes
|
||||
// every mark above it dead weight on a module that will not be unrolled
|
||||
// anyway. Falling out of the unroll path costs nothing correctness-wise:
|
||||
// LowerToConstantSwitch still legalizes the chain, at a cost proportional to
|
||||
// the ARRAY LENGTH rather than to the trip counts.
|
||||
spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function);
|
||||
size_t nestIterations = 1;
|
||||
for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr;
|
||||
loop = loop->GetParent()) {
|
||||
if (!IsBoundedUnrollCandidate(loop)) {
|
||||
continue;
|
||||
size_t iterations = 0;
|
||||
if (!TryGetUnrollTripCount(loop, &iterations)) {
|
||||
break;
|
||||
}
|
||||
// Division, not multiplication, so the test itself cannot overflow.
|
||||
if (iterations > kMaxUnrolledIterations / nestIterations) {
|
||||
break;
|
||||
}
|
||||
Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst();
|
||||
// Only a bare `None` control is promoted, and only when no extra
|
||||
// literal (PartialCount, PeelCount, ...) follows it: the unroller
|
||||
// tests the control word for equality with Unroll, so ORing the bit
|
||||
// into a control that already carries something - DontUnroll above
|
||||
// all - would neither unroll nor mean what it says.
|
||||
// all - would neither unroll nor mean what it says. An `Unroll` this
|
||||
// pass itself already wrote for another chain in the same nest ends the
|
||||
// walk too: everything above it was considered on that pass through.
|
||||
if (mergeInst == nullptr || mergeInst->NumOperands() != 3 ||
|
||||
mergeInst->GetSingleWordOperand(2) !=
|
||||
static_cast<uint32_t>(spv::LoopControlMask::MaskNone)) {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
mergeInst->SetOperand(
|
||||
2, {static_cast<uint32_t>(spv::LoopControlMask::Unroll)});
|
||||
nestIterations *= iterations;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -337,11 +415,11 @@ namespace MobileGL {
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
MGLOG_D("[spirv] storage-block array index: marked enclosing loops for full unrolling");
|
||||
MGLOG_D("[spirv] resource array index: marked enclosing loops for full unrolling");
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::LowerToConstantSwitch() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::LowerToConstantSwitch() {
|
||||
auto* irContext = context();
|
||||
|
||||
bool modified = false;
|
||||
@@ -357,7 +435,8 @@ namespace MobileGL {
|
||||
if (declined.count(use.accessChain->result_id()) != 0) {
|
||||
continue;
|
||||
}
|
||||
const LoweringOutcome outcome = LowerOneChain(use.accessChain, use.arrayLength);
|
||||
const LoweringOutcome outcome =
|
||||
LowerOneChain(use.accessChain, use.arrayLength, use.isImageArray);
|
||||
if (outcome == LoweringOutcome::Declined) {
|
||||
declined.insert(use.accessChain->result_id());
|
||||
continue;
|
||||
@@ -383,17 +462,21 @@ namespace MobileGL {
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength) {
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength,
|
||||
bool isImageArray) {
|
||||
auto* irContext = context();
|
||||
if (arrayLength == 0 || arrayLength > kMaxLoweredArrayLength) {
|
||||
MGLOG_D("[spirv] storage-block array index: array length %u is not lowerable",
|
||||
MGLOG_D("[spirv] resource array index: array length %u is not lowerable",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (!IsLowerableIndexType(irContext, accessChain->GetSingleWordInOperand(1))) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (isImageArray) {
|
||||
return LowerImageChain(accessChain, arrayLength);
|
||||
}
|
||||
|
||||
std::vector<Instruction*> stores;
|
||||
std::vector<Instruction*> loads;
|
||||
@@ -458,8 +541,8 @@ namespace MobileGL {
|
||||
// every path. An index outside [0, length) reaches the default target, which is
|
||||
// the merge block: nothing is stored, which is what indexing a block array out of
|
||||
// range already meant.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength,
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* store) {
|
||||
auto* irContext = context();
|
||||
BasicBlock* block = irContext->get_instr_block(store);
|
||||
@@ -543,8 +626,8 @@ namespace MobileGL {
|
||||
// pick with OpSelect. Reading the elements the shader did not ask for is safe -
|
||||
// every one of them is a storage block this stage already declares, and an ES
|
||||
// driver bounds-checks a storage buffer read that lands outside what is bound.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength,
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load) {
|
||||
auto* irContext = context();
|
||||
uint32_t conditionTypeId = 0;
|
||||
@@ -594,16 +677,283 @@ namespace MobileGL {
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::MarkLoopsForUnroll));
|
||||
// An image array's chain is never stored or loaded THROUGH the way a storage
|
||||
// block's is: it is OpLoad-ed once into an opaque image object, and the image ops
|
||||
// consume that object. So this resolves the chain one CONSUMER at a time - the
|
||||
// round loop in LowerToConstantSwitch recollects after each - and refuses anything
|
||||
// that is not a plain read or write of the loaded image.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageChain(Instruction* accessChain, uint32_t arrayLength) {
|
||||
auto* irContext = context();
|
||||
|
||||
std::vector<Instruction*> loads;
|
||||
bool unsupportedUse = false;
|
||||
irContext->get_def_use_mgr()->ForEachUser(accessChain, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
return;
|
||||
case spv::Op::OpLoad:
|
||||
// Memory operands would be dropped by the per-element rebuild, so a
|
||||
// load carrying any is refused instead.
|
||||
if (user->NumInOperands() == 1) {
|
||||
loads.push_back(user);
|
||||
} else {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// OpImageTexelPointer above all: that is how an imageAtomic* reaches
|
||||
// the array, and running one per element would perform every OTHER
|
||||
// element's atomic as well - a read can be thrown away, a
|
||||
// read-modify-write cannot.
|
||||
unsupportedUse = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (unsupportedUse) {
|
||||
MGLOG_D("[spirv] image array index: chain %%%u has a use this pass cannot rewrite",
|
||||
accessChain->result_id());
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
if (loads.empty()) {
|
||||
// No uses left: the chain itself is what detection is still seeing.
|
||||
irContext->KillInst(accessChain);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
Instruction* load = loads.front();
|
||||
Instruction* consumer = nullptr;
|
||||
bool unsupportedConsumer = false;
|
||||
irContext->get_def_use_mgr()->ForEachUser(load, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
return;
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageRead:
|
||||
if (consumer == nullptr) consumer = user;
|
||||
return;
|
||||
default:
|
||||
// A sampled-image construction, a query, a copy, an argument to a
|
||||
// function: shapes whose per-element rebuild this pass cannot spell
|
||||
// exactly.
|
||||
unsupportedConsumer = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (unsupportedConsumer) {
|
||||
MGLOG_D("[spirv] image array index: the image loaded from chain %%%u is consumed by "
|
||||
"an operation this pass cannot rewrite",
|
||||
accessChain->result_id());
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (consumer == nullptr) {
|
||||
irContext->KillInst(load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
return consumer->opcode() == spv::Op::OpImageWrite
|
||||
? LowerImageWrite(accessChain, arrayLength, load, consumer)
|
||||
: LowerImageRead(accessChain, arrayLength, load, consumer);
|
||||
}
|
||||
|
||||
// Drops |load| and |accessChain| once the rewrite above has taken their last user,
|
||||
// in that order - the load is what uses the chain. Anything still using either is
|
||||
// another consumer a later round will come back for.
|
||||
void LegalizeResourceArrayIndexPass::KillImageChainIfDead(Instruction* accessChain,
|
||||
Instruction* load) {
|
||||
auto* irContext = context();
|
||||
if (irContext->get_def_use_mgr()->NumUsers(load) == 0) {
|
||||
irContext->KillInst(load);
|
||||
}
|
||||
if (irContext->get_def_use_mgr()->NumUsers(accessChain) == 0) {
|
||||
irContext->KillInst(accessChain);
|
||||
}
|
||||
}
|
||||
|
||||
// switch (idx) { case 0: imageStore(arr[0], ...); break; case 1: ... }
|
||||
//
|
||||
// The same block split as LowerStore, for the same reason: whatever followed the
|
||||
// write still runs exactly once on every path, and an index outside [0, length)
|
||||
// reaches the default target - the merge block - so nothing is written, which is
|
||||
// what indexing an image array out of range already meant.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageWrite(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load, Instruction* imageWrite) {
|
||||
auto* irContext = context();
|
||||
BasicBlock* block = irContext->get_instr_block(imageWrite);
|
||||
if (block == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
// Splitting a loop header keeps the label - and so the back edge's target - on
|
||||
// the first half while the OpLoopMerge moves to the second, which is not a loop
|
||||
// any more. Refuse instead of producing that.
|
||||
if (block->GetLoopMergeInst() != nullptr) {
|
||||
MGLOG_D("[spirv] image array index: write sits in a loop header, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
Function* function = block->GetParent();
|
||||
if (function == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
const uint32_t imageTypeId = load->type_id();
|
||||
// Coordinate, texel and any image operands, verbatim: only the image itself is
|
||||
// per-element.
|
||||
std::vector<Operand> tailOperands;
|
||||
for (uint32_t i = 1; i < imageWrite->NumInOperands(); ++i) {
|
||||
tailOperands.push_back(imageWrite->GetInOperand(i));
|
||||
}
|
||||
|
||||
const uint32_t mergeLabelId = irContext->TakeNextId();
|
||||
block->SplitBasicBlock(irContext, mergeLabelId, BasicBlock::iterator(imageWrite));
|
||||
// |imageWrite| now heads the merge block; the per-element writes replace it.
|
||||
irContext->KillInst(imageWrite);
|
||||
|
||||
std::vector<std::pair<Operand::OperandData, uint32_t>> targets;
|
||||
targets.reserve(arrayLength);
|
||||
BasicBlock* insertAfter = block;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t caseLabelId = irContext->TakeNextId();
|
||||
auto caseBlock = MakeUnique<BasicBlock>(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLabel, 0, caseLabelId, std::initializer_list<Operand>{}));
|
||||
caseBlock->SetParent(function);
|
||||
BasicBlock* casePtr = function->InsertBasicBlockAfter(std::move(caseBlock), insertAfter);
|
||||
// Hand-built label; see LowerStore for why it has to be registered here.
|
||||
irContext->AnalyzeDefUse(casePtr->GetLabelInst());
|
||||
irContext->set_instr_block(casePtr->GetLabelInst(), casePtr);
|
||||
|
||||
InstructionBuilder caseBuilder(
|
||||
irContext, casePtr,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(caseBuilder, irContext, accessChain, constantId);
|
||||
Instruction* elementImage =
|
||||
caseBuilder.AddLoad(imageTypeId, elementChain->result_id());
|
||||
|
||||
std::vector<Operand> writeOperands;
|
||||
writeOperands.push_back({SPV_OPERAND_TYPE_ID, {elementImage->result_id()}});
|
||||
for (const Operand& tailOperand : tailOperands) {
|
||||
writeOperands.push_back(tailOperand);
|
||||
}
|
||||
caseBuilder.AddInstruction(
|
||||
MakeUnique<Instruction>(irContext, spv::Op::OpImageWrite, 0, 0, writeOperands));
|
||||
caseBuilder.AddBranch(mergeLabelId);
|
||||
|
||||
targets.push_back({Operand::OperandData{element}, caseLabelId});
|
||||
insertAfter = casePtr;
|
||||
}
|
||||
|
||||
InstructionBuilder switchBuilder(
|
||||
irContext, block, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
switchBuilder.AddSwitch(indexId, mergeLabelId, targets, mergeLabelId);
|
||||
|
||||
KillImageChainIfDead(accessChain, load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] image array index: lowered a dynamic imageStore to a %u-way switch",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
// A read needs no control flow: read every element through a constant index and pick
|
||||
// with OpSelect. The selection happens on the RESULT, not on the image object - an
|
||||
// opaque type may not be selected at all (pre-1.4 OpSelect takes pointers, scalars
|
||||
// and vectors only, and ESSL has no ternary on an image), so what is duplicated is
|
||||
// the OpImageRead.
|
||||
//
|
||||
// Reading the elements the shader did not ask for is safe: every one of them is an
|
||||
// image this stage already declares, and GL 4.6 7.11.2 makes a load through an
|
||||
// image unit whose binding is missing or incompatible return undefined DATA - never
|
||||
// an error, and never a fault - which the select then discards. Contrast an
|
||||
// imageAtomic*, which LowerImageChain refuses for exactly the opposite reason.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageRead(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load, Instruction* imageRead) {
|
||||
auto* irContext = context();
|
||||
uint32_t conditionTypeId = 0;
|
||||
uint32_t dimension = 0;
|
||||
if (!TryGetSelectConditionType(irContext, imageRead->type_id(), &conditionTypeId,
|
||||
&dimension)) {
|
||||
MGLOG_D("[spirv] image array index: read type is not selectable, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
const uint32_t boolTypeId = irContext->get_type_mgr()->GetBoolTypeId();
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
const uint32_t imageTypeId = load->type_id();
|
||||
std::vector<Operand> tailOperands;
|
||||
for (uint32_t i = 1; i < imageRead->NumInOperands(); ++i) {
|
||||
tailOperands.push_back(imageRead->GetInOperand(i));
|
||||
}
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, imageRead,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
uint32_t selectedId = 0;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(builder, irContext, accessChain, constantId);
|
||||
Instruction* elementImage = builder.AddLoad(imageTypeId, elementChain->result_id());
|
||||
|
||||
std::vector<Operand> readOperands;
|
||||
readOperands.push_back({SPV_OPERAND_TYPE_ID, {elementImage->result_id()}});
|
||||
for (const Operand& tailOperand : tailOperands) {
|
||||
readOperands.push_back(tailOperand);
|
||||
}
|
||||
Instruction* elementRead = builder.AddInstruction(
|
||||
MakeUnique<Instruction>(irContext, spv::Op::OpImageRead, imageRead->type_id(),
|
||||
irContext->TakeNextId(), readOperands));
|
||||
if (element == 0) {
|
||||
// Element 0 is the else-arm of the whole ladder, so an out-of-range
|
||||
// index reads it - an undefined element for an undefined index.
|
||||
selectedId = elementRead->result_id();
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* isElement =
|
||||
builder.AddBinaryOp(boolTypeId, spv::Op::OpIEqual, indexId, constantId);
|
||||
uint32_t conditionId = isElement->result_id();
|
||||
if (dimension > 1) {
|
||||
std::vector<uint32_t> components(dimension, conditionId);
|
||||
conditionId = builder.AddCompositeConstruct(conditionTypeId, components)->result_id();
|
||||
}
|
||||
selectedId = builder
|
||||
.AddSelect(imageRead->type_id(), conditionId,
|
||||
elementRead->result_id(), selectedId)
|
||||
->result_id();
|
||||
}
|
||||
|
||||
irContext->ReplaceAllUsesWith(imageRead->result_id(), selectedId);
|
||||
irContext->KillInst(imageRead);
|
||||
KillImageChainIfDead(accessChain, load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] image array index: lowered a dynamic imageLoad to %u constant-indexed "
|
||||
"reads",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass() {
|
||||
LegalizeResourceArrayIndexPass::CreateMarkLoopsForUnrollPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::LowerToConstantSwitch));
|
||||
MakeUnique<LegalizeResourceArrayIndexPass>(Mode::MarkLoopsForUnroll));
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeResourceArrayIndexPass::CreateLowerToConstantSwitchPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeResourceArrayIndexPass>(Mode::LowerToConstantSwitch));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
@@ -0,0 +1,157 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.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>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Desktop GL lets an ARRAY OF SHADER STORAGE BLOCKS and an ARRAY OF IMAGE UNIFORMS
|
||||
// alike be indexed with any dynamically-uniform expression (GL 4.6 core / GLSL 4.30
|
||||
// 4.1.9). GLSL ES keeps the stricter ES 3.1 rule for BOTH - the index must be a
|
||||
// *constant integral expression* - and the drivers enforce it to the letter:
|
||||
//
|
||||
// Qualcomm, storage blocks:
|
||||
// '[' : indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted
|
||||
// Mesa, images:
|
||||
// image arrays indexed with non-constant expressions are forbidden in GLSL ES
|
||||
//
|
||||
// glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints
|
||||
// `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` - or
|
||||
// `uniform image2D g_image[4];` plus `imageStore(g_image[i], ...)` - verbatim, and
|
||||
// the stage never compiles. The backend program then links nothing and every draw
|
||||
// or dispatch that uses it is a silent no-op, which reads back as "the buffer was
|
||||
// never written" rather than as an error - the frontend has already published
|
||||
// GL_LINK_STATUS = TRUE from glslang's own link.
|
||||
//
|
||||
// Verified on the device for the storage-block half: an Adreno 830 ES probe with no
|
||||
// MobileGL in the loop rejects the non-constant subscript with AND without
|
||||
// GL_EXT_gpu_shader5 (which the driver does advertise), and accepts a constant one.
|
||||
// Verified again for the image half on llvmpipe / Mesa 26.1.4 at ES 3.2, on a raw
|
||||
// GLES probe: a scalar image and an array with literal subscripts both write the
|
||||
// units they name, and both a loop-variable subscript and a `const int[]` table
|
||||
// lookup are refused with the message above. So the ES 3.2 "dynamically uniform"
|
||||
// relaxation is not a way out for either resource - every index really has to
|
||||
// become a compile-time constant.
|
||||
//
|
||||
// SAMPLER arrays are deliberately NOT covered. ESSL 3.20 4.1.7 does allow a sampler
|
||||
// array a dynamically-uniform index, and the same probe confirms it: a sampler array
|
||||
// subscripted by a loop variable, and one reached through a const table, both compile
|
||||
// and link. Lowering them would cost code for a rule that does not exist.
|
||||
//
|
||||
// Two modes, used as two halves of one legalization in
|
||||
// ShaderCompiler::LegalizeResourceArrayIndexingForEssl - the same shape, for
|
||||
// the same reasons, as LegalizeFragmentOutputIndexPass:
|
||||
//
|
||||
// MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the
|
||||
// common shape, and full unrolling turns its index into a literal at no cost
|
||||
// in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose
|
||||
// OpLoopMerge carries the Unroll control, so this mode sets that hint on
|
||||
// exactly the loops that enclose an offending access chain, and only when
|
||||
// their trip count is known and small. Must run AFTER ssa-rewrite: both the
|
||||
// trip-count check and the unroller need the induction variable as an OpPhi.
|
||||
// Resource-kind-blind: the offending chain is the same instruction either way.
|
||||
//
|
||||
// LowerToConstantSwitch - the fallback for a genuinely dynamic index
|
||||
// (uniform-sourced, which is what the CTS indirect-addressing and resource-max
|
||||
// cases use). A write through such a chain becomes an OpSwitch over the
|
||||
// array's range with one constant-indexed access per case; a read becomes one
|
||||
// constant-indexed access per element combined with OpSelect. This is what ANGLE
|
||||
// does for the same ES 3.1 rule.
|
||||
//
|
||||
// This half IS kind-specific, because the two resources are consumed
|
||||
// differently. A storage block is reached by OpStore/OpLoad THROUGH the access
|
||||
// chain, so the chain's own users are rewritten. An image's access chain is
|
||||
// first OpLoad-ed into an opaque image OBJECT, which OpImageWrite/OpImageRead
|
||||
// then consume - and an opaque type may not be selected (OpSelect is restricted
|
||||
// to pointers, scalars and vectors before SPIR-V 1.4, and ESSL has no ternary on
|
||||
// image types at all), so it is the image OPERATION that is duplicated per
|
||||
// element, not the loaded object.
|
||||
//
|
||||
// A UNIFORM block array is a different namespace with its own (less strictly
|
||||
// enforced) rule and no observed failure, so it is deliberately left alone rather
|
||||
// than lowered on speculation.
|
||||
//
|
||||
// DirectGLES transpile path only: the original module is legal for Vulkan, which
|
||||
// has no such restriction, and DirectVulkan must keep seeing the array as one
|
||||
// descriptor array.
|
||||
//
|
||||
// The pass DECLINES - leaving the module untouched rather than half-transforming
|
||||
// it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a
|
||||
// function or chained further, an atomic or an OpArrayLength through the chain, a
|
||||
// load carrying memory operands, a spec-constant array length, an index that is
|
||||
// not a 32-bit integer, an image operation other than a plain read or write (an
|
||||
// OpImageTexelPointer, i.e. an imageAtomic*, above all - executing it per element
|
||||
// would perform the other elements' atomics too), or a store sitting in a loop
|
||||
// header block (splitting there would move the OpLoopMerge away from the back
|
||||
// edge's target).
|
||||
class LegalizeResourceArrayIndexPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode {
|
||||
MarkLoopsForUnroll,
|
||||
LowerToConstantSwitch,
|
||||
};
|
||||
|
||||
explicit LegalizeResourceArrayIndexPass(Mode mode) : m_mode(mode) {}
|
||||
|
||||
const char* name() const override {
|
||||
return m_mode == Mode::MarkLoopsForUnroll
|
||||
? "mobilegl-mark-resource-array-index-loops"
|
||||
: "mobilegl-lower-resource-array-index";
|
||||
}
|
||||
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass();
|
||||
static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass();
|
||||
|
||||
// The detection half, on a serialized module: true when an array of storage
|
||||
// blocks or of image uniforms is indexed with anything but an OpConstant. Cheap
|
||||
// enough to gate the whole legalization on (one BuildModule, no serialization)
|
||||
// and used again after the folding chain to decide whether the fallback has to
|
||||
// run at all.
|
||||
static bool BinaryHasDynamicResourceArrayIndexing(const std::vector<uint32_t>& binary);
|
||||
|
||||
private:
|
||||
enum class LoweringOutcome {
|
||||
// The shape is not one this pass can rewrite exactly; the module keeps
|
||||
// the illegal chain rather than a half-transform of it.
|
||||
Declined,
|
||||
Changed,
|
||||
};
|
||||
|
||||
Status MarkLoopsForUnroll();
|
||||
Status LowerToConstantSwitch();
|
||||
|
||||
LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
bool isImageArray);
|
||||
LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* store);
|
||||
LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load);
|
||||
LoweringOutcome LowerImageChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength);
|
||||
void KillImageChainIfDead(spvtools::opt::Instruction* accessChain,
|
||||
spvtools::opt::Instruction* load);
|
||||
LoweringOutcome LowerImageWrite(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load,
|
||||
spvtools::opt::Instruction* imageWrite);
|
||||
LoweringOutcome LowerImageRead(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load,
|
||||
spvtools::opt::Instruction* imageRead);
|
||||
|
||||
Mode m_mode;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,120 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.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>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL 4.3 lets an ARRAY OF SHADER STORAGE BLOCKS be indexed with any
|
||||
// dynamically-uniform expression (GL 4.6 core / GLSL 4.30 4.1.9). GLSL ES keeps
|
||||
// the stricter ES 3.1 rule - the index must be a *constant integral expression* -
|
||||
// and the Qualcomm ES compiler enforces it to the letter:
|
||||
//
|
||||
// '[' : indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted
|
||||
//
|
||||
// glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints
|
||||
// `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` verbatim
|
||||
// and the stage never compiles. The backend program then links nothing and every
|
||||
// draw or dispatch that uses it is a silent no-op, which reads back as "the buffer
|
||||
// was never written" rather than as an error - the frontend has already published
|
||||
// GL_LINK_STATUS = TRUE from glslang's own link.
|
||||
//
|
||||
// Verified on the device: an Adreno 830 ES probe with no MobileGL in the loop
|
||||
// rejects the non-constant subscript with AND without GL_EXT_gpu_shader5 (which
|
||||
// the driver does advertise), and accepts a constant one. So the ES 3.2
|
||||
// "dynamically uniform" relaxation is not a way out - every index really has to
|
||||
// become a compile-time constant.
|
||||
//
|
||||
// Two modes, used as two halves of one legalization in
|
||||
// ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl - the same shape, for
|
||||
// the same reasons, as LegalizeFragmentOutputIndexPass:
|
||||
//
|
||||
// MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the
|
||||
// common shape, and full unrolling turns its index into a literal at no cost
|
||||
// in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose
|
||||
// OpLoopMerge carries the Unroll control, so this mode sets that hint on
|
||||
// exactly the loops that enclose an offending access chain, and only when
|
||||
// their trip count is known and small. Must run AFTER ssa-rewrite: both the
|
||||
// trip-count check and the unroller need the induction variable as an OpPhi.
|
||||
//
|
||||
// LowerToConstantSwitch - the fallback for a genuinely dynamic index
|
||||
// (uniform-sourced, which is what the CTS indirect-addressing and resource-max
|
||||
// cases use). A write through such a chain becomes an OpSwitch over the
|
||||
// array's range with one constant-indexed store per case; a read becomes one
|
||||
// constant-indexed load per element combined with OpSelect. This is what ANGLE
|
||||
// does for the same ES 3.1 rule.
|
||||
//
|
||||
// Storage blocks only. A UNIFORM block array is a different namespace with its own
|
||||
// (less strictly enforced) rule and no observed failure, so it is deliberately left
|
||||
// alone rather than lowered on speculation.
|
||||
//
|
||||
// DirectGLES transpile path only: the original module is legal for Vulkan, which
|
||||
// has no such restriction, and DirectVulkan must keep seeing the array as one
|
||||
// descriptor array.
|
||||
//
|
||||
// The pass DECLINES - leaving the module untouched rather than half-transforming
|
||||
// it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a
|
||||
// function or chained further, an atomic or an OpArrayLength through the chain, a
|
||||
// load carrying memory operands, a spec-constant array length, an index that is
|
||||
// not a 32-bit integer, or a store sitting in a loop header block (splitting there
|
||||
// would move the OpLoopMerge away from the back edge's target).
|
||||
class LegalizeStorageBlockArrayIndexPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode {
|
||||
MarkLoopsForUnroll,
|
||||
LowerToConstantSwitch,
|
||||
};
|
||||
|
||||
explicit LegalizeStorageBlockArrayIndexPass(Mode mode) : m_mode(mode) {}
|
||||
|
||||
const char* name() const override {
|
||||
return m_mode == Mode::MarkLoopsForUnroll
|
||||
? "mobilegl-mark-storage-block-array-index-loops"
|
||||
: "mobilegl-lower-storage-block-array-index";
|
||||
}
|
||||
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass();
|
||||
static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass();
|
||||
|
||||
// The detection half, on a serialized module: true when an array of storage
|
||||
// blocks is indexed with anything but an OpConstant. Cheap enough to gate the
|
||||
// whole legalization on (one BuildModule, no serialization) and used again
|
||||
// after the folding chain to decide whether the fallback has to run at all.
|
||||
static bool BinaryHasDynamicStorageBlockArrayIndexing(const std::vector<uint32_t>& binary);
|
||||
|
||||
private:
|
||||
enum class LoweringOutcome {
|
||||
// The shape is not one this pass can rewrite exactly; the module keeps
|
||||
// the illegal chain rather than a half-transform of it.
|
||||
Declined,
|
||||
Changed,
|
||||
};
|
||||
|
||||
Status MarkLoopsForUnroll();
|
||||
Status LowerToConstantSwitch();
|
||||
|
||||
LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength);
|
||||
LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* store);
|
||||
LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load);
|
||||
|
||||
Mode m_mode;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,652 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.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 "Lower1DSampledImagesPass.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;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// 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;
|
||||
|
||||
// Sampled == 1 is SPIR-V's "used WITH a sampler", i.e. exactly the sampler
|
||||
// uniforms this pass exists for. Sampled == 2 is the storage image
|
||||
// Lower1DArrayImagesPass owns, and Sampled == 0 ("either") is a shape glslang
|
||||
// never emits from GLSL - left out so an unexpected module is declined rather
|
||||
// than rewritten on a guess.
|
||||
bool Is1DSampledImageType(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(kSampledOperand) == 1u;
|
||||
}
|
||||
|
||||
bool Is1DSampledImageTypeOfArrayedness(const Instruction* imageType, bool arrayed) {
|
||||
return Is1DSampledImageType(imageType) &&
|
||||
(imageType->GetSingleWordInOperand(kArrayedOperand) == 1u) == arrayed;
|
||||
}
|
||||
|
||||
// Any Dim1D image still declared with Sampled == 1. Used only to decide whether
|
||||
// the Sampled1D capability is still needed after the rewrite.
|
||||
bool AnyDim1DSampledTypeLeft(IRContext* context) {
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DSampledImageType(&type)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a bare image, a
|
||||
// sampled image, or a pointer/array of either. Same unwrapping as
|
||||
// Lower1DArrayImagesPass, which needs the identical walk.
|
||||
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 a
|
||||
// sampler uniform may be declared as an array of samplers.
|
||||
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;
|
||||
}
|
||||
|
||||
// How this pass classifies an opcode that can touch one of these images.
|
||||
enum class OpKind {
|
||||
// Not an image operation at all: it may CARRY the image or sampled-image
|
||||
// value (OpLoad, OpSampledImage, OpCopyObject, ...) but it names no
|
||||
// coordinate, so the rewrite does not reach it.
|
||||
NotImageOp,
|
||||
// Addresses texels: has a coordinate at in-operand 1 and, from
|
||||
// `imageOperandsIndex`, an optional image-operands mask.
|
||||
Texel,
|
||||
// Reads a property whose result does not depend on Dim. Safe to leave.
|
||||
DimIndependentQuery,
|
||||
// Recognised, and refused: rewriting the type would change the shape of what
|
||||
// the shader consumes, or the operation is one this pass has no translation
|
||||
// for.
|
||||
Decline,
|
||||
};
|
||||
|
||||
struct OpClassification {
|
||||
OpKind kind = OpKind::NotImageOp;
|
||||
uint32_t coordinateOperand = 1;
|
||||
// In-operand index of the ImageOperands mask, when the opcode has one. The
|
||||
// mask itself is OPTIONAL for the implicit-Lod, fetch and gather forms, so
|
||||
// this is an index to test against NumInOperands(), not a promise.
|
||||
uint32_t imageOperandsIndex = 0;
|
||||
};
|
||||
|
||||
OpClassification ClassifyOpcode(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
// (image, coordinate, [operands]) - the mask, when present, is in-operand 2.
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageFetch:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSparseFetch:
|
||||
return {OpKind::Texel, 1u, 2u};
|
||||
|
||||
// (image, coordinate, D_ref, [operands]) - one operand more before the mask.
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjDrefExplicitLod:
|
||||
return {OpKind::Texel, 1u, 3u};
|
||||
|
||||
// OpImageQueryLod names a coordinate and no mask. Its coordinate is the PLANE
|
||||
// components only (no array layer), which the insert-at-1 rule widens just as
|
||||
// correctly as a sampling coordinate.
|
||||
case spv::Op::OpImageQueryLod:
|
||||
return {OpKind::Texel, 1u, /*no mask*/ 0xFFFFFFFFu};
|
||||
|
||||
// Scalar result, identical for Dim1D and Dim2D.
|
||||
case spv::Op::OpImageQueryLevels:
|
||||
return {OpKind::DimIndependentQuery, 0u, 0u};
|
||||
|
||||
// textureSize: int for a sampler1D, ivec2 for the sampler2D it would become.
|
||||
// There is no correct narrower answer to substitute, so the module is left
|
||||
// alone - the sibling pass refuses the same shape for the same reason.
|
||||
case spv::Op::OpImageQuerySize:
|
||||
case spv::Op::OpImageQuerySizeLod:
|
||||
// Gather is not available for 1D samplers in GLSL, so reaching one here means
|
||||
// an input this pass did not anticipate; and its ConstOffsets operand is an
|
||||
// ARRAY of offsets whose widening this pass does not implement.
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
// Storage-image traffic has no business reaching a Sampled == 1 image; if it
|
||||
// does, the module is not the shape this pass reasoned about.
|
||||
case spv::Op::OpImageRead:
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageSparseRead:
|
||||
case spv::Op::OpImageTexelPointer:
|
||||
case spv::Op::OpImageQuerySamples:
|
||||
return {OpKind::Decline, 0u, 0u};
|
||||
|
||||
default:
|
||||
return {OpKind::NotImageOp, 0u, 0u};
|
||||
}
|
||||
}
|
||||
|
||||
// How many ids each ImageOperands bit contributes, in the bit order SPIR-V lays
|
||||
// them out in. Only the bits that carry ids need an entry; the rest contribute
|
||||
// nothing and are skipped by having a count of zero.
|
||||
struct ImageOperandBit {
|
||||
spv::ImageOperandsMask bit;
|
||||
uint32_t idCount;
|
||||
};
|
||||
constexpr ImageOperandBit kImageOperandBits[] = {
|
||||
{spv::ImageOperandsMask::Bias, 1u},
|
||||
{spv::ImageOperandsMask::Lod, 1u},
|
||||
{spv::ImageOperandsMask::Grad, 2u},
|
||||
{spv::ImageOperandsMask::ConstOffset, 1u},
|
||||
{spv::ImageOperandsMask::Offset, 1u},
|
||||
{spv::ImageOperandsMask::ConstOffsets, 1u},
|
||||
{spv::ImageOperandsMask::Sample, 1u},
|
||||
{spv::ImageOperandsMask::MinLod, 1u},
|
||||
{spv::ImageOperandsMask::MakeTexelAvailable, 1u},
|
||||
{spv::ImageOperandsMask::MakeTexelVisible, 1u},
|
||||
{spv::ImageOperandsMask::NonPrivateTexel, 0u},
|
||||
{spv::ImageOperandsMask::VolatileTexel, 0u},
|
||||
{spv::ImageOperandsMask::SignExtend, 0u},
|
||||
{spv::ImageOperandsMask::ZeroExtend, 0u},
|
||||
{spv::ImageOperandsMask::Nontemporal, 0u},
|
||||
{spv::ImageOperandsMask::Offsets, 1u},
|
||||
};
|
||||
|
||||
// Where each of the operands this pass rewrites sits, for one instruction. An
|
||||
// index of 0 means "not present" - in-operand 0 is always the image, so it can
|
||||
// never be a real position for one of these.
|
||||
struct OperandPositions {
|
||||
uint32_t gradX = 0;
|
||||
uint32_t gradY = 0;
|
||||
uint32_t constOffset = 0;
|
||||
uint32_t offset = 0;
|
||||
// A bit this pass does not know how to widen appeared on a covered image.
|
||||
bool unsupported = false;
|
||||
|
||||
bool Any() const { return gradX != 0 || constOffset != 0 || offset != 0; }
|
||||
};
|
||||
|
||||
OperandPositions LocateOperands(const Instruction& instruction,
|
||||
uint32_t imageOperandsIndex) {
|
||||
OperandPositions positions;
|
||||
if (imageOperandsIndex == 0xFFFFFFFFu ||
|
||||
instruction.NumInOperands() <= imageOperandsIndex) {
|
||||
return positions;
|
||||
}
|
||||
const uint32_t mask = instruction.GetSingleWordInOperand(imageOperandsIndex);
|
||||
uint32_t next = imageOperandsIndex + 1u;
|
||||
for (const ImageOperandBit& entry : kImageOperandBits) {
|
||||
if ((mask & static_cast<uint32_t>(entry.bit)) == 0u) continue;
|
||||
switch (entry.bit) {
|
||||
case spv::ImageOperandsMask::Grad:
|
||||
positions.gradX = next;
|
||||
positions.gradY = next + 1u;
|
||||
break;
|
||||
case spv::ImageOperandsMask::ConstOffset:
|
||||
positions.constOffset = next;
|
||||
break;
|
||||
case spv::ImageOperandsMask::Offset:
|
||||
positions.offset = next;
|
||||
break;
|
||||
case spv::ImageOperandsMask::ConstOffsets:
|
||||
case spv::ImageOperandsMask::Offsets:
|
||||
// An array of offsets, only meaningful for gather - which is declined
|
||||
// above. Refuse rather than translate half of it.
|
||||
positions.unsupported = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
next += entry.idCount;
|
||||
}
|
||||
// Every id the mask claimed has to actually be there; a truncated operand
|
||||
// list means the instruction is not the shape this walk assumed.
|
||||
if (next > instruction.NumInOperands()) {
|
||||
positions.unsupported = true;
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
// Whether this instruction so much as mentions a value whose type resolves to a
|
||||
// covered image. Used to make sure nothing reaches these images through an opcode
|
||||
// this pass never considered: the answer decides between rewriting and declining,
|
||||
// never between two different rewrites.
|
||||
template <typename CoveredFn>
|
||||
bool MentionsCoveredImage(IRContext* context, const Instruction& instruction,
|
||||
const CoveredFn& covered) {
|
||||
bool mentions = false;
|
||||
instruction.ForEachInId([&](const uint32_t* id) {
|
||||
if (mentions || id == nullptr) return;
|
||||
if (covered(ResolveImageType(context, *id))) mentions = true;
|
||||
});
|
||||
return mentions;
|
||||
}
|
||||
|
||||
// The component type of a value, and how many of them it has. A scalar reports a
|
||||
// count of 1; anything that is neither an int/float scalar nor a vector of one
|
||||
// reports 0, which every caller treats as "not a shape this pass translates".
|
||||
struct ValueShape {
|
||||
const analysis::Type* componentType = nullptr;
|
||||
uint32_t componentCount = 0;
|
||||
bool IsScalar() const { return componentCount == 1u; }
|
||||
};
|
||||
|
||||
ValueShape DescribeValue(IRContext* context, uint32_t valueId) {
|
||||
ValueShape shape;
|
||||
Instruction* def = context->get_def_use_mgr()->GetDef(valueId);
|
||||
if (def == nullptr) return shape;
|
||||
const analysis::Type* type = context->get_type_mgr()->GetType(def->type_id());
|
||||
if (type == nullptr) return shape;
|
||||
const analysis::Vector* asVector = type->AsVector();
|
||||
const analysis::Type* component =
|
||||
asVector != nullptr ? asVector->element_type() : type;
|
||||
if (component == nullptr) return shape;
|
||||
if (component->AsInteger() == nullptr && component->AsFloat() == nullptr) {
|
||||
return shape;
|
||||
}
|
||||
shape.componentType = component;
|
||||
shape.componentCount = asVector != nullptr ? asVector->element_count() : 1u;
|
||||
return shape;
|
||||
}
|
||||
|
||||
// Which 1D sampled images this module is to be rewritten for, decided per
|
||||
// arrayed-ness because that is the granularity of the OpTypeImage declarations
|
||||
// glslang emits. A category is in scope only when the module actually performs a
|
||||
// lookup on it carrying an Offset, ConstOffset or Grad - the operands SPIRV-Cross
|
||||
// prints with the wrong arity - so a shader that only samples and fetches keeps
|
||||
// SPIRV-Cross's own correct emission untouched.
|
||||
struct LoweringScope {
|
||||
bool arrayed = false;
|
||||
bool nonArrayed = false;
|
||||
|
||||
bool Any() const { return arrayed || nonArrayed; }
|
||||
bool Covers(const Instruction* imageType) const {
|
||||
return (arrayed && Is1DSampledImageTypeOfArrayedness(imageType, true)) ||
|
||||
(nonArrayed && Is1DSampledImageTypeOfArrayedness(imageType, false));
|
||||
}
|
||||
};
|
||||
|
||||
LoweringScope ResolveLoweringScope(IRContext* context) {
|
||||
LoweringScope scope;
|
||||
// The type table settles the common case, and it is nearly every shader: no
|
||||
// 1D sampled image declared at all, so the code is never walked.
|
||||
bool declared = false;
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DSampledImageType(&type)) {
|
||||
declared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!declared) return scope;
|
||||
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
const OpClassification classification =
|
||||
ClassifyOpcode(instruction.opcode());
|
||||
if (classification.kind != OpKind::Texel ||
|
||||
instruction.NumInOperands() <= classification.coordinateOperand) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(context, instruction.GetSingleWordInOperand(0));
|
||||
if (!Is1DSampledImageType(imageType)) continue;
|
||||
const OperandPositions positions =
|
||||
LocateOperands(instruction, classification.imageOperandsIndex);
|
||||
if (!positions.Any()) continue;
|
||||
if (imageType->GetSingleWordInOperand(kArrayedOperand) == 1u) {
|
||||
scope.arrayed = true;
|
||||
} else {
|
||||
scope.nonArrayed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
// Everything this pass will touch, collected before a single word is changed.
|
||||
// Planning first is what lets every refusal be a clean "leave the module alone":
|
||||
// there is no point at which the module is half converted and the pass then
|
||||
// discovers it cannot finish.
|
||||
struct RewritePlan {
|
||||
struct Site {
|
||||
Instruction* instruction = nullptr;
|
||||
uint32_t coordinateOperand = 0;
|
||||
OperandPositions operands;
|
||||
};
|
||||
std::vector<Site> sites;
|
||||
bool declined = false;
|
||||
};
|
||||
|
||||
RewritePlan PlanRewrite(IRContext* context, const LoweringScope& scope) {
|
||||
RewritePlan plan;
|
||||
const auto covered = [&scope](const Instruction* type) {
|
||||
return scope.Covers(type);
|
||||
};
|
||||
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
const OpClassification classification =
|
||||
ClassifyOpcode(instruction.opcode());
|
||||
|
||||
if (classification.kind == OpKind::NotImageOp ||
|
||||
classification.kind == OpKind::DimIndependentQuery) {
|
||||
// These name no coordinate, so they need no rewrite - but an
|
||||
// opcode this pass has never classified must not reach one of
|
||||
// these images unnoticed. NotImageOp is the catch-all, so the
|
||||
// check is on it.
|
||||
if (classification.kind == OpKind::NotImageOp &&
|
||||
instruction.opcode() != spv::Op::OpLoad &&
|
||||
instruction.opcode() != spv::Op::OpStore &&
|
||||
instruction.opcode() != spv::Op::OpCopyObject &&
|
||||
instruction.opcode() != spv::Op::OpSampledImage &&
|
||||
instruction.opcode() != spv::Op::OpImage &&
|
||||
instruction.opcode() != spv::Op::OpAccessChain &&
|
||||
instruction.opcode() != spv::Op::OpInBoundsAccessChain &&
|
||||
instruction.opcode() != spv::Op::OpPhi &&
|
||||
instruction.opcode() != spv::Op::OpSelect &&
|
||||
instruction.opcode() != spv::Op::OpFunctionCall &&
|
||||
MentionsCoveredImage(context, instruction, covered)) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction.NumInOperands() < 1) continue;
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(context, instruction.GetSingleWordInOperand(0));
|
||||
if (!scope.Covers(imageType)) continue;
|
||||
|
||||
if (classification.kind == OpKind::Decline) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
if (instruction.NumInOperands() <= classification.coordinateOperand) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
const OperandPositions positions =
|
||||
LocateOperands(instruction, classification.imageOperandsIndex);
|
||||
if (positions.unsupported) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Confirm here, before anything is written, that every operand
|
||||
// about to be widened has the shape the widening assumes. The
|
||||
// coordinate may be a scalar or a short vector; the offset and
|
||||
// the two gradients must be SCALARS, which for a Dim1D image is
|
||||
// not an assumption but the validator's own rule
|
||||
// (GetPlaneCoordSize(1D) == 1). Checking it up front is what
|
||||
// keeps the apply phase total.
|
||||
const ValueShape coordinate = DescribeValue(
|
||||
context, instruction.GetSingleWordInOperand(
|
||||
classification.coordinateOperand));
|
||||
if (coordinate.componentCount == 0u || coordinate.componentCount > 3u) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
const uint32_t scalarOperands[] = {positions.gradX, positions.gradY,
|
||||
positions.offset,
|
||||
positions.constOffset};
|
||||
for (const uint32_t position : scalarOperands) {
|
||||
if (position == 0u) continue;
|
||||
if (!DescribeValue(context,
|
||||
instruction.GetSingleWordInOperand(position))
|
||||
.IsScalar()) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
// ConstOffset has to stay a constant expression, so its widened
|
||||
// form is built as a module-scope constant - which is only
|
||||
// possible if the operand really is one.
|
||||
if (positions.constOffset != 0u &&
|
||||
context->get_constant_mgr()->FindDeclaredConstant(
|
||||
instruction.GetSingleWordInOperand(positions.constOffset)) ==
|
||||
nullptr) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.sites.push_back(
|
||||
{&instruction, classification.coordinateOperand, positions});
|
||||
}
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(
|
||||
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;
|
||||
}
|
||||
return ResolveLoweringScope(context.get()).Any();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status Lower1DSampledImagesPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
const LoweringScope scope = ResolveLoweringScope(irContext);
|
||||
if (!scope.Any()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
RewritePlan plan = PlanRewrite(irContext, scope);
|
||||
if (plan.declined) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// A zero of a given 32-bit scalar type. The literal word is the VALUE's bit
|
||||
// pattern, which for a float zero is 0 as well - so one helper serves the integer
|
||||
// coordinate of a fetch, the float coordinate of a sample and the float gradients
|
||||
// alike, without a second spelling to keep in step.
|
||||
const auto zeroOf = [&](const analysis::Type* componentType,
|
||||
uint32_t componentTypeId) -> uint32_t {
|
||||
const analysis::Constant* constant =
|
||||
constantMgr->GetConstant(componentType, {0u});
|
||||
if (constant == nullptr) return 0u;
|
||||
const Instruction* defining =
|
||||
constantMgr->GetDefiningInstruction(constant, componentTypeId);
|
||||
return defining != nullptr ? defining->result_id() : 0u;
|
||||
};
|
||||
|
||||
// The whole of the arity repair, in one place: insert a zero at component 1.
|
||||
// Scalar u becomes (u, 0); (u, layer) becomes (u, 0, layer); (u, q) becomes
|
||||
// (u, 0, q). See the header for why one rule covers every shape.
|
||||
const auto widen = [&](uint32_t valueId, Instruction* before,
|
||||
bool mustBeConstant) -> uint32_t {
|
||||
const ValueShape shape = DescribeValue(irContext, valueId);
|
||||
if (shape.componentCount == 0u) return 0u;
|
||||
|
||||
const uint32_t componentTypeId = typeMgr->GetTypeInstruction(shape.componentType);
|
||||
if (componentTypeId == 0u) return 0u;
|
||||
analysis::Vector widenedCandidate(shape.componentType, shape.componentCount + 1u);
|
||||
const uint32_t widenedTypeId = typeMgr->GetTypeInstruction(&widenedCandidate);
|
||||
const uint32_t zeroId = zeroOf(shape.componentType, componentTypeId);
|
||||
if (widenedTypeId == 0u || zeroId == 0u) return 0u;
|
||||
|
||||
// ConstOffset must remain a constant expression - the validator says so
|
||||
// outright ("Expected Image Operand ConstOffset to be a const object") - so
|
||||
// for it the widened value is built as a module-scope OpConstantComposite
|
||||
// rather than as an instruction in the block. Only the scalar shape is
|
||||
// reachable: the plan phase refuses anything else, because a Dim1D image's
|
||||
// offset has exactly one component by the validator's own arity rule.
|
||||
if (mustBeConstant) {
|
||||
if (!shape.IsScalar()) return 0u;
|
||||
const analysis::Type* widenedType = typeMgr->GetType(widenedTypeId);
|
||||
const analysis::Constant* widenedConstant =
|
||||
widenedType != nullptr
|
||||
? constantMgr->GetConstant(widenedType, {valueId, zeroId})
|
||||
: nullptr;
|
||||
if (widenedConstant == nullptr) return 0u;
|
||||
const Instruction* defining =
|
||||
constantMgr->GetDefiningInstruction(widenedConstant, widenedTypeId);
|
||||
return defining != nullptr ? defining->result_id() : 0u;
|
||||
}
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, before,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
std::vector<uint32_t> componentIds;
|
||||
componentIds.reserve(shape.componentCount + 1u);
|
||||
if (shape.IsScalar()) {
|
||||
componentIds.push_back(valueId);
|
||||
componentIds.push_back(zeroId);
|
||||
} else {
|
||||
for (uint32_t i = 0; i < shape.componentCount; ++i) {
|
||||
Instruction* extracted =
|
||||
builder.AddCompositeExtract(componentTypeId, valueId, {i});
|
||||
if (extracted == nullptr) return 0u;
|
||||
componentIds.push_back(extracted->result_id());
|
||||
if (i == 0u) componentIds.push_back(zeroId);
|
||||
}
|
||||
}
|
||||
Instruction* widened =
|
||||
builder.AddCompositeConstruct(widenedTypeId, componentIds);
|
||||
return widened != nullptr ? widened->result_id() : 0u;
|
||||
};
|
||||
|
||||
for (RewritePlan::Site& site : plan.sites) {
|
||||
Instruction* instruction = site.instruction;
|
||||
|
||||
struct Target {
|
||||
uint32_t position;
|
||||
bool mustBeConstant;
|
||||
};
|
||||
const Target targets[] = {
|
||||
{site.coordinateOperand, false},
|
||||
{site.operands.gradX, false},
|
||||
{site.operands.gradY, false},
|
||||
{site.operands.offset, false},
|
||||
{site.operands.constOffset, true},
|
||||
};
|
||||
for (const Target& target : targets) {
|
||||
// Position 0 is the image operand, so it is this plan's "absent" marker
|
||||
// for everything except the coordinate, which is never 0.
|
||||
if (target.position == 0u) continue;
|
||||
const uint32_t widenedId =
|
||||
widen(instruction->GetSingleWordInOperand(target.position), instruction,
|
||||
target.mustBeConstant);
|
||||
if (widenedId == 0u) {
|
||||
// Reachable only if the module's shapes disagree with what the plan
|
||||
// recorded. Failing here makes the caller keep the input binary,
|
||||
// which is the same outcome as a decline.
|
||||
return Status::Failure;
|
||||
}
|
||||
instruction->SetInOperand(target.position, {widenedId});
|
||||
}
|
||||
irContext->UpdateDefUse(instruction);
|
||||
}
|
||||
|
||||
// Only now, with no lookup still spelling a 1D coordinate, does the type become
|
||||
// the 2D one - which is what ES stores a GL_TEXTURE_1D(_ARRAY) as anyway
|
||||
// (MapToBackendTextureTarget), and what SPIRV-Cross was already PRINTING for it.
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (scope.Covers(&type)) {
|
||||
type.SetInOperand(kDimOperand, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
|
||||
// Sampled1D describes the types just rewritten. Drop it only if no 1D SAMPLED
|
||||
// image is left at all - a module may still hold one this pass left alone (a
|
||||
// category with no offset or gradient on it), and that one still needs the
|
||||
// capability. Image1D is deliberately untouched: it belongs to the storage images
|
||||
// Lower1DArrayImagesPass owns, and they may still be Dim1D here. Shader is
|
||||
// declared by any module reaching this point, so restating it keeps the
|
||||
// instruction valid and RemoveDuplicates collapses the pair.
|
||||
if (!AnyDim1DSampledTypeLeft(irContext)) {
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
const auto value =
|
||||
static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (value == spv::Capability::Sampled1D) {
|
||||
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken Lower1DSampledImagesPass::CreateLower1DSampledImagesPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<Lower1DSampledImagesPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,116 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.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 {
|
||||
// The SAMPLED-image half of the 1D story. Lower1DArrayImagesPass owns the storage
|
||||
// half and says there, correctly for what it needed, that SPIRV-Cross's SAMPLER path
|
||||
// "already handles the 1D-array shape correctly and must be left to it". That is true
|
||||
// of the COORDINATE and false of everything else the lookup carries.
|
||||
//
|
||||
// ES has no 1D texture, so SPIRV-Cross emits a 1D sampler as a 2D one - `case Dim1D:
|
||||
// res += options.es ? "2D" : "1D"` - and fakes the missing coordinate component at
|
||||
// each call site (spirv_glsl.cpp, the `imgtype.image.dim == Dim1D && options.es`
|
||||
// branches: `vec2(coord, 0.0)` non-arrayed, `vec3(coord.x, 0.0, coord.y)` arrayed,
|
||||
// which is the same (u, 0, layer) the 2D-array texture actually stores). But the
|
||||
// OFFSET operand and the two GRADIENT operands are printed straight through with
|
||||
// their original 1D arity:
|
||||
//
|
||||
// if (args.offset) { ...; farg_str += bitcast_expression(SPIRType::Int, args.offset); }
|
||||
// if (args.grad_x || args.grad_y) { ...; farg_str += to_expression(args.grad_x); ... }
|
||||
//
|
||||
// So a `textureLodOffset(sampler1DArray, vec2, float, int)` comes out as
|
||||
// `textureLodOffset(sampler2DArray, vec3, float, int)`, for which ESSL has no
|
||||
// overload, and the driver answers "'textureLodOffset' : no matching overloaded
|
||||
// function found". That loses the stage, and with it the program - which is how ONE
|
||||
// sampler1DArray lookup took down the nine-sampler compute shader of
|
||||
// KHR-GL43.compute_shader.resource-texture, whose dispatch then silently did nothing
|
||||
// and left the SSBO reading back the zeros the test uploaded.
|
||||
//
|
||||
// Observed failing on an Adreno 830 by isolating each form: textureOffset,
|
||||
// textureLodOffset and texelFetchOffset on both sampler1D and sampler1DArray, and
|
||||
// textureGrad on sampler1DArray. The same shaders with a 2D sampler compile, so the
|
||||
// functions exist - only the argument arity is wrong.
|
||||
//
|
||||
// WHY NOT PATCH SPIRV-CROSS. 3rdparty/SPIRV-Cross is a submodule pinned to KhronosGroup
|
||||
// upstream, not to a MobileGL fork (contrast 3rdparty/glslang), so an in-tree edit
|
||||
// would live outside this repository's history.
|
||||
//
|
||||
// WHY NOT WIDEN JUST THE OPERANDS. Emitting an ivec2 offset against a type still
|
||||
// declared Dim1D is an INVALID module, not a clever shortcut: the validator computes
|
||||
// the required arity from the image's own Dim (validate_image.cpp, GetPlaneCoordSize
|
||||
// -> "Expected Image Operand Offset to have 1 component") and would latch a failure on
|
||||
// every validating lane. So the type has to move too, and once it does the coordinate
|
||||
// has to move with it - which is what this pass does, in the module, before
|
||||
// SPIRV-Cross ever applies its own emulation.
|
||||
//
|
||||
// The rewrite is exactly SPIRV-Cross's own, restated on the SPIR-V side so that
|
||||
// coordinate, offset and gradient are all widened by one piece of code: a zero is
|
||||
// INSERTED AT COMPONENT 1 of each. That single rule is right for every shape, because
|
||||
// a 1D coordinate lays out as [u][array layer][proj q] and the plane occupies index 0
|
||||
// alone - so (u) -> (u, 0), (u, layer) -> (u, 0, layer) and (u, q) -> (u, 0, q) all
|
||||
// fall out of it, and so do the scalar offset -> ivec2 and the scalar gradients ->
|
||||
// vec2. The Dref value is a separate SPIR-V operand rather than a coordinate
|
||||
// component, so the shadow forms need nothing extra.
|
||||
//
|
||||
// NO CROSS-STAGE HAZARD, and this is the one place this pass is on firmer ground than
|
||||
// its storage-image sibling, whose header records the opposite as a known limitation.
|
||||
// That pass can rewrite uimage1DArray to uimage2DArray in one stage and decline in
|
||||
// another, and the two then spell the SAME uniform `uimage2D` and `uimage2DArray` and
|
||||
// the ES link fails on a type mismatch. Here the two spellings COINCIDE: SPIRV-Cross
|
||||
// prints Dim1D as "2D" on ES already, so a stage this pass rewrote and a stage it left
|
||||
// alone both declare `sampler2D` / `sampler2DArray`. Partial application across a
|
||||
// program's stages is therefore invisible at the interface.
|
||||
//
|
||||
// Deliberately narrow, on three axes - the sibling's reasoning, applied to this
|
||||
// resource:
|
||||
//
|
||||
// * SAMPLED images only (Sampled == 1). Storage images are the sibling's.
|
||||
// * Only when the module actually carries an Offset, ConstOffset or Grad operand on
|
||||
// a 1D sampled image, i.e. only where SPIRV-Cross's emission is ALREADY broken.
|
||||
// A shader that only calls texture()/textureLod()/texelFetch() on a sampler1D
|
||||
// keeps taking SPIRV-Cross's own (correct) output byte for byte, so this pass has
|
||||
// no way to regress it. The gate is decided per arrayed-ness, matching the two
|
||||
// distinct OpTypeImage declarations glslang emits.
|
||||
// * ESSL only. Vulkan has VK_IMAGE_VIEW_TYPE_1D natively and the offset and gradient
|
||||
// arities are the ones the module already spells, so DirectVulkan must see the
|
||||
// module unchanged.
|
||||
//
|
||||
// A size query on a covered image is DECLINED rather than half-translated, for the
|
||||
// sibling's reason: textureSize(sampler1D) yields an int and textureSize(sampler2D) an
|
||||
// ivec2, so rewriting the type while leaving the query would hand the shader a value of
|
||||
// the wrong shape. Refusing leaves the module byte for byte and is no worse than today.
|
||||
//
|
||||
// Every decline is decided BEFORE anything is rewritten - the pass plans the whole
|
||||
// edit, and only then applies it - so there is no state in which it has half-converted
|
||||
// a module and then given up. Anything it does not recognise reaching one of these
|
||||
// images (a gather, an unexpected image opcode) is a decline, not a guess.
|
||||
class Lower1DSampledImagesPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-lower-1d-sampled-images"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether a module carries the shape this pass exists for: a 1D SAMPLED image
|
||||
// reached by a lookup with an Offset, ConstOffset or Grad operand. One parse
|
||||
// answers it, and the answer is no for very nearly every shader - the common path
|
||||
// must not build an Optimizer at all.
|
||||
static bool BinaryHasOffsetOrGrad1DSampledImage(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLower1DSampledImagesPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,563 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.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 "WidenImageFormatsPass.h"
|
||||
|
||||
// For IsSpirvCrossEsslPrintableFormat: the two passes share one question about the emitter, and
|
||||
// the answer belongs where the rest of the image-format tables already are.
|
||||
#include "BakeImageFormatsPass.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_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 <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
|
||||
// 5 Sampled, 6 Format.
|
||||
constexpr uint32_t kImageSampledTypeOperand = 0;
|
||||
constexpr uint32_t kImageSampledOperand = 5;
|
||||
constexpr uint32_t kImageFormatOperand = 6;
|
||||
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
|
||||
// sampler. The only kind that carries a format qualifier in any GLSL dialect.
|
||||
constexpr uint32_t kSampledStorageImage = 2;
|
||||
|
||||
// OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands.
|
||||
// OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional.
|
||||
constexpr uint32_t kImageAccessImageOperand = 0;
|
||||
constexpr uint32_t kImageWriteTexelOperand = 2;
|
||||
|
||||
// The exact carrier of a non-core image format: the core GLSL ES format with the
|
||||
// SAME component type and the SAME per-channel width, differing only in channel
|
||||
// count. `channels` is what the original format really has, which is what every
|
||||
// access through the carrier is masked back to.
|
||||
//
|
||||
// Only formats that widen EXACTLY appear here. r11f_g11f_b10f, rgb10_a2,
|
||||
// rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm and r16_snorm have no
|
||||
// same-width core carrier - every candidate is either lossy or changes the numeric
|
||||
// domain a sampler would read - and are deliberately absent, so they keep the
|
||||
// honest "no GLSL ES spelling" diagnostic rather than a silent approximation.
|
||||
struct ImageFormatWidening {
|
||||
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
|
||||
uint32_t Channels = 0;
|
||||
|
||||
explicit operator bool() const { return Carrier != spv::ImageFormat::Unknown; }
|
||||
};
|
||||
|
||||
ImageFormatWidening WideningOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
// Float.
|
||||
case spv::ImageFormat::Rg32f: return {spv::ImageFormat::Rgba32f, 2};
|
||||
case spv::ImageFormat::Rg16f: return {spv::ImageFormat::Rgba16f, 2};
|
||||
case spv::ImageFormat::R16f: return {spv::ImageFormat::Rgba16f, 1};
|
||||
// Unsigned normalized.
|
||||
case spv::ImageFormat::Rg8: return {spv::ImageFormat::Rgba8, 2};
|
||||
case spv::ImageFormat::R8: return {spv::ImageFormat::Rgba8, 1};
|
||||
// Signed normalized.
|
||||
case spv::ImageFormat::Rg8Snorm: return {spv::ImageFormat::Rgba8Snorm, 2};
|
||||
case spv::ImageFormat::R8Snorm: return {spv::ImageFormat::Rgba8Snorm, 1};
|
||||
// Signed integer.
|
||||
case spv::ImageFormat::Rg32i: return {spv::ImageFormat::Rgba32i, 2};
|
||||
case spv::ImageFormat::Rg16i: return {spv::ImageFormat::Rgba16i, 2};
|
||||
case spv::ImageFormat::R16i: return {spv::ImageFormat::Rgba16i, 1};
|
||||
case spv::ImageFormat::Rg8i: return {spv::ImageFormat::Rgba8i, 2};
|
||||
case spv::ImageFormat::R8i: return {spv::ImageFormat::Rgba8i, 1};
|
||||
// Unsigned integer.
|
||||
case spv::ImageFormat::Rg32ui: return {spv::ImageFormat::Rgba32ui, 2};
|
||||
case spv::ImageFormat::Rg16ui: return {spv::ImageFormat::Rgba16ui, 2};
|
||||
case spv::ImageFormat::R16ui: return {spv::ImageFormat::Rgba16ui, 1};
|
||||
case spv::ImageFormat::Rg8ui: return {spv::ImageFormat::Rgba8ui, 2};
|
||||
case spv::ImageFormat::R8ui: return {spv::ImageFormat::Rgba8ui, 1};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// The GL 4.2 image format table (core spec table 8.26) as SPIR-V ImageFormats.
|
||||
// Written as literals rather than through the GL headers because this lives in
|
||||
// MG_Util, which the GL frontend's enums do not reach; the same list, in the same
|
||||
// order, as BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat.
|
||||
spv::ImageFormat SpirvImageFormatOfGL(Uint glInternalFormat) {
|
||||
switch (glInternalFormat) {
|
||||
case 0x8814: /*GL_RGBA32F*/ return spv::ImageFormat::Rgba32f;
|
||||
case 0x881A: /*GL_RGBA16F*/ return spv::ImageFormat::Rgba16f;
|
||||
case 0x8230: /*GL_RG32F*/ return spv::ImageFormat::Rg32f;
|
||||
case 0x822F: /*GL_RG16F*/ return spv::ImageFormat::Rg16f;
|
||||
case 0x8C3A: /*GL_R11F_G11F_B10F*/ return spv::ImageFormat::R11fG11fB10f;
|
||||
case 0x822E: /*GL_R32F*/ return spv::ImageFormat::R32f;
|
||||
case 0x822D: /*GL_R16F*/ return spv::ImageFormat::R16f;
|
||||
case 0x8D70: /*GL_RGBA32UI*/ return spv::ImageFormat::Rgba32ui;
|
||||
case 0x8D76: /*GL_RGBA16UI*/ return spv::ImageFormat::Rgba16ui;
|
||||
case 0x8D7C: /*GL_RGBA8UI*/ return spv::ImageFormat::Rgba8ui;
|
||||
case 0x906F: /*GL_RGB10_A2UI*/ return spv::ImageFormat::Rgb10a2ui;
|
||||
case 0x823C: /*GL_RG32UI*/ return spv::ImageFormat::Rg32ui;
|
||||
case 0x823A: /*GL_RG16UI*/ return spv::ImageFormat::Rg16ui;
|
||||
case 0x8238: /*GL_RG8UI*/ return spv::ImageFormat::Rg8ui;
|
||||
case 0x8236: /*GL_R32UI*/ return spv::ImageFormat::R32ui;
|
||||
case 0x8234: /*GL_R16UI*/ return spv::ImageFormat::R16ui;
|
||||
case 0x8232: /*GL_R8UI*/ return spv::ImageFormat::R8ui;
|
||||
case 0x8D82: /*GL_RGBA32I*/ return spv::ImageFormat::Rgba32i;
|
||||
case 0x8D88: /*GL_RGBA16I*/ return spv::ImageFormat::Rgba16i;
|
||||
case 0x8D8E: /*GL_RGBA8I*/ return spv::ImageFormat::Rgba8i;
|
||||
case 0x823B: /*GL_RG32I*/ return spv::ImageFormat::Rg32i;
|
||||
case 0x8239: /*GL_RG16I*/ return spv::ImageFormat::Rg16i;
|
||||
case 0x8237: /*GL_RG8I*/ return spv::ImageFormat::Rg8i;
|
||||
case 0x8235: /*GL_R32I*/ return spv::ImageFormat::R32i;
|
||||
case 0x8233: /*GL_R16I*/ return spv::ImageFormat::R16i;
|
||||
case 0x8231: /*GL_R8I*/ return spv::ImageFormat::R8i;
|
||||
case 0x8058: /*GL_RGBA8*/ return spv::ImageFormat::Rgba8;
|
||||
case 0x805B: /*GL_RGBA16*/ return spv::ImageFormat::Rgba16;
|
||||
case 0x8059: /*GL_RGB10_A2*/ return spv::ImageFormat::Rgb10A2;
|
||||
case 0x822B: /*GL_RG8*/ return spv::ImageFormat::Rg8;
|
||||
case 0x822C: /*GL_RG16*/ return spv::ImageFormat::Rg16;
|
||||
case 0x8229: /*GL_R8*/ return spv::ImageFormat::R8;
|
||||
case 0x822A: /*GL_R16*/ return spv::ImageFormat::R16;
|
||||
case 0x8F97: /*GL_RGBA8_SNORM*/ return spv::ImageFormat::Rgba8Snorm;
|
||||
case 0x8F9B: /*GL_RGBA16_SNORM*/ return spv::ImageFormat::Rgba16Snorm;
|
||||
case 0x8F95: /*GL_RG8_SNORM*/ return spv::ImageFormat::Rg8Snorm;
|
||||
case 0x8F99: /*GL_RG16_SNORM*/ return spv::ImageFormat::Rg16Snorm;
|
||||
case 0x8F94: /*GL_R8_SNORM*/ return spv::ImageFormat::R8Snorm;
|
||||
case 0x8F98: /*GL_R16_SNORM*/ return spv::ImageFormat::R16Snorm;
|
||||
default:
|
||||
return spv::ImageFormat::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Uint GLInternalFormatOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
case spv::ImageFormat::Rgba32f: return 0x8814; // GL_RGBA32F
|
||||
case spv::ImageFormat::Rgba16f: return 0x881A; // GL_RGBA16F
|
||||
case spv::ImageFormat::Rgba8: return 0x8058; // GL_RGBA8
|
||||
case spv::ImageFormat::Rgba8Snorm: return 0x8F97; // GL_RGBA8_SNORM
|
||||
case spv::ImageFormat::Rgba32i: return 0x8D82; // GL_RGBA32I
|
||||
case spv::ImageFormat::Rgba16i: return 0x8D88; // GL_RGBA16I
|
||||
case spv::ImageFormat::Rgba8i: return 0x8D8E; // GL_RGBA8I
|
||||
case spv::ImageFormat::Rgba32ui: return 0x8D70; // GL_RGBA32UI
|
||||
case spv::ImageFormat::Rgba16ui: return 0x8D76; // GL_RGBA16UI
|
||||
case spv::ImageFormat::Rgba8ui: return 0x8D7C; // GL_RGBA8UI
|
||||
default:
|
||||
// Only the carriers need the reverse direction, and every carrier is one
|
||||
// of the four-channel core formats above.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ChannelsOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
case spv::ImageFormat::R32f:
|
||||
case spv::ImageFormat::R16f:
|
||||
case spv::ImageFormat::R16:
|
||||
case spv::ImageFormat::R8:
|
||||
case spv::ImageFormat::R16Snorm:
|
||||
case spv::ImageFormat::R8Snorm:
|
||||
case spv::ImageFormat::R32i:
|
||||
case spv::ImageFormat::R16i:
|
||||
case spv::ImageFormat::R8i:
|
||||
case spv::ImageFormat::R32ui:
|
||||
case spv::ImageFormat::R16ui:
|
||||
case spv::ImageFormat::R8ui:
|
||||
return 1;
|
||||
case spv::ImageFormat::Rg32f:
|
||||
case spv::ImageFormat::Rg16f:
|
||||
case spv::ImageFormat::Rg16:
|
||||
case spv::ImageFormat::Rg8:
|
||||
case spv::ImageFormat::Rg16Snorm:
|
||||
case spv::ImageFormat::Rg8Snorm:
|
||||
case spv::ImageFormat::Rg32i:
|
||||
case spv::ImageFormat::Rg16i:
|
||||
case spv::ImageFormat::Rg8i:
|
||||
case spv::ImageFormat::Rg32ui:
|
||||
case spv::ImageFormat::Rg16ui:
|
||||
case spv::ImageFormat::Rg8ui:
|
||||
return 2;
|
||||
case spv::ImageFormat::R11fG11fB10f:
|
||||
return 3;
|
||||
case spv::ImageFormat::Rgba32f:
|
||||
case spv::ImageFormat::Rgba16f:
|
||||
case spv::ImageFormat::Rgba16:
|
||||
case spv::ImageFormat::Rgb10A2:
|
||||
case spv::ImageFormat::Rgba8:
|
||||
case spv::ImageFormat::Rgba16Snorm:
|
||||
case spv::ImageFormat::Rgba8Snorm:
|
||||
case spv::ImageFormat::Rgba32i:
|
||||
case spv::ImageFormat::Rgba16i:
|
||||
case spv::ImageFormat::Rgba8i:
|
||||
case spv::ImageFormat::Rgba32ui:
|
||||
case spv::ImageFormat::Rgba16ui:
|
||||
case spv::ImageFormat::Rgba8ui:
|
||||
case spv::ImageFormat::Rgb10a2ui:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsWidenableStorageImageType(const Instruction* type,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
|
||||
if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false;
|
||||
const auto format =
|
||||
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
|
||||
if (!WideningOfSpirvImageFormat(format)) return false;
|
||||
if (onlyFormatsSpirvCrossRefusesToPrint &&
|
||||
BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) {
|
||||
// The driver can spell this one and the emitter will print it; widening it
|
||||
// would spend two to four times the texture memory to change nothing.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint WidenImageFormatsPass::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
|
||||
const ImageFormatWidening widening =
|
||||
WideningOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
|
||||
if (!widening) return 0;
|
||||
return GLInternalFormatOfSpirvImageFormat(widening.Carrier);
|
||||
}
|
||||
|
||||
Uint WidenImageFormatsPass::ImageFormatChannelCount(Uint glInternalFormat) {
|
||||
return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
|
||||
}
|
||||
|
||||
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(
|
||||
IRContext* context, const bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(
|
||||
const Vector<Uint32>& binary, const bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
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());
|
||||
return DeclaresWidenableImageFormat(context.get(), onlyFormatsSpirvCrossRefusesToPrint);
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status WidenImageFormatsPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// Cheap gate first: no widenable image type, and the module is handed back
|
||||
// byte-identical - which is every shader but a handful.
|
||||
std::vector<Instruction*> imageTypes;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (IsWidenableStorageImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
|
||||
imageTypes.push_back(&type);
|
||||
}
|
||||
}
|
||||
if (imageTypes.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// What each widenable image type becomes, and the mask its accesses take. Keyed on
|
||||
// the type's result id so the access walk below can ask about an image VALUE by
|
||||
// its type without re-deriving anything.
|
||||
struct WidenedImage {
|
||||
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
|
||||
uint32_t Channels = 0;
|
||||
uint32_t SampledTypeId = 0;
|
||||
};
|
||||
std::map<uint32_t, WidenedImage> widenedByTypeId;
|
||||
for (Instruction* type : imageTypes) {
|
||||
const auto format =
|
||||
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
|
||||
const ImageFormatWidening widening = WideningOfSpirvImageFormat(format);
|
||||
widenedByTypeId.emplace(type->result_id(),
|
||||
WidenedImage{widening.Carrier, widening.Channels,
|
||||
type->GetSingleWordInOperand(kImageSampledTypeOperand)});
|
||||
}
|
||||
|
||||
// Collect the accesses BEFORE anything is mutated, and refuse the whole rewrite if
|
||||
// any of them is a shape this pass cannot mask end to end. A widened declaration
|
||||
// whose accesses were left unmasked is worse than the compile error it replaced:
|
||||
// the shader runs and quietly reads the carrier's surplus channels, which GL says
|
||||
// are 0 and 1. Refusing hands the stage back to the "no GLSL ES spelling"
|
||||
// diagnostic instead, which at least names the failure.
|
||||
std::vector<Instruction*> reads;
|
||||
std::vector<Instruction*> writes;
|
||||
Bool rewritable = true;
|
||||
for (auto funcIt = irContext->module()->begin();
|
||||
funcIt != irContext->module()->end() && rewritable; ++funcIt) {
|
||||
funcIt->ForEachInst([&](Instruction* inst) {
|
||||
if (!rewritable) return;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpImageRead:
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageSparseRead:
|
||||
case spv::Op::OpImageTexelPointer:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// OpImageTexelPointer names the image VARIABLE (a pointer), the other
|
||||
// three an image VALUE; both reach the OpTypeImage through the def's
|
||||
// type, one hop further for the pointer.
|
||||
const Instruction* imageDef =
|
||||
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
||||
if (imageDef == nullptr) return;
|
||||
uint32_t imageTypeId = imageDef->type_id();
|
||||
if (const Instruction* imageType = defUseMgr->GetDef(imageTypeId);
|
||||
imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) {
|
||||
imageTypeId = imageType->GetSingleWordInOperand(1);
|
||||
}
|
||||
const auto widenedIt = widenedByTypeId.find(imageTypeId);
|
||||
if (widenedIt == widenedByTypeId.end()) return;
|
||||
|
||||
if (inst->opcode() == spv::Op::OpImageRead) {
|
||||
reads.push_back(inst);
|
||||
return;
|
||||
}
|
||||
if (inst->opcode() == spv::Op::OpImageWrite) {
|
||||
writes.push_back(inst);
|
||||
return;
|
||||
}
|
||||
// OpImageSparseRead yields a struct rather than a plain texel vector, and
|
||||
// OpImageTexelPointer is an image atomic - which spirv-val already
|
||||
// restricts to r32i/r32ui/r32f, all three of them core formats that never
|
||||
// reach this table. Neither is expressible in the ESSL this backend emits,
|
||||
// so rather than mask a shape that has never been seen, decline.
|
||||
rewritable = false;
|
||||
});
|
||||
}
|
||||
if (!rewritable) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// The four-component (0, .., 0, 1) constant each mask shuffles its surplus
|
||||
// channels out of, one per component type in play. GL defines an imageLoad from a
|
||||
// format with fewer than four channels as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so pinning the carrier's
|
||||
// surplus channels to exactly these values is the whole of the emulation.
|
||||
std::map<uint32_t, uint32_t> zeroOneConstantBySampledType; // sampled type id -> constant id
|
||||
std::map<uint32_t, uint32_t> vec4TypeBySampledType; // sampled type id -> v4 type id
|
||||
auto resolveMaskMaterial = [&](uint32_t sampledTypeId, uint32_t& outConstantId,
|
||||
uint32_t& outVec4TypeId) -> Bool {
|
||||
if (const auto cached = zeroOneConstantBySampledType.find(sampledTypeId);
|
||||
cached != zeroOneConstantBySampledType.end()) {
|
||||
outConstantId = cached->second;
|
||||
outVec4TypeId = vec4TypeBySampledType[sampledTypeId];
|
||||
return outConstantId != 0 && outVec4TypeId != 0;
|
||||
}
|
||||
const Instruction* sampledType = defUseMgr->GetDef(sampledTypeId);
|
||||
if (sampledType == nullptr) return false;
|
||||
|
||||
uint32_t oneWord = 0;
|
||||
std::unique_ptr<analysis::Type> component;
|
||||
if (sampledType->opcode() == spv::Op::OpTypeFloat &&
|
||||
sampledType->GetSingleWordInOperand(0) == 32) {
|
||||
component = spvtools::MakeUnique<analysis::Float>(32);
|
||||
oneWord = 0x3F800000u; // 1.0f
|
||||
} else if (sampledType->opcode() == spv::Op::OpTypeInt &&
|
||||
sampledType->GetSingleWordInOperand(0) == 32) {
|
||||
// OpTypeInt in-operands: 0 width, 1 signedness.
|
||||
component = spvtools::MakeUnique<analysis::Integer>(
|
||||
32, sampledType->GetSingleWordInOperand(1) != 0);
|
||||
oneWord = 1u;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
analysis::Type* componentReg = typeMgr->GetRegisteredType(component.get());
|
||||
if (componentReg == nullptr) return false;
|
||||
const analysis::Constant* zero = constantMgr->GetConstant(componentReg, {0u});
|
||||
const analysis::Constant* one = constantMgr->GetConstant(componentReg, {oneWord});
|
||||
if (zero == nullptr || one == nullptr) return false;
|
||||
const Instruction* zeroInst = constantMgr->GetDefiningInstruction(zero);
|
||||
const Instruction* oneInst = constantMgr->GetDefiningInstruction(one);
|
||||
if (zeroInst == nullptr || oneInst == nullptr) return false;
|
||||
|
||||
analysis::Vector vector(componentReg, 4);
|
||||
const uint32_t vec4TypeId = typeMgr->GetTypeInstruction(&vector);
|
||||
if (vec4TypeId == 0) return false;
|
||||
// Through the id rather than through GetRegisteredType(&vector): the
|
||||
// instruction the line above declared (or found) is the one the constant has
|
||||
// to be typed by, and asking the manager for its type is what guarantees the
|
||||
// two are the same registered object.
|
||||
analysis::Type* vectorReg = typeMgr->GetType(vec4TypeId);
|
||||
if (vectorReg == nullptr) return false;
|
||||
// A vector constant's "literal words" are the IDS of its components
|
||||
// (ConstantManager::CreateConstant -> GetConstantsFromIds).
|
||||
const analysis::Constant* zeroOne = constantMgr->GetConstant(
|
||||
vectorReg, {zeroInst->result_id(), zeroInst->result_id(), zeroInst->result_id(),
|
||||
oneInst->result_id()});
|
||||
if (zeroOne == nullptr) return false;
|
||||
const Instruction* zeroOneInst = constantMgr->GetDefiningInstruction(zeroOne);
|
||||
if (zeroOneInst == nullptr) return false;
|
||||
|
||||
outConstantId = zeroOneInst->result_id();
|
||||
outVec4TypeId = vec4TypeId;
|
||||
zeroOneConstantBySampledType.emplace(sampledTypeId, outConstantId);
|
||||
vec4TypeBySampledType.emplace(sampledTypeId, outVec4TypeId);
|
||||
return true;
|
||||
};
|
||||
|
||||
// OpVectorShuffle selects components 0-3 from the first vector and 4-7 from the
|
||||
// second, so with (0, 0, 0, 1) as the second operand the mask for a `channels`-
|
||||
// channel format is [0 .. channels-1] followed by 4 + i for the rest: the surplus
|
||||
// channels take the constant's 0s and, at index 3, its 1.
|
||||
auto maskComponents = [](uint32_t channels) {
|
||||
std::vector<Operand> components;
|
||||
components.reserve(4);
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
components.push_back(
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {i < channels ? i : 4u + i}});
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
||||
auto widenedOf = [&](const Instruction* inst) -> const WidenedImage* {
|
||||
const Instruction* imageDef =
|
||||
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
||||
if (imageDef == nullptr) return nullptr;
|
||||
const auto it = widenedByTypeId.find(imageDef->type_id());
|
||||
return it == widenedByTypeId.end() ? nullptr : &it->second;
|
||||
};
|
||||
|
||||
// Every constant and vector type the masks will need, declared BEFORE the first
|
||||
// instruction is inserted. The constant and type managers append to the module's
|
||||
// globals and keep their own def-use bookkeeping straight; the shuffles below do
|
||||
// not (this pass invalidates every analysis at the end instead), so doing the two
|
||||
// in the other order would have the managers consult a def-use map that no longer
|
||||
// describes the function bodies.
|
||||
for (const auto& widened : widenedByTypeId) {
|
||||
uint32_t unusedConstantId = 0;
|
||||
uint32_t unusedVec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened.second.SampledTypeId, unusedConstantId, unusedVec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
}
|
||||
|
||||
// Masks first, while every image type still carries its ORIGINAL format: the
|
||||
// rewrite below only touches the format operand, so the accesses' types do not
|
||||
// move and the order is free either way - but doing it first keeps a failed
|
||||
// resolve from leaving a half-widened module behind.
|
||||
for (Instruction* write : writes) {
|
||||
const WidenedImage* widened = widenedOf(write);
|
||||
if (widened == nullptr) continue;
|
||||
uint32_t zeroOneId = 0;
|
||||
uint32_t vec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t texelId = write->GetSingleWordInOperand(kImageWriteTexelOperand);
|
||||
const Instruction* texel = defUseMgr->GetDef(texelId);
|
||||
// SPIR-V allows a scalar texel; GLSL's imageStore always passes a gvec4, and a
|
||||
// shape this has never seen is refused rather than guessed at.
|
||||
if (texel == nullptr || texel->type_id() != vec4TypeId) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t maskedId = irContext->TakeNextId();
|
||||
if (maskedId == 0) return Status::Failure;
|
||||
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
|
||||
for (const Operand& component : maskComponents(widened->Channels)) {
|
||||
shuffleOperands.push_back(component);
|
||||
}
|
||||
write->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands));
|
||||
write->SetInOperand(kImageWriteTexelOperand, {maskedId});
|
||||
}
|
||||
|
||||
for (Instruction* read : reads) {
|
||||
const WidenedImage* widened = widenedOf(read);
|
||||
if (widened == nullptr) continue;
|
||||
uint32_t zeroOneId = 0;
|
||||
uint32_t vec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
if (read->type_id() != vec4TypeId) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
// The ORIGINAL instruction keeps its result id and becomes the shuffle, and a
|
||||
// copy of the read is inserted in front of it under a fresh id. That way every
|
||||
// existing use of the read stays intact without a ReplaceAllUsesWith that
|
||||
// would also rewrite the shuffle's own operand (the idiom
|
||||
// EmulateNoPerspectivePass uses for the same reason).
|
||||
const uint32_t rawReadId = irContext->TakeNextId();
|
||||
if (rawReadId == 0) return Status::Failure;
|
||||
Instruction::OperandList readOperands;
|
||||
for (uint32_t i = 0; i < read->NumInOperands(); ++i) {
|
||||
readOperands.push_back(read->GetInOperand(i));
|
||||
}
|
||||
read->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpImageRead, vec4TypeId, rawReadId, readOperands));
|
||||
read->SetOpcode(spv::Op::OpVectorShuffle);
|
||||
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {rawReadId}},
|
||||
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
|
||||
for (const Operand& component : maskComponents(widened->Channels)) {
|
||||
shuffleOperands.push_back(component);
|
||||
}
|
||||
read->SetInOperands(Move(shuffleOperands));
|
||||
}
|
||||
|
||||
// The declaration itself, last. Only the format operand moves: the carrier has the
|
||||
// same component type as the original by construction, so the OpTypeImage's
|
||||
// Sampled Type still agrees with it (which is what spirv-val checks) and no
|
||||
// pointer, array or access-chain type has to be rebuilt.
|
||||
//
|
||||
// Two image types can COLLIDE here - `layout(rg32f)` and `layout(rgba32f)` in one
|
||||
// module both become Rgba32f - and duplicate non-aggregate type declarations are
|
||||
// invalid SPIR-V. The caller runs spirv-tools' RemoveDuplicates pass immediately
|
||||
// after this one, which joins them (and cascades to the pointer and array types
|
||||
// that named them) rather than this pass carrying its own join.
|
||||
for (Instruction* type : imageTypes) {
|
||||
const auto widenedIt = widenedByTypeId.find(type->result_id());
|
||||
if (widenedIt == widenedByTypeId.end()) continue;
|
||||
// No def-use re-analysis: the Image Format operand is a LITERAL, so no use of
|
||||
// any id moves, and the masks above already left the manager describing a
|
||||
// module that has since grown instructions it was never told about. Every
|
||||
// analysis is dropped below instead.
|
||||
type->SetInOperand(kImageFormatOperand, {static_cast<uint32_t>(widenedIt->second.Carrier)});
|
||||
}
|
||||
|
||||
// StorageImageExtendedFormats is deliberately left declared even though every
|
||||
// remaining format is now one of the thirteen that need no capability: a
|
||||
// capability a module no longer exercises is valid SPIR-V, and dropping one is
|
||||
// only safe after proving no extended format is left ANYWHERE, including in image
|
||||
// types this pass declined.
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken WidenImageFormatsPass::CreateWidenImageFormatsPass(
|
||||
const bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<WidenImageFormatsPass>(onlyFormatsSpirvCrossRefusesToPrint));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,130 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.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 {
|
||||
// Emulates the desktop-GL image formats GLSL ES cannot spell by CHANNEL WIDENING: a
|
||||
// storage image DECLARED `layout(rg32f)` is re-declared `layout(rgba32f)` and every
|
||||
// access through it is masked back to the two channels GL says it has.
|
||||
//
|
||||
// WHY IT IS NEEDED AT ALL. GL 4.2 has forty image formats; GLSL ES 3.1 has thirteen,
|
||||
// and GL_NV_image_formats - the only extension that adds the rest - is advertised by
|
||||
// none of Adreno 830, Mali-G1-Ultra MC12 or Mali-G925-Immortalis MC12 (probed on all
|
||||
// three, with `#extension ... : enable` also rejected, so "the driver implements it
|
||||
// unadvertised" is refuted rather than assumed). A shader that declares one of the
|
||||
// other twenty-six therefore has NO legal ESSL spelling, and it fails in one of two
|
||||
// ways: SPIRV-Cross throws for its is_desktop_only_format set and no text is produced
|
||||
// at all, or the token reaches the driver and is rejected ("'rg32f' : not a legal
|
||||
// layout qualifier id"). Either way the stage is lost, the backend program is
|
||||
// unusable, and every draw with it silently renders nothing while the frontend keeps
|
||||
// reporting GL_LINK_STATUS = TRUE. Dropping the qualifier instead is not an escape:
|
||||
// all three drivers reject a format-LESS image declaration outright ("all images have
|
||||
// to define layout format" / "S0001: Image must specify a format layout qualifier"),
|
||||
// readonly and writeonly alike, at both #version 310 es and 320 es. And unlike a
|
||||
// numeric limit there is nothing honest to report either - GL has no "this image
|
||||
// format is unsupported" query - so the format has to be emulated.
|
||||
//
|
||||
// WHAT WIDENING MEANS. Seventeen of the twenty-six have a core ESSL format of the
|
||||
// SAME PER-CHANNEL WIDTH AND COMPONENT TYPE, differing only in channel count
|
||||
// (rg32f -> rgba32f, r8ui -> rgba8ui, rg8_snorm -> rgba8_snorm, ...). Carried in one
|
||||
// of those the emulation is EXACT, not approximate: every value is representable bit
|
||||
// for bit, and GL's own image semantics do the rest -
|
||||
//
|
||||
// * imageLoad on a format with fewer than four channels returns (r, 0, 0, 1);
|
||||
// * imageStore drops the components the format does not have.
|
||||
//
|
||||
// so the two surplus channels of the carrier are not free storage, they are values GL
|
||||
// already defines. This pass pins them: every OpImageWrite through a widened image has
|
||||
// its texel replaced by (r[, g[, b]], 0.., 1) and every OpImageRead has its result
|
||||
// masked the same way. Masking BOTH is deliberate belt and braces - the write mask
|
||||
// alone keeps the storage canonical for a sampler and for glGetTexImage, the read mask
|
||||
// alone survives storage this shader never wrote (glTexStorage with no upload, whose
|
||||
// surplus channels are undefined).
|
||||
//
|
||||
// The other NINE (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16, r16,
|
||||
// rgba16_snorm, rg16_snorm, r16_snorm) have NO same-width core carrier and are
|
||||
// deliberately NOT widened here: every carrier for them is either lossy or changes the
|
||||
// numeric domain of the texture a `sampler2D` would read from it. They keep the honest
|
||||
// "no GLSL ES spelling" diagnostic instead of silently changing an application's
|
||||
// quantisation behaviour.
|
||||
//
|
||||
// MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the
|
||||
// ES texture behind the image has to be allocated in the carrier format too, and
|
||||
// glBindImageTexture has to be handed the carrier (on Adreno the bind of the narrow
|
||||
// format is GL_INVALID_VALUE for nineteen of the twenty-six, and on both Malis for
|
||||
// twenty-five). Both are done in DirectGLES against the same table below, so the two
|
||||
// sides agree by construction rather than by convention. Binding a narrow texture
|
||||
// through a wide image is NOT an option: every tested driver accepts it silently, so
|
||||
// it reads and writes out of bounds undetected.
|
||||
//
|
||||
// ESSL ONLY. DirectVulkan takes the declared format natively and resolves the view
|
||||
// format from the same bind state, so the module must reach it unchanged.
|
||||
class WidenImageFormatsPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
// `onlyFormatsSpirvCrossRefusesToPrint` narrows the pass to the formats that have
|
||||
// no ESSL route even on a driver that DOES advertise GL_NV_image_formats.
|
||||
// SPIRV-Cross's is_desktop_only_format set - r8ui, rg16f, r16i and fifteen others -
|
||||
// makes it THROW for an ESSL target rather than print a token, and the throw takes
|
||||
// the stage with it whatever the driver could have accepted. Mesa is exactly that
|
||||
// case: it advertises the extension, so nothing else needs widening there, and
|
||||
// `layout(r8ui) uimage2D` still lost its whole program until this ran for it.
|
||||
//
|
||||
// Off, the pass widens every format in the table, which is what a driver without
|
||||
// the extension needs. The caller sets it from
|
||||
// g_GLESCapabilities.SupportsExtendedImageFormats, and the SAME rule decides
|
||||
// whether the ES texture storage and the glBindImageTexture argument widen
|
||||
// (TextureImpl::GetImageBindableStorageWidening) - all three have to agree or the
|
||||
// shader addresses a texel size the storage does not have.
|
||||
explicit WidenImageFormatsPass(bool onlyFormatsSpirvCrossRefusesToPrint = false)
|
||||
: m_onlyFormatsSpirvCrossRefusesToPrint(onlyFormatsSpirvCrossRefusesToPrint) {}
|
||||
|
||||
const char* name() const override { return "mobilegl-widen-image-formats"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether the module declares a storage image whose format this pass would widen,
|
||||
// i.e. whether running it could change anything. Answered from a single parse so
|
||||
// the caller can skip the optimizer run entirely - which is every shader but a
|
||||
// handful. `onlyFormatsSpirvCrossRefusesToPrint` must match what the run will use,
|
||||
// or the gate answers a question the pass is not being asked.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
// The same question asked of a module the caller has ALREADY parsed, so a stage
|
||||
// that has to answer several gate questions pays one BuildModule rather than one
|
||||
// per gate - see ShaderCompiler::ProbeSpirvGateFeatures, and the ~10% it cost
|
||||
// compile-heavy CTS cases when two gates each parsed for themselves.
|
||||
static bool DeclaresWidenableImageFormat(spvtools::opt::IRContext* context,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
|
||||
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
|
||||
// when the format needs no widening (it is core already) or cannot be widened
|
||||
// exactly (the nine above, and anything that is not an image format at all).
|
||||
// Used by DirectGLES for the texture storage and the glBindImageTexture argument,
|
||||
// so that all three layers pick the same carrier.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
|
||||
// Channels the GL internal format really has (1-4), or 0 when it is not one of the
|
||||
// forty image formats. The count the widened accesses are masked back to.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
|
||||
private:
|
||||
bool m_onlyFormatsSpirvCrossRefusesToPrint = false;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -162,6 +162,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
|
||||
builder.Value(inputs.maxColorTextureSamples);
|
||||
builder.Value(inputs.maxIntegerSamples);
|
||||
builder.Value(inputs.maxDepthTextureSamples);
|
||||
|
||||
@@ -566,9 +566,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
//
|
||||
// Unconditional passes take no input but the module and so need no key material:
|
||||
// StripUboMemberRelaxedPrecision, LowerRectImages, Lower1DArrayImages,
|
||||
// LegalizeStorageBlockArrayIndexing and FlattenAtomicCounterBlockOffsets. Each self-gates
|
||||
// on the module's own content and is armed by nothing, so the SPIR-V already in this key
|
||||
// covers them completely.
|
||||
// Lower1DSampledImages, LegalizeResourceArrayIndexing and
|
||||
// FlattenAtomicCounterBlockOffsets. Each self-gates on the module's own content and is
|
||||
// armed by nothing, so the SPIR-V already in this key covers them completely.
|
||||
//
|
||||
// THE TEST FOR THAT CLAIM IS NOT THE SIGNATURE. LowerViewportIndexForEssl is equally
|
||||
// module-only to look at, yet SupportsViewportArray is in this key because that bit ARMS
|
||||
@@ -602,6 +602,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// --- driver capability bits that arm or steer a pass ---
|
||||
Bool supportsViewportArray = false;
|
||||
Bool supportsNoperspectiveInterpolation = false;
|
||||
// GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage
|
||||
// image whose format GLSL ES core cannot spell in the core format that carries it and
|
||||
// masks its accesses back - so a driver that HAS the extension and one that does not get
|
||||
// materially different ESSL from the same module.
|
||||
Bool supportsExtendedImageFormats = false;
|
||||
Int32 maxColorTextureSamples = 0;
|
||||
Int32 maxIntegerSamples = 0;
|
||||
Int32 maxDepthTextureSamples = 0;
|
||||
|
||||
Reference in New Issue
Block a user