[Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage

This commit is contained in:
2026-08-22 04:30:06 -04:00
parent 7eac33d17b
commit 5e29e7d266
10 changed files with 705 additions and 37 deletions
+20 -7
View File
@@ -1483,14 +1483,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
// its own to widen (its texels are the application's buffer object), so
// WidenImageFormatsPass declines to widen 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.
//
// What a buffer image takes instead is the SPLIT, which is the same three-layer move
// through a different door: the glTexBuffer view above and the bind below both name
// the single-channel base format, and the shader subscripts it two components per
// original texel. Same gate on both sides, so the two cannot disagree.
GLenum bindFormat = imageBinding.Format;
if (imageBinding.Texture->GetTarget() != TextureTarget::TextureBuffer &&
TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
GL_UNKNOWN_MGL) {
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
boundFormatSplit != GL_UNKNOWN_MGL) {
bindFormat = boundFormatSplit;
}
}
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
if (boundFormatWidening) {
@@ -3822,6 +3822,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, TextureTarget::TextureBuffer);
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
// own to widen, but the VIEW its format describes can be re-described one
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
// the format the application asked for (see GetImageBindableBufferSplitFormat).
if (m_imageBindableStorageRequired) {
if (const GLenum splitFormat =
TextureImpl::GetImageBindableBufferSplitFormat(textureBufferObject->GetFormat());
splitFormat != GL_UNKNOWN_MGL) {
glInternalFormat = splitFormat;
}
}
if (needsRegeneration) {
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
+17
View File
@@ -316,6 +316,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return widening;
}
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const auto base = static_cast<GLenum>(
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
if (base == 0) {
return GL_UNKNOWN_MGL;
}
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
// or none of them may, or the shader subscripts a view the buffer is not described as.
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
return GL_UNKNOWN_MGL;
}
return base;
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
+19
View File
@@ -150,6 +150,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl
@@ -988,6 +988,93 @@ void main()
}
}
// A BUFFER image, which takes neither of the emulations above. Its texels are the
// application's buffer object - at the size and layout the application gave it, and
// usually also a vertex, index or storage buffer - so there is nothing to reallocate a
// carrier in. What CAN be done is a SPLIT: rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the view is re-declared and every subscript is
// doubled (WidenImageFormatsPass, and the matching glTexBuffer/glBindImageTexture format
// in TextureImpl).
//
// THE NUMBERS HERE ARE THE ONES THAT PINNED THE OLD BUG. Widening a buffer image instead
// leaves the shader striding 16 bytes through 8-byte texels: measured on an Adreno 830
// with this exact 32-byte GL_RG32F buffer and this exact shader, the readback came back
// [1,100] [0,1] [2,100] [0,1] - texels 0 and 1 landed on top of all four, and texels 2 and
// 3 were written past the end of the application's buffer.
//
// This runs on every backend, and on a driver that CAN spell rg32f for an imageBuffer
// (Mesa's, which the software lanes use) nothing is split at all - which is the other half
// of the claim: the arming has to agree with the shader, so a split that fired where the
// driver needed none would double every subscript and fail here just as loudly.
TEST_F(NonCoreImageFormatScenario, BufferImageAddressesTheApplicationsOwnTexels) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
GLint maxTextureBufferSize = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
constexpr int kBufferTexels = 4;
const std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, -1.0f);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
GL_DYNAMIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
if (const GLenum error = FirstGLError()) {
glDeleteBuffers(1, &buffer);
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
}
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg32f, binding = 0) writeonly uniform imageBuffer narrow;
void main()
{
int texel = int(gl_GlobalInvocationID.x);
imageStore(narrow, texel, vec4(float(texel + 1), 100.0, 3.0, 4.0));
}
)");
if (storeProgram == 0) {
glDeleteBuffers(1, &buffer);
return;
}
BindImage(kNarrowUnit, texture, GL_RG32F, GL_WRITE_ONLY);
glUseProgram(storeProgram);
glDispatchCompute(kBufferTexels, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
std::vector<float> readback(seed.size(), -12345.0f);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
EXPECT_EQ(FirstGLError(), 0u) << "reading the buffer back errored";
for (int texel = 0; texel < kBufferTexels; ++texel) {
EXPECT_FLOAT_EQ(readback[texel * 2 + 0], static_cast<float>(texel + 1))
<< "texel " << texel << " red";
EXPECT_FLOAT_EQ(readback[texel * 2 + 1], 100.0f) << "texel " << texel << " green";
}
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteBuffers(1, &buffer);
while (glGetError() != GL_NO_ERROR) {
}
}
// 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
@@ -260,8 +260,9 @@ void main() {
}
)";
// 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.
// rg32f again, but as a BUFFER image. Same format, and NOT the same emulation: a buffer
// image's texels are the application's buffer object, so there is nothing to reallocate a
// carrier in - but the same bytes can be VIEWED as twice as many r32f texels, which is exact.
const char* const kRg32fBufferLoadStore = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
@@ -270,6 +271,28 @@ void main() {
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
fragColor = texel;
}
)";
// ...and one that asks the image how big it is, which the split has to halve: the ES view has
// twice the texels the application's format describes.
const char* const kRg32fBufferSize = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
fragColor = vec4(float(imageSize(img)));
}
)";
// rg16f as a buffer image: two channels of 16-bit float, whose single-channel base r16f core
// ESSL does not have. Nothing to split it into, so it keeps the honest failure.
const char* const kRg16fBufferLoadStore = R"(#version 430 core
layout(rg16f, 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;
}
)";
// rgb10_a2ui: FOUR unsigned-integer channels of 10, 10, 10 and 2 bits, carried in an rgba16ui
@@ -591,37 +614,157 @@ TEST(WidenImageFormats, PackedFloatImageOnlyReachesEsslThroughTheCarrier) {
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
// A BUFFER image is never WIDENED, whatever its format, and the format alone cannot say so -
// rg32f is carried in an rgba32f 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) {
//
// It is SPLIT instead, which is the opposite move: the bytes stay exactly where they are and the
// SUBSCRIPT changes. rg32f over N texels and r32f over 2N texels describe the same memory, so
// component j of texel i is texel 2i + j, and the base format is one of the thirteen ES has.
TEST(WidenImageFormats, BufferImagesAreSplitByTheSubscriptRatherThanWidened) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
ASSERT_FALSE(spirv.empty());
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
<< "the fixture stopped declaring the format this test is about";
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(split));
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R32f))
<< "the base format is the SINGLE-channel one, not the four-channel carrier a 2D image "
"would take - a buffer image that gained texel width would run off the end of the "
"application's buffer";
// ONE imageLoad became TWO, and ONE imageStore became two as well: each component of the
// original texel is its own texel of the base view.
EXPECT_EQ(CollectImageReadResultIds(split).size(), 2u * CollectImageReadResultIds(spirv).size());
EXPECT_EQ(CollectImageWriteTexelIds(split).size(), 2u * CollectImageWriteTexelIds(spirv).size());
// ...and the store's two texels are the two components, not the same one twice.
const auto shuffles = CollectVectorShuffles(split);
const auto texelIds = CollectImageWriteTexelIds(split);
ASSERT_EQ(texelIds.size(), 2u);
const VectorShuffle* firstTexel = FindShuffleWithResult(shuffles, texelIds[0]);
const VectorShuffle* secondTexel = FindShuffleWithResult(shuffles, texelIds[1]);
ASSERT_NE(firstTexel, nullptr);
ASSERT_NE(secondTexel, nullptr);
EXPECT_TRUE(HasComponents(*firstTexel, {0u, 4u, 4u, 7u}))
<< "expected (r, 0, 0, 1) - component 0 of the texel into a one-channel base format";
EXPECT_TRUE(HasComponents(*secondTexel, {1u, 4u, 4u, 7u}))
<< "expected (g, 0, 0, 1) - component 1 into the NEXT base texel";
// The subscript arithmetic itself: one multiply and one add per access.
Uint32 multiplies = 0;
Uint32 adds = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpIMul) ++multiplies;
if (opcode == spv::Op::OpIAdd) ++adds;
});
EXPECT_GE(multiplies, 2u) << "2i, once for the load and once for the store";
EXPECT_GE(adds, 2u) << "2i + 1, once for the load and once for the store";
// And what reaches the driver names a format ES has.
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("r32f"), String::npos) << after.text;
EXPECT_EQ(after.text.find("rg32f"), String::npos)
<< "the token no ES driver accepts is still in the emitted source:\n"
<< after.text;
}
// imageSize() has to be halved with everything else: the ES view really does have twice the texels
// the application's format describes, so a shader that walks the buffer by its own size would run
// off the end of it - or, on a well-behaved driver, spend half its invocations past the data.
TEST(WidenImageFormats, ASplitBufferImageReportsTheSizeItsOwnFormatDescribes) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferSize);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
Uint32 sizeQueries = 0;
Uint32 divisions = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpImageQuerySize) ++sizeQueries;
if (opcode == spv::Op::OpSDiv || opcode == spv::Op::OpUDiv) ++divisions;
});
EXPECT_EQ(sizeQueries, 1u) << "the query itself is not duplicated, only divided";
EXPECT_EQ(divisions, 1u);
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("imageSize"), String::npos) << after.text;
EXPECT_NE(after.text.find("/ 2"), String::npos)
<< "the reported size must be the application's, not the base view's:\n"
<< after.text;
}
// A buffer image whose base format is NOT core ESSL has nothing to split into, and must keep the
// honest "no GLSL ES spelling" failure rather than take a wider one: rg16f's components are 16-bit
// floats and core ESSL has no r16f, so a split would have to change the component type.
TEST(WidenImageFormats, ABufferImageWithNoCoreBaseFormatIsLeftAlone) {
const Vector<Uint32> spirv = CompileFragment(kRg16fBufferLoadStore);
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";
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
// 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";
Vector<Uint32> split;
ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true);
if (!split.empty()) {
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
}
}
// 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));
// The table the three layers share, from the other side: only the 32-bit component family has a
// core single-channel base, and a two-dimensional image never takes this route.
TEST(WidenImageFormats, OnlyTheThirtyTwoBitTwoChannelFormatsSplitAsBufferImages) {
struct Case {
Uint format;
Uint base;
const char* name;
};
const Case cases[] = {
{0x8230, 0x822E, "GL_RG32F -> GL_R32F"},
{0x823B, 0x8235, "GL_RG32I -> GL_R32I"},
{0x823C, 0x8236, "GL_RG32UI -> GL_R32UI"},
};
for (const Case& testCase : cases) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(testCase.format), testCase.base)
<< testCase.name;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.base)) << testCase.name;
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.base), 1u) << testCase.name;
}
// No core single-channel base of the right component type, so no split.
for (const Uint refused : {0x822Fu /*RG16F*/, 0x8239u /*RG16I*/, 0x823Au /*RG16UI*/, 0x822Bu /*RG8*/,
0x8F95u /*RG8_SNORM*/, 0x822Cu /*RG16*/, 0x8237u /*RG8I*/, 0x8238u /*RG8UI*/,
// Already core, or four-channel, or not an image format at all.
0x8814u /*RGBA32F*/, 0x822Eu /*R32F*/, 0x8051u /*RGB8*/, 0u}) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(refused), 0u)
<< "format 0x" << std::hex << refused;
}
}
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
@@ -1030,6 +1030,10 @@ namespace MobileGL {
outSignedNormalized);
}
Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat);
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -305,6 +305,10 @@ namespace MobileGL {
// component class with the ES storage.
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
// The single-channel core format a non-core BUFFER image is SPLIT into, or 0. See
// WidenImageFormatsPass::SplitCoreEsslBufferImageFormat - DirectGLES asks it for
// glTexBuffer's internal format and for glBindImageTexture's.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
@@ -51,7 +51,9 @@ namespace MobileGL {
// OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands.
// OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional.
// OpImageQuerySize in-operands: 0 image.
constexpr uint32_t kImageAccessImageOperand = 0;
constexpr uint32_t kImageAccessCoordinateOperand = 1;
constexpr uint32_t kImageWriteTexelOperand = 2;
// The carrier of a non-core image format: a core GLSL ES format that represents
@@ -311,6 +313,58 @@ namespace MobileGL {
}
}
// A BUFFER image cannot be widened, but it CAN be SPLIT. Its texels are the
// application's linear buffer - no padding, no swizzle, no mip chain - so an
// rg32f view of N texels and an r32f view of 2N texels describe exactly the same
// bytes, and texel i's two components are components 2i and 2i+1 of the base
// format. That is not an approximation of anything: it is the same memory
// addressed one component at a time, which is why the split is exact where the
// widening (which reallocates) is impossible.
//
// ONLY the 32-bit component family, and for one reason: the base format has to be
// core ESSL, and of the single-channel formats only r32f, r32i and r32ui are.
// rg16f would want an r16f base and rg8i an r8i, and neither exists in core, so
// those buffer images keep the honest "no GLSL ES spelling" failure. Three- and
// four-channel buffer images need nothing: the only four-channel 32-bit formats
// are already core and GL has no three-channel image format at all.
struct BufferImageSplit {
spv::ImageFormat Base = spv::ImageFormat::Unknown;
uint32_t Components = 0;
explicit operator bool() const { return Base != spv::ImageFormat::Unknown; }
};
BufferImageSplit SplitOfBufferImageFormat(spv::ImageFormat format) {
switch (format) {
case spv::ImageFormat::Rg32f: return {spv::ImageFormat::R32f, 2};
case spv::ImageFormat::Rg32i: return {spv::ImageFormat::R32i, 2};
case spv::ImageFormat::Rg32ui: return {spv::ImageFormat::R32ui, 2};
default:
return {};
}
}
Bool IsSplittableBufferImageType(const Instruction* type,
bool onlyFormatsSpirvCrossRefusesToPrint) {
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false;
if (static_cast<spv::Dim>(type->GetSingleWordInOperand(kImageDimOperand)) !=
spv::Dim::Buffer) {
return false;
}
const auto format =
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
if (!SplitOfBufferImageFormat(format)) return false;
// The same narrowing the widening takes, and it has to be the same: a driver
// that can spell rg32f for an imageBuffer needs no split, and splitting it
// anyway would double every subscript for nothing.
if (onlyFormatsSpirvCrossRefusesToPrint &&
BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) {
return false;
}
return true;
}
Bool IsWidenableStorageImageType(const Instruction* type,
bool onlyFormatsSpirvCrossRefusesToPrint) {
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
@@ -327,8 +381,9 @@ namespace MobileGL {
// 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.
// the application's buffer. It is SPLIT instead where its format allows
// (IsSplittableBufferImageType), which addresses the same bytes rather than
// restriding them, and left alone where it does not.
if (static_cast<spv::Dim>(type->GetSingleWordInOperand(kImageDimOperand)) ==
spv::Dim::Buffer) {
return false;
@@ -527,6 +582,19 @@ namespace MobileGL {
return GLInternalFormatOfSpirvImageFormat(widening.Carrier);
}
Uint WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
const BufferImageSplit split =
SplitOfBufferImageFormat(SpirvImageFormatOfGL(glInternalFormat));
if (!split) return 0;
switch (split.Base) {
case spv::ImageFormat::R32f: return 0x822E; // GL_R32F
case spv::ImageFormat::R32i: return 0x8235; // GL_R32I
case spv::ImageFormat::R32ui: return 0x8236; // GL_R32UI
default:
return 0;
}
}
bool WidenImageFormatsPass::NormalizedImageCarrierCodes(Uint glInternalFormat,
Uint32 (&outChannelMax)[4],
bool& outSignedNormalized) {
@@ -548,7 +616,8 @@ namespace MobileGL {
return false;
}
for (const Instruction& type : context->module()->types_values()) {
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint) ||
IsSplittableBufferImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
return true;
}
}
@@ -570,12 +639,15 @@ namespace MobileGL {
// Cheap gate first: no widenable image type, and the module is handed back
// byte-identical - which is every shader but a handful.
std::vector<Instruction*> imageTypes;
std::vector<Instruction*> bufferSplitTypes;
for (Instruction& type : irContext->types_values()) {
if (IsWidenableStorageImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
imageTypes.push_back(&type);
} else if (IsSplittableBufferImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
bufferSplitTypes.push_back(&type);
}
}
if (imageTypes.empty()) {
if (imageTypes.empty() && bufferSplitTypes.empty()) {
return Status::SuccessWithoutChange;
}
@@ -611,8 +683,23 @@ namespace MobileGL {
// the shader runs and quietly reads the carrier's surplus channels, which GL says
// are 0 and 1. Refusing hands the stage back to the "no GLSL ES spelling"
// diagnostic instead, which at least names the failure.
// The same for the buffer images that SPLIT. Keyed the same way and collected in
// the same walk, because the decline below has to be all-or-nothing across both:
// a module with one of each that could only rewrite one of them would emit a
// stage that addresses one image right and the other wrong.
std::map<uint32_t, BufferImageSplit> splitByTypeId;
for (Instruction* type : bufferSplitTypes) {
splitByTypeId.emplace(
type->result_id(),
SplitOfBufferImageFormat(
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand))));
}
std::vector<Instruction*> reads;
std::vector<Instruction*> writes;
std::vector<Instruction*> splitReads;
std::vector<Instruction*> splitWrites;
std::vector<Instruction*> splitSizeQueries;
Bool rewritable = true;
for (auto funcIt = irContext->module()->begin();
funcIt != irContext->module()->end() && rewritable; ++funcIt) {
@@ -623,13 +710,14 @@ namespace MobileGL {
case spv::Op::OpImageWrite:
case spv::Op::OpImageSparseRead:
case spv::Op::OpImageTexelPointer:
case spv::Op::OpImageQuerySize:
break;
default:
return;
}
// OpImageTexelPointer names the image VARIABLE (a pointer), the other
// three an image VALUE; both reach the OpTypeImage through the def's
// type, one hop further for the pointer.
// OpImageTexelPointer names the image VARIABLE (a pointer), the others an
// image VALUE; both reach the OpTypeImage through the def's type, one hop
// further for the pointer.
const Instruction* imageDef =
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
if (imageDef == nullptr) return;
@@ -638,6 +726,19 @@ namespace MobileGL {
imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) {
imageTypeId = imageType->GetSingleWordInOperand(1);
}
if (splitByTypeId.count(imageTypeId) != 0) {
switch (inst->opcode()) {
case spv::Op::OpImageRead: splitReads.push_back(inst); return;
case spv::Op::OpImageWrite: splitWrites.push_back(inst); return;
// imageSize() has to be halved with everything else: the ES view has
// twice the texels the application's format describes, and a shader
// that walks the buffer by its own size would run off the end of it.
case spv::Op::OpImageQuerySize: splitSizeQueries.push_back(inst); return;
default:
rewritable = false;
return;
}
}
const auto widenedIt = widenedByTypeId.find(imageTypeId);
if (widenedIt == widenedByTypeId.end()) return;
@@ -649,6 +750,11 @@ namespace MobileGL {
writes.push_back(inst);
return;
}
// A widened image's size does not move - the carrier has the same texel
// COUNT - so a query through one needs nothing.
if (inst->opcode() == spv::Op::OpImageQuerySize) {
return;
}
// OpImageSparseRead yields a struct rather than a plain texel vector, and
// OpImageTexelPointer is an image atomic - which spirv-val already
// restricts to r32i/r32ui/r32f, all three of them core formats that never
@@ -753,6 +859,79 @@ namespace MobileGL {
return it == widenedByTypeId.end() ? nullptr : &it->second;
};
auto splitOf = [&](const Instruction* inst) -> const BufferImageSplit* {
const Instruction* imageDef =
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
if (imageDef == nullptr) return nullptr;
const auto it = splitByTypeId.find(imageDef->type_id());
return it == splitByTypeId.end() ? nullptr : &it->second;
};
auto splitSampledTypeOf = [&](const Instruction* inst) -> uint32_t {
const Instruction* imageDef =
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
if (imageDef == nullptr) return 0u;
const Instruction* imageType = defUseMgr->GetDef(imageDef->type_id());
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage) return 0u;
return imageType->GetSingleWordInOperand(kImageSampledTypeOperand);
};
// The 1 and the component COUNT the subscript arithmetic multiplies by, one pair
// per integer type a coordinate (or an imageSize result) is spelled in. Resolved
// before any instruction is inserted, for the reason the masks' material is.
std::map<uint32_t, std::pair<uint32_t, uint32_t>> splitConstantsByIntType;
auto resolveSplitCoordConstants = [&](uint32_t intTypeId, uint32_t& outOne,
uint32_t& outComponents) -> Bool {
if (const auto cached = splitConstantsByIntType.find(intTypeId);
cached != splitConstantsByIntType.end()) {
outOne = cached->second.first;
outComponents = cached->second.second;
return outOne != 0 && outComponents != 0;
}
const Instruction* intType = defUseMgr->GetDef(intTypeId);
// A buffer image's coordinate is a 32-bit integer SCALAR in every dialect this
// backend compiles; a vector one is a shape that has never been seen and is
// refused rather than guessed at.
if (intType == nullptr || intType->opcode() != spv::Op::OpTypeInt ||
intType->GetSingleWordInOperand(0) != 32) {
return false;
}
analysis::Integer component(32, intType->GetSingleWordInOperand(1) != 0);
analysis::Type* componentReg = irContext->get_type_mgr()->GetRegisteredType(&component);
if (componentReg == nullptr) return false;
const uint32_t oneId = MakeScalarConstant(irContext, componentReg, 1u);
const uint32_t componentsId = MakeScalarConstant(irContext, componentReg, 2u);
if (oneId == 0u || componentsId == 0u) return false;
splitConstantsByIntType.emplace(intTypeId, std::make_pair(oneId, componentsId));
outOne = oneId;
outComponents = componentsId;
return true;
};
// 2i and 2i+1, inserted in front of `before`.
auto insertSplitCoordinates = [&](Instruction* before, uint32_t coordId, uint32_t& outFirst,
uint32_t& outSecond) -> Bool {
const Instruction* coord = defUseMgr->GetDef(coordId);
if (coord == nullptr) return false;
uint32_t oneId = 0;
uint32_t componentsId = 0;
if (!resolveSplitCoordConstants(coord->type_id(), oneId, componentsId)) return false;
const uint32_t firstId = irContext->TakeNextId();
const uint32_t secondId = irContext->TakeNextId();
if (firstId == 0 || secondId == 0) return false;
before->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIMul, coord->type_id(), firstId,
Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {coordId}},
{SPV_OPERAND_TYPE_ID, {componentsId}}}));
before->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIAdd, coord->type_id(), secondId,
Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {firstId}},
{SPV_OPERAND_TYPE_ID, {oneId}}}));
outFirst = firstId;
outSecond = secondId;
return true;
};
// Every constant and vector type the masks will need, declared BEFORE the first
// instruction is inserted. The constant and type managers append to the module's
// globals and keep their own def-use bookkeeping straight; the shuffles below do
@@ -766,6 +945,36 @@ namespace MobileGL {
return Status::SuccessWithoutChange;
}
}
// ...and everything the buffer-image SPLIT needs: the same (0, .., 0, 1) constant
// for its own sampled types, and the 1 and 2 its subscript arithmetic uses, one
// pair per integer type a coordinate or an imageSize result is spelled in.
for (Instruction* type : bufferSplitTypes) {
uint32_t unusedConstantId = 0;
uint32_t unusedVec4TypeId = 0;
if (!resolveMaskMaterial(type->GetSingleWordInOperand(kImageSampledTypeOperand),
unusedConstantId, unusedVec4TypeId)) {
return Status::SuccessWithoutChange;
}
}
for (const std::vector<Instruction*>* accesses : {&splitReads, &splitWrites}) {
for (Instruction* access : *accesses) {
const Instruction* coord =
defUseMgr->GetDef(access->GetSingleWordInOperand(kImageAccessCoordinateOperand));
uint32_t unusedOneId = 0;
uint32_t unusedComponentsId = 0;
if (coord == nullptr ||
!resolveSplitCoordConstants(coord->type_id(), unusedOneId, unusedComponentsId)) {
return Status::SuccessWithoutChange;
}
}
}
for (Instruction* query : splitSizeQueries) {
uint32_t unusedOneId = 0;
uint32_t unusedComponentsId = 0;
if (!resolveSplitCoordConstants(query->type_id(), unusedOneId, unusedComponentsId)) {
return Status::SuccessWithoutChange;
}
}
// ...and the same for the normalized carriers, whose rewrite needs a good deal
// more of both: the uvec4 an OpImageRead of the carrier yields, the ivec4 the
@@ -1056,6 +1265,155 @@ namespace MobileGL {
read->SetInOperands(Move(shuffleOperands));
}
// THE BUFFER-IMAGE SPLIT. Same three parts as the widening - accesses first, the
// declaration last - but the arithmetic is on the SUBSCRIPT rather than on the
// texel: what was texel i of an rg32f is components 2i and 2i+1 of an r32f over
// the same bytes. A read gathers the pair and fills the two channels the format
// does not have with GL's own 0 and 1; a store writes each component on its own.
//
// TWO OpImageWrites where the application wrote one, and they are not atomic
// together. That is not a coherence hole this introduces: GL already gives an
// imageStore no atomicity ACROSS components, and both writes are issued by the
// same invocation to two texels no other invocation of a well-formed program is
// writing (each invocation owns its own texel i). A program that DID have two
// invocations racing for one texel had undefined results before the split too.
for (Instruction* write : splitWrites) {
const BufferImageSplit* split = splitOf(write);
if (split == nullptr) continue;
uint32_t zeroOneId = 0;
uint32_t vec4TypeId = 0;
if (!resolveMaskMaterial(splitSampledTypeOf(write), zeroOneId, vec4TypeId)) {
return Status::SuccessWithoutChange;
}
const uint32_t texelId = write->GetSingleWordInOperand(kImageWriteTexelOperand);
const Instruction* texel = defUseMgr->GetDef(texelId);
if (texel == nullptr || texel->type_id() != vec4TypeId) {
return Status::SuccessWithoutChange;
}
const uint32_t coordId = write->GetSingleWordInOperand(kImageAccessCoordinateOperand);
uint32_t firstCoordId = 0;
uint32_t secondCoordId = 0;
if (!insertSplitCoordinates(write, coordId, firstCoordId, secondCoordId)) {
return Status::Failure;
}
uint32_t componentTexelIds[2] = {0u, 0u};
for (uint32_t component = 0; component < split->Components; ++component) {
const uint32_t maskedId = irContext->TakeNextId();
if (maskedId == 0) return Status::Failure;
// (texel[component], 0, 0, 1) - a one-channel base format keeps only red,
// and GL's own values for the rest.
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}},
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
shuffleOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {component}});
shuffleOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {4u}});
shuffleOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {4u}});
shuffleOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {7u}});
write->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands));
componentTexelIds[component] = maskedId;
}
// The FIRST component's write is the inserted one and the second is the
// original, so the original instruction (and anything that ordered against
// it) stays where it was.
Instruction::OperandList firstWriteOperands;
for (uint32_t i = 0; i < write->NumInOperands(); ++i) {
firstWriteOperands.push_back(write->GetInOperand(i));
}
firstWriteOperands[kImageAccessCoordinateOperand] = {SPV_OPERAND_TYPE_ID, {firstCoordId}};
firstWriteOperands[kImageWriteTexelOperand] = {SPV_OPERAND_TYPE_ID, {componentTexelIds[0]}};
write->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpImageWrite, 0, 0, firstWriteOperands));
write->SetInOperand(kImageAccessCoordinateOperand, {secondCoordId});
write->SetInOperand(kImageWriteTexelOperand, {componentTexelIds[1]});
}
for (Instruction* read : splitReads) {
const BufferImageSplit* split = splitOf(read);
if (split == nullptr) continue;
uint32_t zeroOneId = 0;
uint32_t vec4TypeId = 0;
if (!resolveMaskMaterial(splitSampledTypeOf(read), zeroOneId, vec4TypeId)) {
return Status::SuccessWithoutChange;
}
if (read->type_id() != vec4TypeId) {
return Status::SuccessWithoutChange;
}
const uint32_t coordId = read->GetSingleWordInOperand(kImageAccessCoordinateOperand);
uint32_t firstCoordId = 0;
uint32_t secondCoordId = 0;
if (!insertSplitCoordinates(read, coordId, firstCoordId, secondCoordId)) {
return Status::Failure;
}
uint32_t componentReadIds[2] = {0u, 0u};
const uint32_t coordIds[2] = {firstCoordId, secondCoordId};
for (uint32_t component = 0; component < split->Components; ++component) {
const uint32_t componentReadId = irContext->TakeNextId();
if (componentReadId == 0) return Status::Failure;
Instruction::OperandList readOperands;
for (uint32_t i = 0; i < read->NumInOperands(); ++i) {
readOperands.push_back(read->GetInOperand(i));
}
readOperands[kImageAccessCoordinateOperand] = {SPV_OPERAND_TYPE_ID, {coordIds[component]}};
read->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpImageRead, vec4TypeId, componentReadId, readOperands));
componentReadIds[component] = componentReadId;
}
// (first.x, second.x, ., .) - the last two selectors are anything in range;
// the mask below replaces them with GL's 0 and 1.
const uint32_t gatheredId = irContext->TakeNextId();
if (gatheredId == 0) return Status::Failure;
Instruction::OperandList gatherOperands{{SPV_OPERAND_TYPE_ID, {componentReadIds[0]}},
{SPV_OPERAND_TYPE_ID, {componentReadIds[1]}}};
gatherOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}});
gatherOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {4u}});
gatherOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}});
gatherOperands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}});
read->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpVectorShuffle, vec4TypeId, gatheredId, gatherOperands));
read->SetOpcode(spv::Op::OpVectorShuffle);
Instruction::OperandList maskOperands{{SPV_OPERAND_TYPE_ID, {gatheredId}},
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
for (const Operand& component : maskComponents(split->Components)) {
maskOperands.push_back(component);
}
read->SetInOperands(Move(maskOperands));
}
for (Instruction* query : splitSizeQueries) {
const BufferImageSplit* split = splitOf(query);
if (split == nullptr) continue;
uint32_t oneId = 0;
uint32_t componentsId = 0;
if (!resolveSplitCoordConstants(query->type_id(), oneId, componentsId)) {
return Status::SuccessWithoutChange;
}
const Instruction* resultType = defUseMgr->GetDef(query->type_id());
if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypeInt) {
return Status::SuccessWithoutChange;
}
const uint32_t rawSizeId = irContext->TakeNextId();
if (rawSizeId == 0) return Status::Failure;
Instruction::OperandList queryOperands;
for (uint32_t i = 0; i < query->NumInOperands(); ++i) {
queryOperands.push_back(query->GetInOperand(i));
}
query->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpImageQuerySize, query->type_id(), rawSizeId, queryOperands));
query->SetOpcode(resultType->GetSingleWordInOperand(1) != 0 ? spv::Op::OpSDiv
: spv::Op::OpUDiv);
query->SetInOperands({{SPV_OPERAND_TYPE_ID, {rawSizeId}},
{SPV_OPERAND_TYPE_ID, {componentsId}}});
}
for (Instruction* type : bufferSplitTypes) {
const auto splitIt = splitByTypeId.find(type->result_id());
if (splitIt == splitByTypeId.end()) continue;
// Only the format: the base format's component type is the original's by
// construction, so the Sampled Type still agrees with it.
type->SetInOperand(kImageFormatOperand, {static_cast<uint32_t>(splitIt->second.Base)});
}
// The declaration itself, last. For most carriers only the format operand moves:
// the carrier has the same component type as the original by construction, so the
// OpTypeImage's Sampled Type still agrees with it (which is what spirv-val checks)
@@ -151,6 +151,16 @@ namespace MobileGL {
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
// The core-ESSL single-channel format a non-core BUFFER image is SPLIT into, or 0
// when the format needs no split or has no core single-channel base. A buffer
// image cannot be WIDENED - its texels are the application's buffer object, which
// has no room to restride - but rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the shader reads and writes each component
// by itself at 2i and 2i+1 instead. DirectGLES asks this for glTexBuffer's
// internal format and for glBindImageTexture's, which have to name the same view
// the shader addresses.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
bool onlyFormatsSpirvCrossRefusesToPrint = false);