diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 0a81fb6c..06538896 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -29,7 +29,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -2706,21 +2708,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(mantissa) / static_cast(mantissaScale), -14); + } + if (exponent == 31u) { + return mantissa == 0u ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return std::ldexp(1.0f + static_cast(mantissa) / static_cast(mantissaScale), + static_cast(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& widenedData) { + constexpr SizeT kSourceTexelBytes = sizeof(Uint32); + if (data == nullptr || byteSize < kSourceTexelBytes) { + return data; + } + const SizeT texelCount = static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(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(data); + auto* dst = reinterpret_cast(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& 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, diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index cfdadae7..2d3ba4c1 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -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{}, 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 8d9d0dcc..b062671c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -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; } }; diff --git a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp index 60f45bb6..53e52209 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp @@ -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 seed(static_cast(kTexels) * 3u, 0.0f); + for (int texel = 0; texel < kTexels; ++texel) { + seed[texel * 3 + 0] = 1.0f + static_cast(texel) / 8.0f; + seed[texel * 3 + 1] = 2.0f + static_cast(texel) / 8.0f; + seed[texel * 3 + 2] = 3.0f + static_cast(texel) / 16.0f; + } + const std::vector wideSeed(static_cast(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 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 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.