mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +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
|
// already calls that undefined, and inventing a carrier for it would only make the
|
||||||
// out-of-class read wider.
|
// out-of-class read wider.
|
||||||
//
|
//
|
||||||
// A BUFFER texture is excluded on both sides: it has no storage of its own to widen
|
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
|
||||||
// (its texels are the application's buffer object), so WidenImageFormatsPass declines
|
// its own to widen (its texels are the application's buffer object), so
|
||||||
// every buffer image and the bind must decline with it, or the driver would be handed
|
// WidenImageFormatsPass declines to widen every buffer image and the bind must decline
|
||||||
// a carrier the shader never addressed. See the Dim::Buffer guard there for the
|
// with it, or the driver would be handed a carrier the shader never addressed. See the
|
||||||
// 32-byte GL_RG32F measurement that pinned it.
|
// 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;
|
GLenum bindFormat = imageBinding.Format;
|
||||||
if (imageBinding.Texture->GetTarget() != TextureTarget::TextureBuffer &&
|
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
|
||||||
TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
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(
|
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
|
||||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||||
if (boundFormatWidening) {
|
if (boundFormatWidening) {
|
||||||
|
|||||||
@@ -3822,6 +3822,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLenum glInternalFormat, glType, glFormat;
|
GLenum glInternalFormat, glType, glFormat;
|
||||||
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
|
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
|
||||||
&glType, TextureTarget::TextureBuffer);
|
&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) {
|
if (needsRegeneration) {
|
||||||
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
|
// 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;
|
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 TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
String ProcessOutColorLocations(const String& glslCode) {
|
String ProcessOutColorLocations(const String& glslCode) {
|
||||||
|
|||||||
@@ -150,6 +150,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||||
};
|
};
|
||||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
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 TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
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
|
// 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
|
// 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
|
// 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
|
// rg32f again, but as a BUFFER image. Same format, and NOT the same emulation: a buffer
|
||||||
// left alone anyway, because a buffer image's texels are the application's buffer object.
|
// 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
|
const char* const kRg32fBufferLoadStore = R"(#version 430 core
|
||||||
layout(rg32f, binding = 0) uniform imageBuffer img;
|
layout(rg32f, binding = 0) uniform imageBuffer img;
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
@@ -270,6 +271,28 @@ void main() {
|
|||||||
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
|
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
|
||||||
fragColor = texel;
|
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
|
// 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;
|
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
|
// A BUFFER image is never WIDENED, whatever its format, and the format alone cannot say so -
|
||||||
// carried exactly when it is an image2D. What makes the difference is that widening REALLOCATES
|
// rg32f is carried in an rgba32f when it is an image2D. What makes the difference is that widening
|
||||||
// the texture behind the image in the carrier, and a buffer image has no texture storage to
|
// REALLOCATES the texture behind the image in the carrier, and a buffer image has no texture
|
||||||
// reallocate: its texels are the application's buffer object, usually also a vertex, index or
|
// storage to reallocate: its texels are the application's buffer object, usually also a vertex,
|
||||||
// storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels - the
|
// index or storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels
|
||||||
// measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
|
// - 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
|
// [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.
|
// 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);
|
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
|
||||||
ASSERT_FALSE(spirv.empty());
|
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);
|
const auto types = CollectStorageImageTypes(spirv);
|
||||||
ASSERT_EQ(types.size(), 1u);
|
ASSERT_EQ(types.size(), 1u);
|
||||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
|
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
|
||||||
<< "the fixture stopped declaring the format this test is about";
|
|
||||||
|
|
||||||
// The gate says no, so the optimizer is never even run for it...
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||||
// ...and running it anyway changes nothing, which is what keeps the gate and the pass from
|
Vector<Uint32> split;
|
||||||
// disagreeing about a module.
|
ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true);
|
||||||
Vector<Uint32> widened;
|
if (!split.empty()) {
|
||||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
const auto afterTypes = CollectStorageImageTypes(split);
|
||||||
/*enableSpirvValidation=*/true);
|
ASSERT_EQ(afterTypes.size(), 1u);
|
||||||
EXPECT_TRUE(widened.empty() || widened == spirv) << "a buffer image was rewritten";
|
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
|
// The table the three layers share, from the other side: only the 32-bit component family has a
|
||||||
// reason - a widening that had simply stopped working.
|
// core single-channel base, and a two-dimensional image never takes this route.
|
||||||
const Vector<Uint32> planar = CompileFragment(kRg32fLoadStore);
|
TEST(WidenImageFormats, OnlyTheThirtyTwoBitTwoChannelFormatsSplitAsBufferImages) {
|
||||||
ASSERT_FALSE(planar.empty());
|
struct Case {
|
||||||
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(planar));
|
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) {
|
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
|
||||||
|
|||||||
@@ -1030,6 +1030,10 @@ namespace MobileGL {
|
|||||||
outSignedNormalized);
|
outSignedNormalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
|
||||||
|
return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat);
|
||||||
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||||
const std::set<String>& blockNames,
|
const std::set<String>& blockNames,
|
||||||
std::set<String>& flattenedBlockNames,
|
std::set<String>& flattenedBlockNames,
|
||||||
|
|||||||
@@ -305,6 +305,10 @@ namespace MobileGL {
|
|||||||
// component class with the ES storage.
|
// component class with the ES storage.
|
||||||
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
||||||
bool& outSignedNormalized);
|
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,
|
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
bool enableSpirvValidation = false);
|
bool enableSpirvValidation = false);
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ namespace MobileGL {
|
|||||||
|
|
||||||
// OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands.
|
// OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands.
|
||||||
// OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional.
|
// OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional.
|
||||||
|
// OpImageQuerySize in-operands: 0 image.
|
||||||
constexpr uint32_t kImageAccessImageOperand = 0;
|
constexpr uint32_t kImageAccessImageOperand = 0;
|
||||||
|
constexpr uint32_t kImageAccessCoordinateOperand = 1;
|
||||||
constexpr uint32_t kImageWriteTexelOperand = 2;
|
constexpr uint32_t kImageWriteTexelOperand = 2;
|
||||||
|
|
||||||
// The carrier of a non-core image format: a core GLSL ES format that represents
|
// 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 IsWidenableStorageImageType(const Instruction* type,
|
||||||
bool onlyFormatsSpirvCrossRefusesToPrint) {
|
bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
|
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
|
// 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] -
|
// (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
|
// 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"
|
// the application's buffer. It is SPLIT instead where its format allows
|
||||||
// failure instead, which loses the same stage but corrupts nothing.
|
// (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)) ==
|
if (static_cast<spv::Dim>(type->GetSingleWordInOperand(kImageDimOperand)) ==
|
||||||
spv::Dim::Buffer) {
|
spv::Dim::Buffer) {
|
||||||
return false;
|
return false;
|
||||||
@@ -527,6 +582,19 @@ namespace MobileGL {
|
|||||||
return GLInternalFormatOfSpirvImageFormat(widening.Carrier);
|
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,
|
bool WidenImageFormatsPass::NormalizedImageCarrierCodes(Uint glInternalFormat,
|
||||||
Uint32 (&outChannelMax)[4],
|
Uint32 (&outChannelMax)[4],
|
||||||
bool& outSignedNormalized) {
|
bool& outSignedNormalized) {
|
||||||
@@ -548,7 +616,8 @@ namespace MobileGL {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const Instruction& type : context->module()->types_values()) {
|
for (const Instruction& type : context->module()->types_values()) {
|
||||||
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
|
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint) ||
|
||||||
|
IsSplittableBufferImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,12 +639,15 @@ namespace MobileGL {
|
|||||||
// Cheap gate first: no widenable image type, and the module is handed back
|
// Cheap gate first: no widenable image type, and the module is handed back
|
||||||
// byte-identical - which is every shader but a handful.
|
// byte-identical - which is every shader but a handful.
|
||||||
std::vector<Instruction*> imageTypes;
|
std::vector<Instruction*> imageTypes;
|
||||||
|
std::vector<Instruction*> bufferSplitTypes;
|
||||||
for (Instruction& type : irContext->types_values()) {
|
for (Instruction& type : irContext->types_values()) {
|
||||||
if (IsWidenableStorageImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
|
if (IsWidenableStorageImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
|
||||||
imageTypes.push_back(&type);
|
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;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,8 +683,23 @@ namespace MobileGL {
|
|||||||
// the shader runs and quietly reads the carrier's surplus channels, which GL says
|
// 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"
|
// are 0 and 1. Refusing hands the stage back to the "no GLSL ES spelling"
|
||||||
// diagnostic instead, which at least names the failure.
|
// 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*> reads;
|
||||||
std::vector<Instruction*> writes;
|
std::vector<Instruction*> writes;
|
||||||
|
std::vector<Instruction*> splitReads;
|
||||||
|
std::vector<Instruction*> splitWrites;
|
||||||
|
std::vector<Instruction*> splitSizeQueries;
|
||||||
Bool rewritable = true;
|
Bool rewritable = true;
|
||||||
for (auto funcIt = irContext->module()->begin();
|
for (auto funcIt = irContext->module()->begin();
|
||||||
funcIt != irContext->module()->end() && rewritable; ++funcIt) {
|
funcIt != irContext->module()->end() && rewritable; ++funcIt) {
|
||||||
@@ -623,13 +710,14 @@ namespace MobileGL {
|
|||||||
case spv::Op::OpImageWrite:
|
case spv::Op::OpImageWrite:
|
||||||
case spv::Op::OpImageSparseRead:
|
case spv::Op::OpImageSparseRead:
|
||||||
case spv::Op::OpImageTexelPointer:
|
case spv::Op::OpImageTexelPointer:
|
||||||
|
case spv::Op::OpImageQuerySize:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// OpImageTexelPointer names the image VARIABLE (a pointer), the other
|
// OpImageTexelPointer names the image VARIABLE (a pointer), the others an
|
||||||
// three an image VALUE; both reach the OpTypeImage through the def's
|
// image VALUE; both reach the OpTypeImage through the def's type, one hop
|
||||||
// type, one hop further for the pointer.
|
// further for the pointer.
|
||||||
const Instruction* imageDef =
|
const Instruction* imageDef =
|
||||||
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
||||||
if (imageDef == nullptr) return;
|
if (imageDef == nullptr) return;
|
||||||
@@ -638,6 +726,19 @@ namespace MobileGL {
|
|||||||
imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) {
|
imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) {
|
||||||
imageTypeId = imageType->GetSingleWordInOperand(1);
|
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);
|
const auto widenedIt = widenedByTypeId.find(imageTypeId);
|
||||||
if (widenedIt == widenedByTypeId.end()) return;
|
if (widenedIt == widenedByTypeId.end()) return;
|
||||||
|
|
||||||
@@ -649,6 +750,11 @@ namespace MobileGL {
|
|||||||
writes.push_back(inst);
|
writes.push_back(inst);
|
||||||
return;
|
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
|
// OpImageSparseRead yields a struct rather than a plain texel vector, and
|
||||||
// OpImageTexelPointer is an image atomic - which spirv-val already
|
// OpImageTexelPointer is an image atomic - which spirv-val already
|
||||||
// restricts to r32i/r32ui/r32f, all three of them core formats that never
|
// 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;
|
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
|
// 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
|
// 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
|
// globals and keep their own def-use bookkeeping straight; the shuffles below do
|
||||||
@@ -766,6 +945,36 @@ namespace MobileGL {
|
|||||||
return Status::SuccessWithoutChange;
|
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
|
// ...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
|
// more of both: the uvec4 an OpImageRead of the carrier yields, the ivec4 the
|
||||||
@@ -1056,6 +1265,155 @@ namespace MobileGL {
|
|||||||
read->SetInOperands(Move(shuffleOperands));
|
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 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
|
// 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)
|
// 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],
|
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
||||||
bool& outSignedNormalized);
|
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(
|
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
|
||||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user