mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
[Fix, Test] (DirectGLES, ShaderTranspiler): emulate the 17 exactly-carriable non-core image formats by channel widening
This commit is contained in:
@@ -294,6 +294,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,45 @@ 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). Armed only where there is no
|
||||
// GL_NV_image_formats to spell the format natively - a driver that has the extension
|
||||
// keeps the declaration and gets the `#extension` directive instead.
|
||||
Bool ImageFormatWillBeWidened(Uint glInternalFormat) {
|
||||
return !g_GLESCapabilities.SupportsExtendedImageFormats && glInternalFormat != 0 &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// What the format bake needs from the frontend, collected in one walk of the uniform
|
||||
@@ -5074,17 +5220,24 @@ 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.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 +5255,31 @@ 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.",
|
||||
name.c_str(), unit, boundFormat);
|
||||
recordUnspellableFormat(
|
||||
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
|
||||
continue;
|
||||
// Outside the GLSL ES core set and with no GL_NV_image_formats to spell
|
||||
// it. If the format widens exactly it is still baked HERE, narrow, and
|
||||
// WidenImageFormatsForEssl re-declares it in its core carrier immediately
|
||||
// afterwards - both routes into that pass end in the same place.
|
||||
//
|
||||
// If it does not widen 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.
|
||||
if (!ImageFormatWillBeWidened(boundFormat)) {
|
||||
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 +5311,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));
|
||||
@@ -5197,8 +5368,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
|
||||
@@ -5445,6 +5616,38 @@ 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.
|
||||
//
|
||||
// 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 ARMS this -
|
||||
// see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes no other
|
||||
// input: what it rewrites is a pure function of the module's own declared formats,
|
||||
// and the module is already the largest thing in the L2 key.
|
||||
//
|
||||
// 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 (!g_GLESCapabilities.SupportsExtendedImageFormats &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresWidenableImageFormat(*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl(
|
||||
*effectiveSpirv, widenedImageFormatSpirv, enableSpirvValidation) &&
|
||||
!widenedImageFormatSpirv.empty()) {
|
||||
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
|
||||
@@ -5781,6 +5984,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;
|
||||
|
||||
@@ -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,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) {
|
||||
// Same arming as WidenImageFormatsForEssl. A driver with GL_NV_image_formats spells
|
||||
// the narrow format natively, and widening the storage behind a shader that still
|
||||
// says `rg32f` would make the two disagree about the texel size.
|
||||
if (g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
return {};
|
||||
}
|
||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const auto carrier = static_cast<GLenum>(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested));
|
||||
if (carrier == 0) {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,7 @@ add_executable(
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeStorageBlockArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
WidenImageFormatsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -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,397 @@
|
||||
// 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/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;
|
||||
}
|
||||
|
||||
// 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, /*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, /*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}));
|
||||
}
|
||||
|
||||
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, /*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, /*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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
#include "SpirvPasses/WidenImageFormatsPass.h"
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -771,6 +772,35 @@ namespace MobileGL {
|
||||
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
|
||||
}
|
||||
|
||||
bool ShaderCompiler::WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(WidenImageFormatsPass::CreateWidenImageFormatsPass());
|
||||
// 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) {
|
||||
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary);
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -231,6 +231,31 @@ 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.
|
||||
static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares a storage image WidenImageFormatsForEssl would
|
||||
// widen. One module parse, so the ~every shader that declares none pays no
|
||||
// optimizer run.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary);
|
||||
// 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
// 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"
|
||||
|
||||
#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) {
|
||||
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));
|
||||
return static_cast<Bool>(WideningOfSpirvImageFormat(format));
|
||||
}
|
||||
} // 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(const Vector<Uint32>& binary) {
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (IsWidenableStorageImageType(&type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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)) {
|
||||
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;
|
||||
type->SetInOperand(kImageFormatOperand, {static_cast<uint32_t>(widenedIt->second.Carrier)});
|
||||
defUseMgr->AnalyzeInstUse(type);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<WidenImageFormatsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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:
|
||||
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.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary);
|
||||
|
||||
// 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();
|
||||
};
|
||||
} // 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);
|
||||
|
||||
@@ -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