mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Merge] (DirectGLES): take the image-widening repairs under the viewport routing
This commit is contained in:
@@ -1482,8 +1482,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
//
|
||||
// A BUFFER texture is excluded on both sides: it has no storage of its own to widen
|
||||
// (its texels are the application's buffer object), so WidenImageFormatsPass declines
|
||||
// every buffer image and the bind must decline with it, or the driver would be handed
|
||||
// a carrier the shader never addressed. See the Dim::Buffer guard there for the
|
||||
// 32-byte GL_RG32F measurement that pinned it.
|
||||
GLenum bindFormat = imageBinding.Format;
|
||||
if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
||||
if (imageBinding.Texture->GetTarget() != TextureTarget::TextureBuffer &&
|
||||
TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
||||
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||
if (boundFormatWidening) {
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
@@ -2836,21 +2838,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
widenedData, IsIntegerWidenableFormat(format));
|
||||
}
|
||||
|
||||
// One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are
|
||||
// e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to
|
||||
// read and the exponent bias is the 15 a 5-bit exponent always carries.
|
||||
static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) {
|
||||
const Uint32 mantissaScale = 1u << mantissaBits;
|
||||
const Uint32 mantissa = bits & (mantissaScale - 1u);
|
||||
const Uint32 exponent = bits >> mantissaBits;
|
||||
if (exponent == 0u) {
|
||||
// Subnormal, and zero with it: no implied leading 1, and the exponent is the
|
||||
// smallest NORMAL one rather than the encoded 0.
|
||||
return std::ldexp(static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale), -14);
|
||||
}
|
||||
if (exponent == 31u) {
|
||||
return mantissa == 0u ? std::numeric_limits<Float>::infinity()
|
||||
: std::numeric_limits<Float>::quiet_NaN();
|
||||
}
|
||||
return std::ldexp(1.0f + static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale),
|
||||
static_cast<Int>(exponent) - 15);
|
||||
}
|
||||
|
||||
// The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F
|
||||
// carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha
|
||||
// channel, which is the same constant the shader-side mask writes, so a texel this
|
||||
// function produced and a texel an imageStore produced are indistinguishable.
|
||||
//
|
||||
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is:
|
||||
// the driver reads a full width*height*depth*4 floats for the transfer it was handed.
|
||||
static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data,
|
||||
SizeT byteSize, Vector<Uint8>& widenedData) {
|
||||
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
|
||||
if (data == nullptr || byteSize < kSourceTexelBytes) {
|
||||
return data;
|
||||
}
|
||||
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||
if (texelCount == 0) {
|
||||
return data;
|
||||
}
|
||||
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
|
||||
|
||||
widenedData.assign(texelCount * 4u * sizeof(Float), 0);
|
||||
const auto* src = static_cast<const Uint8*>(data);
|
||||
auto* dst = reinterpret_cast<Float*>(widenedData.data());
|
||||
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
|
||||
Float rgb[3] = {0.0f, 0.0f, 0.0f};
|
||||
if (i < copyTexelCount) {
|
||||
Uint32 packed = 0;
|
||||
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
|
||||
// buffer with no alignment promise of its own.
|
||||
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
|
||||
rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u);
|
||||
rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u);
|
||||
rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u);
|
||||
}
|
||||
dst[0] = rgb[0];
|
||||
dst[1] = rgb[1];
|
||||
dst[2] = rgb[2];
|
||||
dst[3] = 1.0f;
|
||||
}
|
||||
return widenedData.data();
|
||||
}
|
||||
|
||||
// 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.
|
||||
// transfer, so its narrower client data has to be repacked the same way the three-channel
|
||||
// colour-renderable widening repacks its own.
|
||||
//
|
||||
// Two shapes, because the carriers come in two kinds. Seventeen of the eighteen keep the
|
||||
// frontend format's component TYPE and only add channels, so padding the shadow out to
|
||||
// four components is the whole conversion. r11f_g11f_b10f does not: its shadow is one
|
||||
// PACKED 32-bit word per texel and its carrier is GL_RGBA16F, so the word has to be
|
||||
// DECODED into four floats. Reading it as three components of the carrier's type - what
|
||||
// the repack below would do - would take twelve bytes from a four-byte texel and shear
|
||||
// the level, which is what the allFormats LOAD walkers see and the STORE ones do not (a
|
||||
// store overwrites every texel the upload got wrong).
|
||||
//
|
||||
// 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.
|
||||
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
|
||||
// reports a count for, 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 conversion 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) {
|
||||
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
|
||||
return data;
|
||||
}
|
||||
if (widening.PackedFloatSource) {
|
||||
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
|
||||
}
|
||||
if (widening.SourceChannels == 4) {
|
||||
return data;
|
||||
}
|
||||
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
|
||||
@@ -5280,12 +5359,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// The GL internal format a glslang layout format names, for the seventeen non-core
|
||||
// formats WidenImageFormatsForEssl carries exactly plus nothing else: the only
|
||||
// The GL internal format a glslang layout format names, for the eighteen non-core
|
||||
// formats WidenImageFormatsForEssl carries losslessly 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.
|
||||
//
|
||||
// IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms
|
||||
// the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the
|
||||
// pass would carry but this switch answers 0 for never gets the chance: the module
|
||||
// reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the
|
||||
// only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has
|
||||
// one. That is exactly what r11f_g11f_b10f did until it was added here.
|
||||
Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) {
|
||||
switch (format) {
|
||||
case glslang::ElfRg32f: return 0x8230; // GL_RG32F
|
||||
@@ -5305,6 +5391,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
case glslang::ElfR16ui: return 0x8234; // GL_R16UI
|
||||
case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI
|
||||
case glslang::ElfR8ui: return 0x8232; // GL_R8UI
|
||||
// Not a channel widening but a lossless re-encoding into rgba16f - the one entry
|
||||
// here whose carrier has a different per-channel layout. See
|
||||
// WidenImageFormatsPass.h.
|
||||
case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -5365,11 +5455,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
// Eighteen of the twenty-six non-core formats are re-declared in a core
|
||||
// format that carries them losslessly, with every access masked back to
|
||||
// the channels GL says they have (WidenImageFormatsForEssl, and the
|
||||
// matching storage/bind widening in TextureImpl). Those need neither the
|
||||
// extension nor the diagnostic: there IS a legal spelling for them now.
|
||||
if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) {
|
||||
inputs.declaresWidenableImageFormat = true;
|
||||
} else {
|
||||
|
||||
@@ -277,6 +277,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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);
|
||||
// r11f_g11f_b10f is the one carrier that is not a channel widening, and the transfer
|
||||
// pair has to say so. Every other entry keeps the frontend format's own component
|
||||
// type - a GL_RG16F shadow is halves and so is its GL_RGBA16F carrier, so padding the
|
||||
// channels is the whole conversion. This shadow is a PACKED 32-bit word (GL_RGB with
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV, TextureFormatProcessor::NormalizePixelFormat), and
|
||||
// no ES driver accepts that type for a GL_RGBA16F level. GL_FLOAT is asked for
|
||||
// instead - legal for GL_RGBA16F, and the type the unpack in
|
||||
// PrepareImageWidenedUpload writes - so the two sides name the same layout.
|
||||
if (internalFormat == TextureInternalFormat::R11FG11FB10F) {
|
||||
widening.Format = GL_RGBA;
|
||||
widening.Type = GL_FLOAT;
|
||||
widening.PackedFloatSource = true;
|
||||
}
|
||||
return widening;
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
|
||||
@@ -113,6 +113,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||
// RG8UI), so the carrier decides.
|
||||
Bool IntegerData = false;
|
||||
// The frontend shadow is a PACKED word rather than SourceChannels separate components
|
||||
// of the carrier's own type, so the upload has to DECODE it instead of padding it out
|
||||
// (PrepareImageWidenedUpload). True only for r11f_g11f_b10f, whose shadow is one
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV per texel and whose carrier is GL_RGBA16F: the
|
||||
// channel repack every other entry uses would read three floats out of a four-byte
|
||||
// texel and shear the level.
|
||||
Bool PackedFloatSource = false;
|
||||
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
|
||||
@@ -262,6 +262,95 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R11F_G11F_B10F, the format the allFormats and allTargets walkers stop at once the
|
||||
// channel widening has carried everything before it - and the one carrier that is NOT a
|
||||
// channel widening. It has no core format of its own per-channel width, so it is carried
|
||||
// in GL_RGBA16F, whose 5-bit exponent and longer mantissa hold every 11f (e5m6) and 10f
|
||||
// (e5m5) value exactly.
|
||||
//
|
||||
// What makes this case different from every other one here, and why it is worth its own
|
||||
// test: the frontend's shadow for this format is ONE PACKED 32-BIT WORD per texel, not
|
||||
// three components of the carrier's type. The upload therefore has to DECODE it, where
|
||||
// every other widening only pads channels onto data already in the right component type.
|
||||
// A widening that reused the channel repack reads three floats out of a four-byte texel
|
||||
// and shears the whole level - which a STORE test cannot see, because the dispatch
|
||||
// overwrites every texel the upload got wrong. So the seed here is per-texel distinct and
|
||||
// is checked through an imageLoad BEFORE anything is stored.
|
||||
//
|
||||
// Every constant is chosen to be exact in both encodings, so the comparisons can be
|
||||
// equality rather than tolerance: the 1/8 steps need three mantissa bits of the 11f
|
||||
// channels' six, and the 1/16 steps at exponent 1 need one of the 10f channel's five.
|
||||
TEST_F(NonCoreImageFormatScenario, PackedFloatImageDecodesItsUploadAndDropsSurplusStores) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
std::vector<float> seed(static_cast<std::size_t>(kTexels) * 3u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 3 + 0] = 1.0f + static_cast<float>(texel) / 8.0f;
|
||||
seed[texel * 3 + 1] = 2.0f + static_cast<float>(texel) / 8.0f;
|
||||
seed[texel * 3 + 2] = 3.0f + static_cast<float>(texel) / 16.0f;
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_R11F_G11F_B10F, GL_RGB, GL_FLOAT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r11f_g11f_b10f, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r11f_g11f_b10f, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(5.0, 6.0, 7.0, 8.0));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
// THE UPLOAD, read back through the image. A sheared decode still produces plausible
|
||||
// floats, so the check is per texel and the seed never repeats a value.
|
||||
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 0], seed[texel * 3 + 0]) << "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], seed[texel * 3 + 1]) << "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], seed[texel * 3 + 2]) << "texel " << texel << " blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
|
||||
// THE STORE. Three channels survive and the fourth is dropped, which is the mask this
|
||||
// format needs and no other widened format does - every other carrier here pins two
|
||||
// or three of the carrier's channels, this one pins only alpha.
|
||||
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
const std::vector<float> stored = ReadFloats(narrow, GL_RGB, 3);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_FLOAT_EQ(stored[texel * 3 + 0], 5.0f) << "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(stored[texel * 3 + 1], 6.0f) << "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(stored[texel * 3 + 2], 7.0f) << "texel " << texel << " blue";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
|
||||
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
|
||||
// no text was produced for the stage and the dispatch could not run.
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
// 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
|
||||
// become a core carrier that loses nothing, 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 eight formats with no lossless carrier at all - 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.
|
||||
@@ -229,9 +229,35 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// 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.
|
||||
// r11f_g11f_b10f: THREE float channels in a packed 32-bit word, and the only format the four
|
||||
// CTS allFormats/allTargets walkers still aborted on after the channel widening landed - it
|
||||
// has no core carrier of the same per-channel width, so it took rgba16f, whose 5-bit exponent
|
||||
// and longer mantissa represent every 11f and 10f value exactly.
|
||||
const char* const kR11fG11fB10fLoadStore = R"(#version 430 core
|
||||
layout(r11f_g11f_b10f, 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;
|
||||
}
|
||||
)";
|
||||
|
||||
// rg32f again, but as a BUFFER image. Same format, same carrier on paper - and it must be
|
||||
// left alone anyway, because a buffer image's texels are the application's buffer object.
|
||||
const char* const kRg32fBufferLoadStore = R"(#version 430 core
|
||||
layout(rg32f, binding = 0) uniform imageBuffer img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, int(gl_FragCoord.x));
|
||||
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16 is one of the EIGHT with no core carrier at all - core ESSL has no 16-bit normalized
|
||||
// format, so every candidate loses range or changes the component type the texture presents.
|
||||
// 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;
|
||||
@@ -247,7 +273,7 @@ void main() {
|
||||
// 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) {
|
||||
TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
struct Case {
|
||||
Uint requested;
|
||||
Uint carrier;
|
||||
@@ -272,6 +298,10 @@ TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
|
||||
{0x8234, 0x8D76, 1, "GL_R16UI -> GL_RGBA16UI"},
|
||||
{0x8238, 0x8D7C, 2, "GL_RG8UI -> GL_RGBA8UI"},
|
||||
{0x8232, 0x8D7C, 1, "GL_R8UI -> GL_RGBA8UI"},
|
||||
// The one entry that is a re-encoding rather than a channel widening: 11f is e5m6 and 10f
|
||||
// is e5m5 against a half's s1e5m10, so the carrier is still lossless - and three channels,
|
||||
// so the mask has to pin only alpha.
|
||||
{0x8C3A, 0x881A, 3, "GL_R11F_G11F_B10F -> GL_RGBA16F"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier)
|
||||
@@ -287,7 +317,7 @@ TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) {
|
||||
TEST(WidenImageFormats, CoreFormatsAndTheEightWithoutALosslessCarrierAreRefused) {
|
||||
// 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*/,
|
||||
@@ -297,10 +327,12 @@ TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) {
|
||||
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*/,
|
||||
// The eight with no LOSSLESS core carrier: core ESSL has no 16-bit normalized format and no
|
||||
// 10-bit one, so every candidate for these either loses range or changes the component type
|
||||
// the texture presents to anything that samples it. Deliberately left to the honest
|
||||
// diagnostic. r11f_g11f_b10f is NOT among them - rgba16f holds every value it can, so it is
|
||||
// carried above.
|
||||
for (const Uint hardFormat : {0x8059u /*RGB10_A2*/,
|
||||
0x906Fu /*RGB10_A2UI*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/,
|
||||
0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/,
|
||||
0x8F98u /*R16_SNORM*/}) {
|
||||
@@ -357,6 +389,101 @@ TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked
|
||||
<< "the mask must be a separate value, or it would feed itself";
|
||||
}
|
||||
|
||||
// The three-channel case, which no format exercised before r11f_g11f_b10f was carried: only ALPHA
|
||||
// is surplus, so the mask must take r, g and b from the texel and nothing but the fourth component
|
||||
// from the (0, 0, 0, 1) constant. A mask that zeroed blue here - the shape a two-channel format
|
||||
// wants - would silently drop the third channel of every store.
|
||||
TEST(WidenImageFormats, ThreeChannelPackedFloatImageBecomesRgba16fWithOnlyAlphaPinned) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kR11fG11fB10fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R11fG11fB10f));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16f));
|
||||
|
||||
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) << "the imageStore texel is not a masked value";
|
||||
EXPECT_TRUE(HasComponents(*storeMask, {0u, 1u, 2u, 7u}))
|
||||
<< "expected (r, g, b, 1) - components 0, 1 and 2 of the texel, then 3 of (0,0,0,1)";
|
||||
|
||||
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, 2u, 7u}));
|
||||
}
|
||||
|
||||
// ...and the same module through the emitter, which is where the failure actually showed: ESSL has
|
||||
// no `r11f_g11f_b10f` token, SPIRV-Cross throws for it, and the throw took every image uniform
|
||||
// declared in the same stage with it.
|
||||
TEST(WidenImageFormats, PackedFloatImageOnlyReachesEsslThroughTheCarrier) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kR11fG11fB10fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const EsslAttempt before = EmitEssl(spirv);
|
||||
EXPECT_FALSE(before.succeeded)
|
||||
<< "SPIRV-Cross printed r11f_g11f_b10f for an ES target; the widening's premise has "
|
||||
"changed:\n"
|
||||
<< before.text;
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("rgba16f"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("r11f_g11f_b10f"), String::npos) << after.text;
|
||||
}
|
||||
|
||||
// A BUFFER image is declined whatever its format, and the format alone cannot say so - rg32f is
|
||||
// carried exactly when it is an image2D. What makes the difference is that widening REALLOCATES
|
||||
// the texture behind the image in the carrier, and a buffer image has no texture storage to
|
||||
// reallocate: its texels are the application's buffer object, usually also a vertex, index or
|
||||
// storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels - the
|
||||
// measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
|
||||
// [1,100] [0,1] [2,100] [0,1] instead of [1,100] [2,100] [3,100] [4,100], with the last two texels
|
||||
// written past the end of the application's buffer.
|
||||
TEST(WidenImageFormats, BufferImagesAreDeclinedEvenWhenTheirFormatHasACarrier) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const auto types = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
|
||||
<< "the fixture stopped declaring the format this test is about";
|
||||
|
||||
// The gate says no, so the optimizer is never even run for it...
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
// ...and running it anyway changes nothing, which is what keeps the gate and the pass from
|
||||
// disagreeing about a module.
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
EXPECT_TRUE(widened.empty() || widened == spirv) << "a buffer image was rewritten";
|
||||
|
||||
// The same format in a NON-buffer image still widens, or this test would pass for the wrong
|
||||
// reason - a widening that had simply stopped working.
|
||||
const Vector<Uint32> planar = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(planar.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(planar));
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kR8uiLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace MobileGL {
|
||||
// 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 kImageDimOperand = 1;
|
||||
constexpr uint32_t kImageSampledOperand = 5;
|
||||
constexpr uint32_t kImageFormatOperand = 6;
|
||||
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
|
||||
@@ -50,16 +51,40 @@ namespace MobileGL {
|
||||
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.
|
||||
// The carrier of a non-core image format: a core GLSL ES format that represents
|
||||
// every value the original can hold, WITHOUT LOSS. `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.
|
||||
// Almost every entry is a pure CHANNEL widening - same component type, same
|
||||
// per-channel width, more channels (rg32f -> rgba32f) - and for those the carrier
|
||||
// is bit-exact: the storage holds the identical encoding, only wider.
|
||||
//
|
||||
// r11f_g11f_b10f is the one entry that is not. It has no same-width core carrier,
|
||||
// so it takes rgba16f, and the two encodings differ. What matters is that the
|
||||
// carrier is still LOSSLESS: an 11-bit float is e5m6 and a 10-bit float is e5m5,
|
||||
// while a half is s1e5m10 - the SAME 5-bit exponent with a strictly longer
|
||||
// mantissa - so every value the packed format can represent has an exact half.
|
||||
// Nothing an application stores is rounded away.
|
||||
//
|
||||
// What DOES change is the reverse direction: the carrier can hold values the
|
||||
// packed format could not - negatives (11f and 10f are unsigned), and mantissa
|
||||
// bits finer than the 6 and 5 the format quantises to - so a value written through
|
||||
// the image and then SAMPLED comes back on half's grid rather than the packed
|
||||
// format's. That is a strictly finer grid, never a lossy one, and it is measured
|
||||
// against the alternative, which is not a more faithful quantisation but no
|
||||
// program at all: `layout(r11f_g11f_b10f)` has no ESSL spelling, SPIRV-Cross
|
||||
// throws for it, and the stage - with every other image uniform declared beside it
|
||||
// - is lost (KHR-GL43.shader_image_load_store.basic-allFormats-*, which fail on
|
||||
// this format alone, and multiple-uniforms, where one such declaration killed a
|
||||
// program holding eight images).
|
||||
//
|
||||
// The remaining eight - rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm,
|
||||
// rg16_snorm and r16_snorm - stay absent, and for a stronger reason than
|
||||
// quantisation: core ESSL has no 16-bit normalized format at all and no 10-bit
|
||||
// one, so every candidate carrier for them either loses range or changes the
|
||||
// component TYPE the texture presents. 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;
|
||||
@@ -73,6 +98,9 @@ namespace MobileGL {
|
||||
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};
|
||||
// Not a channel widening but a lossless re-encoding - see above. Three
|
||||
// channels, so the fourth reads as the 1 GL defines for a format without one.
|
||||
case spv::ImageFormat::R11fG11fB10f: return {spv::ImageFormat::Rgba16f, 3};
|
||||
// Unsigned normalized.
|
||||
case spv::ImageFormat::Rg8: return {spv::ImageFormat::Rgba8, 2};
|
||||
case spv::ImageFormat::R8: return {spv::ImageFormat::Rgba8, 1};
|
||||
@@ -219,6 +247,24 @@ namespace MobileGL {
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
|
||||
if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false;
|
||||
// A BUFFER image is never widened, whatever its format. Widening works because
|
||||
// the ES texture behind the image can be REALLOCATED in the carrier, so the
|
||||
// texel the shader addresses and the texel the storage holds stay the same
|
||||
// size. A buffer image has no storage of its own to reallocate: its texels are
|
||||
// the application's buffer object, at the size and layout the application gave
|
||||
// it, and that buffer is usually also a vertex, index or storage buffer whose
|
||||
// contents are not ours to relayout.
|
||||
//
|
||||
// Widening one anyway makes the shader stride 16 bytes through 8-byte texels.
|
||||
// Measured on an Adreno 830 with a 32-byte GL_RG32F buffer and a shader storing
|
||||
// (i+1, 100) at texel i: the readback came back [1,100] [0,1] [2,100] [0,1] -
|
||||
// texels 0 and 1 landed on top of all four, texels 2 and 3 ran off the end of
|
||||
// the application's buffer. Declining leaves the honest "no GLSL ES spelling"
|
||||
// failure instead, which loses the same stage but corrupts nothing.
|
||||
if (static_cast<spv::Dim>(type->GetSingleWordInOperand(kImageDimOperand)) ==
|
||||
spv::Dim::Buffer) {
|
||||
return false;
|
||||
}
|
||||
const auto format =
|
||||
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
|
||||
if (!WideningOfSpirvImageFormat(format)) return false;
|
||||
|
||||
@@ -54,12 +54,23 @@ namespace MobileGL {
|
||||
// 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.
|
||||
// r11f_g11f_b10f has no same-width core carrier either, and takes rgba16f anyway,
|
||||
// because that carrier is still LOSSLESS: 11f is e5m6 and 10f is e5m5 against a half's
|
||||
// s1e5m10 - the SAME 5-bit exponent with a strictly longer mantissa - so every value
|
||||
// the packed format can hold has an exact half. Only the reverse direction differs
|
||||
// (the carrier also holds negatives, which 11f and 10f cannot sign, and mantissa bits
|
||||
// finer than the 6 and 5 they quantise to, so a value written through the image and
|
||||
// then SAMPLED lands on half's grid rather than the packed format's). That is measured
|
||||
// against the alternative, which is not a truer quantisation but no program at all:
|
||||
// the SPIRV-Cross throw takes the whole stage, every image uniform declared beside it
|
||||
// included.
|
||||
//
|
||||
// The other EIGHT (rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm,
|
||||
// r16_snorm) are deliberately NOT widened here: core ESSL has no 16-bit normalized
|
||||
// format at all and no 10-bit one, so every carrier for them either loses range or
|
||||
// changes the component TYPE the texture a `sampler2D` would read presents. They keep
|
||||
// the honest "no GLSL ES spelling" diagnostic instead of silently changing an
|
||||
// application's numeric domain.
|
||||
//
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user