mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 14:48:32 +09:00
[Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui
This commit is contained in:
@@ -39,6 +39,8 @@
|
||||
// store, or masked it with the wrong constants, or widened the storage without widening the bind,
|
||||
// fails these on the device while the software lanes stay green.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -176,6 +178,62 @@ namespace MGITest {
|
||||
return texels;
|
||||
}
|
||||
|
||||
// A GL_TEXTURE_CUBE_MAP_ARRAY of `cubeCount` cubes, i.e. 6 * cubeCount layer-faces
|
||||
// addressed as array layers. The target the allTargets walkers reach last and the one
|
||||
// that has caught the most emulation bugs, because it is the only one whose ES
|
||||
// equivalent is a 2D array with a different addressing rule from the GL name.
|
||||
GLuint MakeCubeArrayTexture(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType,
|
||||
const void* seed, int cubeCount) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent,
|
||||
6 * cubeCount);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating cube-array storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (seed != nullptr) {
|
||||
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent,
|
||||
6 * cubeCount, uploadFormat, uploadType, seed);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindLayeredImage(GLuint unit, GLuint texture, GLenum internalFormat, GLenum access) {
|
||||
glBindImageTexture(unit, texture, 0, GL_TRUE, 0, access, internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u)
|
||||
<< "glBindImageTexture refused layered format " << std::hex << internalFormat;
|
||||
}
|
||||
|
||||
// Sets `name` from `values`, which must hold 4 * count floats.
|
||||
void SetVec4Array(GLuint program, const char* name, const std::vector<float>& values,
|
||||
int count) {
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, name);
|
||||
ASSERT_GE(location, 0) << "the uniform array '" << name << "' was not reflected";
|
||||
glUniform4fv(location, count, values.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "setting '" << name << "' errored";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<float> ReadFloatsFrom(GLenum target, GLuint texture, GLenum format,
|
||||
int componentsPerTexel, int texelCount) {
|
||||
std::vector<float> texels(static_cast<std::size_t>(texelCount) * componentsPerTexel,
|
||||
-12345.0f);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, 0, format, GL_FLOAT, texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<GLuint> ReadUints(GLuint texture, GLenum format, int componentsPerTexel) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
|
||||
0xFFFFFFFFu);
|
||||
@@ -506,6 +564,430 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RG16, the first of the seven NORMALIZED formats and the first carrier that changes the
|
||||
// shader-visible TYPE: core ESSL has no 16-bit normalized image format of any width, and
|
||||
// no float carrier is honest either (a half has eleven mantissa bits against sixteen), so
|
||||
// the rgba16ui behind it holds the format's own CODES and every access converts.
|
||||
//
|
||||
// Both directions of GL 4.6 2.3.5 are checked, and the STORE direction is checked as exact
|
||||
// INTEGER CODES rather than as floats within a tolerance - which is the point of a code
|
||||
// carrier over a float one, and the only thing that would catch a rounding rule that was
|
||||
// merely close. The values are chosen so that the products are exact in float32: 0.25 and
|
||||
// 0.75 land off a tie, 0.5 lands exactly ON one (0.5 * 65535 = 32767.5), and the two
|
||||
// out-of-range values must be clamped before they are rounded rather than after.
|
||||
TEST_F(NonCoreImageFormatScenario, UnsignedNormalizedImageCarriesItsCodesBothWays) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
// THE UPLOAD. Distinct per texel, and the codes are the shadow's own 16-bit words: a
|
||||
// widening that padded or sheared them still produces plausible normalized floats.
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels) * 2u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 2 + 0] = static_cast<GLushort>(texel * 4001);
|
||||
seed[texel * 2 + 1] = static_cast<GLushort>(65535 - texel * 3001);
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RG16, GL_RG, GL_UNSIGNED_SHORT, 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 (rg16, 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 (rg16, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[texel * 2 + 0])
|
||||
<< "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), seed[texel * 2 + 1])
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f)
|
||||
<< "texel " << texel << ": imageLoad on a two-channel format must report 0 for blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
|
||||
// THE STORE, per GL 4.6 2.3.5: c = round(clamp(f, 0, 1) * (2^b - 1)), with a tie
|
||||
// rounded away from zero.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 65535}, {0.5f, 32768}, {0.25f, 16384},
|
||||
{0.75f, 49151}, {-0.5f, 0}, {2.0f, 65535}, {-1.0f, 0},
|
||||
{1.0f / 131072.0f, 0}, // 0.4999923 of a code: rounds DOWN
|
||||
{3.0f / 131072.0f, 1}, // 1.4999771 of a code: rounds DOWN to 1
|
||||
{1.0f / 65535.0f, 1}, // exactly one code
|
||||
{32767.0f / 65535.0f, 32767}, {32768.0f / 65535.0f, 32768},
|
||||
// 0.125 * 65535 = 8191.875 and 0.875 * 65535 = 57343.125 - neither is a tie, and
|
||||
// both round DOWN, which is the pair that catches a conversion that scaled by 2^b
|
||||
// instead of 2^b - 1.
|
||||
{65534.0f / 65535.0f, 65534}, {0.125f, 8192}, {0.875f, 57343},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
values[texel * 4 + 0] = boundaries[texel].value;
|
||||
values[texel * 4 + 1] = boundaries[texel].value;
|
||||
values[texel * 4 + 2] = 0.5f; // dropped: a two-channel format has no blue
|
||||
values[texel * 4 + 3] = 0.5f; // dropped: nor an alpha
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
// Read back through the IMAGE, so what is compared is the code the store actually
|
||||
// wrote rather than anything the readback path might renormalize on its own.
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " stored " << boundaries[texel].value;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f) << "texel " << texel << " blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f) << "texel " << texel << " alpha";
|
||||
}
|
||||
|
||||
// ...and glGetTexImage owes the application the NORMALIZED value, whatever the ES
|
||||
// storage holds. The whole texture is an integer one now, so this is the only place
|
||||
// the readback conversion is exercised at all.
|
||||
const std::vector<float> viaGetTexImage = ReadFloats(narrow, GL_RG, 2);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " red through glGetTexImage";
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green through glGetTexImage";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGBA16_SNORM, the signed twin. Two things differ and both are one-line mistakes: the
|
||||
// code is a two's-complement 16-bit integer stored in an UNSIGNED carrier channel, so it
|
||||
// has to be sign-extended on the way out (a zero extension reads every negative value as
|
||||
// something near +1), and the decode is max(c / 32767, -1) rather than the bare division,
|
||||
// because the code -32768 exists and GL says it means exactly -1.
|
||||
TEST_F(NonCoreImageFormatScenario, SignedNormalizedImageSignExtendsItsCodesAndClampsAtMinusOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kSnorm16Max = 32767.0;
|
||||
|
||||
// The seed walks the whole signed range, INCLUDING -32768, whose decode is the one
|
||||
// value the division alone gets wrong.
|
||||
const GLshort seedCodes[kTexels] = {0, 32767, -32767, -32768, 1, -1, 16384, -16384,
|
||||
12345, -12345, 32766, -32766, 255, -256, 4095, -4096};
|
||||
std::vector<GLshort> seed(static_cast<std::size_t>(kTexels) * 4u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 4 + 0] = seedCodes[texel];
|
||||
seed[texel * 4 + 1] = static_cast<GLshort>(-seedCodes[texel] == -32768 ? 32767
|
||||
: -seedCodes[texel]);
|
||||
seed[texel * 4 + 2] = seedCodes[(texel + 1) % kTexels];
|
||||
seed[texel * 4 + 3] = seedCodes[(texel + 2) % kTexels];
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -12.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, 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 (rgba16_snorm, 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 (rgba16_snorm, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const GLshort code = seed[texel * 4 + channel];
|
||||
const float expected =
|
||||
std::max(static_cast<float>(code) / static_cast<float>(kSnorm16Max), -1.0f);
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel], expected)
|
||||
<< "texel " << texel << " channel " << channel << " code " << code;
|
||||
}
|
||||
}
|
||||
|
||||
// THE STORE: c = round(clamp(f, -1, 1) * (2^(b-1) - 1)), ties away from zero on BOTH
|
||||
// sides - which is what makes -0.5 land on -16384 rather than on -16383.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 32767}, {-1.0f, -32767}, {0.5f, 16384},
|
||||
{-0.5f, -16384}, {2.0f, 32767}, {-2.0f, -32767}, {0.25f, 8192},
|
||||
{-0.25f, -8192}, {1.0f / 32767.0f, 1}, {-1.0f / 32767.0f, -1},
|
||||
// Three quarters of a code, not half: GL leaves the direction of a TIE to the
|
||||
// implementation ("if two values are equally near, the implementation may choose
|
||||
// either"), and Magma hands these formats to Vulkan unemulated, so a value exactly
|
||||
// on 0.5 of a code is the one thing the two backends are allowed to disagree
|
||||
// about. Every entry here is off a tie except the ones at 0.5 and 0.25 of the
|
||||
// RANGE, whose products (16383.5 and 8192) round the same way under either rule.
|
||||
{0.75f / 32767.0f, 1}, {-0.75f / 32767.0f, -1},
|
||||
{16383.0f / 32767.0f, 16383}, {-16383.0f / 32767.0f, -16383},
|
||||
{0.125f, 4096},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
values[texel * 4 + channel] = boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + channel] * kSnorm16Max),
|
||||
boundaries[texel].code)
|
||||
<< "texel " << texel << " channel " << channel << " stored "
|
||||
<< boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB10_A2, the one normalized format whose channels are not all the same width: three
|
||||
// of ten bits and one of two. A single denominator would be right for three quarters of
|
||||
// every texel and wildly wrong for the fourth - alpha 1.0 would come back as 3/1023.
|
||||
TEST_F(NonCoreImageFormatScenario, TenTenTenTwoImageUsesItsOwnPerChannelDenominators) {
|
||||
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<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
|
||||
std::vector<GLuint> seedCodes(static_cast<std::size_t>(kTexels) * 4u, 0u);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
const GLuint r = static_cast<GLuint>(texel) * 67u;
|
||||
const GLuint g = 1023u - static_cast<GLuint>(texel) * 13u;
|
||||
const GLuint b = 341u + static_cast<GLuint>(texel);
|
||||
const GLuint a = static_cast<GLuint>(texel) % 4u;
|
||||
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
|
||||
seedCodes[texel * 4 + 0] = r;
|
||||
seedCodes[texel * 4 + 1] = g;
|
||||
seedCodes[texel * 4 + 2] = b;
|
||||
seedCodes[texel * 4 + 3] = a;
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow =
|
||||
MakeTexture(GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, 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 (rgb10_a2, 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 (rgb10_a2, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.5, 1.0, 1.0));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const auto denominator = channel == 3 ? 3.0f : 1023.0f;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel],
|
||||
static_cast<float>(seedCodes[texel * 4 + channel]) / denominator)
|
||||
<< "texel " << texel << " channel " << channel;
|
||||
}
|
||||
}
|
||||
|
||||
// 0.5 through a TWO-bit channel is 1.5 of a code and rounds away from zero to 2, which
|
||||
// is 2/3 back - a value only the two-bit denominator can produce.
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * 1023.0), 0) << "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * 1023.0), 512) << "texel " << texel << " green";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 2] * 1023.0), 1023) << "texel " << texel << " blue";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 3] * 3.0), 3) << "texel " << texel << " alpha";
|
||||
}
|
||||
}
|
||||
|
||||
// The same carrier on a GL_TEXTURE_CUBE_MAP_ARRAY, the target the allTargets walkers reach
|
||||
// last and the one whose ES equivalent is addressed differently from its GL name (six
|
||||
// layer-faces per cube, as array layers). Nothing about the format conversion changes with
|
||||
// the target - which is exactly the claim, since the storage widening, the layered bind and
|
||||
// the per-layer readback all have their own code paths for this target alone.
|
||||
TEST_F(NonCoreImageFormatScenario, NormalizedImageCarriesEveryLayerFaceOfACubeMapArray) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
constexpr int kCubes = 2;
|
||||
constexpr int kLayerFaces = 6 * kCubes;
|
||||
constexpr int kTexelsPerFace = kExtent * kExtent;
|
||||
constexpr int kTexels = kTexelsPerFace * kLayerFaces;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels), 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel] = static_cast<GLushort>((texel * 5477u) & 0xFFFFu);
|
||||
}
|
||||
const GLuint narrow =
|
||||
MakeCubeArrayTexture(GL_R16, GL_RED, GL_UNSIGNED_SHORT, seed.data(), kCubes);
|
||||
if (narrow == 0) return;
|
||||
|
||||
// THE UPLOAD, read back through glGetTexImage across every layer-face. A carrier that
|
||||
// widened the storage but seeded only the first face leaves the rest at zero, which is
|
||||
// what the per-layer readback path is there to catch, and this target is the only one
|
||||
// whose readback goes layer by layer.
|
||||
const std::vector<float> uploaded =
|
||||
ReadFloatsFrom(GL_TEXTURE_CUBE_MAP_ARRAY, narrow, GL_RED, 1, kTexels);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(uploaded[texel] * kUnorm16Max), seed[texel])
|
||||
<< "texel " << texel << " of " << kTexels;
|
||||
}
|
||||
|
||||
// ...and through an imageCubeArray, which is the declaration the shader half has to
|
||||
// carry for this target: a layered bind, an ivec3 coordinate whose z is the
|
||||
// layer-face, and the same unpack as every other target.
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexelsPerFace) * 4u, -1.0f);
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (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 (r16, binding = 0) readonly uniform imageCubeArray narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
uniform int g_layerFace;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, ivec3(coord, g_layerFace)));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0) return;
|
||||
|
||||
// Two faces, one of them past the first cube, so a carrier that addressed only the
|
||||
// first six layer-faces cannot pass.
|
||||
for (const int layerFace : {1, 9}) {
|
||||
glUseProgram(loadProgram);
|
||||
const GLint location = glGetUniformLocation(loadProgram, "g_layerFace");
|
||||
ASSERT_GE(location, 0) << "g_layerFace was not reflected";
|
||||
glUniform1i(location, layerFace);
|
||||
glUseProgram(0);
|
||||
|
||||
BindLayeredImage(kNarrowUnit, narrow, GL_R16, 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 < kTexelsPerFace; ++texel) {
|
||||
const int sourceTexel = layerFace * kTexelsPerFace + texel;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[sourceTexel])
|
||||
<< "layer-face " << layerFace << " texel " << texel;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 0.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a one-channel format must report 0 for green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The other consumer of the same texture. A widened texture's ES storage really does have
|
||||
// four channels, so a sampler reading it raw would see whatever the carrier holds; the
|
||||
// logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges
|
||||
|
||||
Reference in New Issue
Block a user