mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Fix, Test] (DirectGLES): decode the packed r11f_g11f_b10f shadow into the float level its rgba16f carrier is uploaded as
This commit is contained in:
@@ -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>
|
||||
@@ -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<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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user