mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user