From 5e29e7d266c8e18e8f663c10fe1d91b4c8eb41a6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 04:30:06 -0400 Subject: [PATCH] [Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 27 +- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 13 + MobileGL/MG_Backend/DirectGLES/Utils.cpp | 17 + MobileGL/MG_Backend/DirectGLES/Utils.h | 19 + .../Scenarios/NonCoreImageFormatScenario.cpp | 87 ++++ .../WidenImageFormatsTest.cpp | 189 +++++++-- .../ShaderTranspiler/ShaderCompiler.cpp | 4 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 4 + .../SpirvPasses/WidenImageFormatsPass.cpp | 372 +++++++++++++++++- .../SpirvPasses/WidenImageFormatsPass.h | 10 + 10 files changed, 705 insertions(+), 37 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 69fb6772..ed802a5f 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index e2456b9b..cb579db3 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index c0f0a84e..ff2f8681 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -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( + 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) { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 8a214048..7c5a34fe 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp index ddd8f5fa..4a807fb6 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp @@ -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 seed(static_cast(kBufferTexels) * 2u, -1.0f); + + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_TEXTURE_BUFFER, buffer); + glBufferData(GL_TEXTURE_BUFFER, static_cast(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 readback(seed.size(), -12345.0f); + glBindBuffer(GL_TEXTURE_BUFFER, buffer); + glGetBufferSubData(GL_TEXTURE_BUFFER, 0, + static_cast(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(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 diff --git a/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp index 3a604acf..7b92135d 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp @@ -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 spirv = CompileFragment(kRg32fBufferLoadStore); ASSERT_FALSE(spirv.empty()); + const auto beforeTypes = CollectStorageImageTypes(spirv); + ASSERT_EQ(beforeTypes.size(), 1u); + EXPECT_EQ(beforeTypes.front().format, static_cast(spv::ImageFormat::Rg32f)) + << "the fixture stopped declaring the format this test is about"; + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector 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(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 spirv = CompileFragment(kRg32fBufferSize); + ASSERT_FALSE(spirv.empty()); + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector 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 spirv = CompileFragment(kRg16fBufferLoadStore); + ASSERT_FALSE(spirv.empty()); + const auto types = CollectStorageImageTypes(spirv); ASSERT_EQ(types.size(), 1u); - EXPECT_EQ(types.front().format, static_cast(spv::ImageFormat::Rg32f)) - << "the fixture stopped declaring the format this test is about"; + EXPECT_EQ(types.front().format, static_cast(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 widened; - ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false, - /*enableSpirvValidation=*/true); - EXPECT_TRUE(widened.empty() || widened == spirv) << "a buffer image was rewritten"; + Vector 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(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 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) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 142bfd69..04418a84 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1030,6 +1030,10 @@ namespace MobileGL { outSignedNormalized); } + Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) { + return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat); + } + bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, const std::set& blockNames, std::set& flattenedBlockNames, diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index a95125d1..f1a5ee6e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -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& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp index c8770d70..6f194a5c 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp @@ -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(type->GetSingleWordInOperand(kImageDimOperand)) != + spv::Dim::Buffer) { + return false; + } + const auto format = + static_cast(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(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(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 imageTypes; + std::vector 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 splitByTypeId; + for (Instruction* type : bufferSplitTypes) { + splitByTypeId.emplace( + type->result_id(), + SplitOfBufferImageFormat( + static_cast(type->GetSingleWordInOperand(kImageFormatOperand)))); + } + std::vector reads; std::vector writes; + std::vector splitReads; + std::vector splitWrites; + std::vector 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> 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( + irContext, spv::Op::OpIMul, coord->type_id(), firstId, + Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {coordId}}, + {SPV_OPERAND_TYPE_ID, {componentsId}}})); + before->InsertBefore(spvtools::MakeUnique( + 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* 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( + 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( + 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( + 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( + 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( + 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(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) diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h index aeee3f1f..eac216cb 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h @@ -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);