mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
700 lines
34 KiB
C++
700 lines
34 KiB
C++
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
//
|
|
// The post-transpile textual passes the DirectGLES ("Espryt") backend runs over the ESSL
|
|
// SPIRV-Cross hands it (MG_Backend/DirectGLES/Utils.cpp). No GL context and no driver: the
|
|
// passes are pure String -> String, so the shapes they have to survive can be pinned here
|
|
// instead of only on a device.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <MG_Backend/DirectGLES/Utils.h>
|
|
|
|
using namespace MobileGL;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
|
|
|
namespace {
|
|
Bool Contains(const String& haystack, const String& needle) {
|
|
return haystack.find(needle) != String::npos;
|
|
}
|
|
|
|
SizeT CountOf(const String& haystack, const String& needle) {
|
|
SizeT count = 0;
|
|
for (SizeT pos = haystack.find(needle); pos != String::npos; pos = haystack.find(needle, pos + 1)) {
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
|
} // namespace
|
|
|
|
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
|
// NonWritable+NonReadable, then clears NonReadable at the OpImageRead and NonWritable at the
|
|
// OpImageWrite, so an image the shader both reads and writes comes out carrying NEITHER
|
|
// `readonly` nor `writeonly` - which ESSL rejects for any format other than r32f/r32i/r32ui
|
|
// (GLSL ES 3.20 4.10). The device compile then fails and the draw silently binds program 0.
|
|
TEST(SplitReadWriteImageUniformsTest, ReadWriteImageIsSplitIntoAnAliasingPair) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 2, rgba8) uniform highp image2D goku;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
highp vec4 loaded = imageLoad(goku, ivec2(gl_FragCoord.xy));
|
|
imageStore(goku, ivec2(gl_FragCoord.xy), loaded + vec4(0.25));
|
|
mg_FragColor = loaded;
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
|
|
// Both halves: same binding, same format, same type - which is what makes two image
|
|
// variables on one image unit legal - and both `coherent`, which is what makes the store
|
|
// through one of them visible to the load through the other.
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
|
|
EXPECT_TRUE(Contains(
|
|
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
|
|
|
// The load keeps the original name, the store moves to the writeonly half.
|
|
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ","));
|
|
EXPECT_FALSE(Contains(out, "imageStore(goku,"));
|
|
}
|
|
|
|
// The split has to survive RemoveLayoutBinding, which runs straight after it: an ES image
|
|
// unit cannot be assigned through the API, so the layout qualifier is the only binding
|
|
// mechanism and both halves must still carry theirs afterwards.
|
|
TEST(SplitReadWriteImageUniformsTest, BothHalvesKeepTheirBindingThroughRemoveLayoutBinding) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 5, rgba8) uniform highp image2D goku;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
}
|
|
)";
|
|
const String out = RemoveLayoutBinding(SplitReadWriteImageUniforms(source));
|
|
EXPECT_EQ(CountOf(out, "binding = 5"), 2u);
|
|
}
|
|
|
|
// Cheap hardening: the pass does not depend on SPIRV-Cross getting the read-only case right,
|
|
// and a shader that only reads must not pay for a second uniform.
|
|
TEST(SplitReadWriteImageUniformsTest, ReadOnlyImageGetsReadonlyAndIsNotSplit) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 1, rgba16f) uniform highp image2DArray trunks;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = imageLoad(trunks, ivec3(0));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray trunks;"));
|
|
EXPECT_FALSE(Contains(out, "writeonly"));
|
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
|
EXPECT_EQ(CountOf(out, "image2DArray"), 1u);
|
|
}
|
|
|
|
TEST(SplitReadWriteImageUniformsTest, WriteOnlyImageGetsWriteonlyAndIsNotSplit) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 3, rgba8) uniform highp image2D gohan;
|
|
void main()
|
|
{
|
|
imageStore(gohan, ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D gohan;"));
|
|
EXPECT_FALSE(Contains(out, "readonly"));
|
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
|
}
|
|
|
|
// r32f / r32i / r32ui are exactly the formats GLSL ES 3.20 4.10 exempts from the rule, so a
|
|
// read+write image in one of them is already legal and must not be doubled.
|
|
TEST(SplitReadWriteImageUniformsTest, ExemptFormatsAreLeftCompletelyAlone) {
|
|
for (const char* format : {"r32f", "r32i", "r32ui"}) {
|
|
const String type = String(format) == "r32f" ? "image2D" : (String(format) == "r32i" ? "iimage2D" : "uimage2D");
|
|
const String source = "#version 320 es\nlayout(binding = 4, " + String(format) + ") uniform highp " + type +
|
|
" vegeta;\nvoid main()\n{\n imageStore(vegeta, ivec2(0), imageLoad(vegeta, "
|
|
"ivec2(0)));\n}\n";
|
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source) << "format " << format;
|
|
}
|
|
}
|
|
|
|
// A declaration SPIRV-Cross already qualified is none of this pass's business.
|
|
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreUntouched) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 0, rgba8) uniform readonly highp image2D reader;
|
|
layout(binding = 1, rgba8) uniform writeonly highp image2D writer;
|
|
void main()
|
|
{
|
|
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
|
|
}
|
|
)";
|
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
|
}
|
|
|
|
// The binding of an image array is the array's base; splitting must keep the array on both
|
|
// halves (dropping the subscript would silently turn 3 units into 1).
|
|
TEST(SplitReadWriteImageUniformsTest, ImageArraySplitsAndKeepsItsArraySize) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 6, rgba8) uniform highp image2D gohan[3];
|
|
void main()
|
|
{
|
|
imageStore(gohan[1], ivec2(0), imageLoad(gohan[2], ivec2(0)));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
|
|
EXPECT_TRUE(Contains(
|
|
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
|
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
|
}
|
|
|
|
// The rewrite is by identifier, not by substring: "goku" must not reach into "goku_hd", and
|
|
// the two images have to be classified independently.
|
|
TEST(SplitReadWriteImageUniformsTest, ANameThatIsAPrefixOfAnotherIsNotClobbered) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 1, rgba8) uniform highp image2D goku;
|
|
layout(binding = 2, rgba8) uniform highp image2D goku_hd;
|
|
void main()
|
|
{
|
|
highp vec4 loaded = imageLoad(goku, ivec2(0));
|
|
imageStore(goku, ivec2(0), loaded);
|
|
imageStore(goku_hd, ivec2(0), loaded);
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
|
|
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
|
|
// place, not split, and left non-coherent because nothing aliases it.
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
|
|
EXPECT_TRUE(Contains(
|
|
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
|
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
|
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
|
EXPECT_FALSE(Contains(out, WriteAlias("goku_hd")));
|
|
}
|
|
|
|
// Other qualifiers belong to both halves, and the memory qualifier goes where SPIRV-Cross
|
|
// puts it (right after `uniform`) so the image-rebinding regex in Managers.cpp still matches.
|
|
TEST(SplitReadWriteImageUniformsTest, ExistingQualifiersAreCarriedOntoBothHalves) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 2, rgba8) uniform coherent restrict highp image2D goku;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
|
EXPECT_TRUE(
|
|
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
|
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
|
|
// compile error in ESSL, so the source's own has to be recognized.
|
|
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
|
}
|
|
|
|
// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a
|
|
// same-variable read-after-write within one invocation by construction, but once the store goes
|
|
// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and
|
|
// the ordering only holds if both are coherent. Desktop sources almost never say so - they had
|
|
// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's
|
|
// store/load/compare loop started reading back the value it had not stored yet.
|
|
TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 2, rgba8) uniform highp image2D goku;
|
|
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), vec4(1.0));
|
|
mg_FragColor = imageLoad(goku, ivec2(0));
|
|
imageStore(storeOnly, ivec2(0), vec4(2.0));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
|
|
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
|
|
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
|
|
// place, has no alias to stay visible to, and must not pay for uncached access.
|
|
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
|
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
|
}
|
|
|
|
// The ORDERING half of the split, which `coherent` alone does not buy. Coherent makes the store
|
|
// through one variable VISIBLE to a load through the other; it says nothing about the order of
|
|
// the two within a single invocation, and the ES compiler - seeing a write to one variable and a
|
|
// read of another it has no reason to believe alias - is free to serve the read from before the
|
|
// write. That is what advanced-memory-order measured on Adreno with the coherent pair already in
|
|
// place. memoryBarrierImage() is the primitive that orders them.
|
|
TEST(SplitReadWriteImageUniformsTest, EverySplitStoreIsFollowedByAnImageMemoryBarrier) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 2, rgba8) uniform highp image2D goku;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), vec4(1.0));
|
|
highp vec4 first = imageLoad(goku, ivec2(0));
|
|
imageStore(goku, ivec2(0), vec4(2.0));
|
|
mg_FragColor = first + imageLoad(goku, ivec2(0));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
|
|
<< out;
|
|
// One per store, not one per shader and not one per load.
|
|
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 2u) << out;
|
|
}
|
|
|
|
// The barrier belongs to the SPLIT alone. A store-only image was repaired in place, nothing
|
|
// aliases it, and paying for a barrier there would slow down every shader that merely writes an
|
|
// image - which is most of them.
|
|
TEST(SplitReadWriteImageUniformsTest, ARepairedButUnsplitStoreGetsNoBarrier) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
|
|
void main()
|
|
{
|
|
imageStore(storeOnly, ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
|
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
|
|
}
|
|
|
|
// The store site is found by matching the call's own parentheses, not by looking for the next
|
|
// ')', so a nested call in the value argument does not truncate the statement and the barrier
|
|
// still lands after the whole thing.
|
|
TEST(SplitReadWriteImageUniformsTest, TheBarrierLandsAfterAStoreWithNestedParentheses) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 6, rgba8) uniform highp image2D gohan[3];
|
|
void main()
|
|
{
|
|
imageStore(gohan[1], ivec2(0), max(imageLoad(gohan[2], ivec2(0)), vec4(0.5)));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "max(imageLoad(gohan[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();")) << out;
|
|
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
|
|
}
|
|
|
|
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
|
|
// such a declaration legal.
|
|
TEST(SplitReadWriteImageUniformsTest, ImageSizeAloneDoesNotCountAsALoadOrAStore) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 8, rgba8ui) uniform highp uimage2D sizeOnly;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = vec4(float(imageSize(sizeOnly).x));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D sizeOnly;"));
|
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
|
}
|
|
|
|
// The alias must not land on an identifier the shader already uses.
|
|
TEST(SplitReadWriteImageUniformsTest, AliasNameAvoidsAnExistingIdentifier) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 6, rgba8) uniform highp image2D taken;
|
|
highp vec4 mg_imageWrite_taken;
|
|
void main()
|
|
{
|
|
imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + mg_imageWrite_taken);
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_FALSE(Contains(out, "image2D " + WriteAlias("taken") + ";"));
|
|
EXPECT_TRUE(Contains(out, "image2D " + WriteAlias("taken") + "X;"));
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("taken") + "X,"));
|
|
EXPECT_TRUE(Contains(out, "+ mg_imageWrite_taken)"));
|
|
}
|
|
|
|
// A use the pass cannot account for (here: the image handed to a user function) means it
|
|
// cannot know every store site, so it declines rather than emitting a half-rewritten shader.
|
|
TEST(SplitReadWriteImageUniformsTest, AnUnrecognizedUseLeavesTheDeclarationAlone) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 2, rgba8) uniform highp image2D passed;
|
|
highp vec4 helper(highp image2D img) { return imageLoad(img, ivec2(0)); }
|
|
void main()
|
|
{
|
|
imageStore(passed, ivec2(0), helper(passed));
|
|
}
|
|
)";
|
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
|
}
|
|
|
|
TEST(SplitReadWriteImageUniformsTest, ShaderWithoutImagesIsReturnedUnchanged) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 0) uniform highp sampler2D goku;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = texture(goku, vec2(0.5));
|
|
}
|
|
)";
|
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// RetargetTextureBufferExtension
|
|
//
|
|
// Buffer textures are core in the OpenGL 3.1+ context MobileGL advertises, but in ES they
|
|
// only became core in 3.2; below that they need EXT_texture_buffer or OES_texture_buffer.
|
|
// SPIRV-Cross hardcodes the EXT spelling for every Dim=Buffer image it emits below ESSL 320
|
|
// and offers no way to ask for the other one, so on a driver that advertises only the OES
|
|
// name the `: require` is a hard compile error over a single token.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
using Tier = MobileGL::MG_External::GLESCapabilities::TextureBufferTier;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RetargetTextureBufferExtension;
|
|
|
|
namespace {
|
|
// What SPIRV-Cross actually emits for `uniform isamplerBuffer CloudFaces;` at ESSL 310 -
|
|
// the shape that empties Minecraft 26.3's cloud layer on a driver without the extension.
|
|
const String kBufferTextureShader = R"(#version 310 es
|
|
#extension GL_EXT_texture_buffer : require
|
|
precision highp float;
|
|
uniform highp isamplerBuffer CloudFaces;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = vec4(texelFetch(CloudFaces, gl_VertexID).r);
|
|
}
|
|
)";
|
|
} // namespace
|
|
|
|
TEST(RetargetTextureBufferExtensionTest, OesOnlyDriverGetsTheOesDirective) {
|
|
const String out = RetargetTextureBufferExtension(kBufferTextureShader, Tier::ExtensionOES);
|
|
EXPECT_TRUE(Contains(out, "#extension GL_OES_texture_buffer : require"))
|
|
<< "the OES driver's own spelling must reach the directive:\n" << out;
|
|
EXPECT_FALSE(Contains(out, "GL_EXT_texture_buffer"))
|
|
<< "the EXT spelling this driver does not advertise must be gone:\n" << out;
|
|
// Only the directive changes; the declaration and the fetch are identical between the two
|
|
// extensions and must not be touched.
|
|
EXPECT_TRUE(Contains(out, "uniform highp isamplerBuffer CloudFaces;"));
|
|
EXPECT_TRUE(Contains(out, "texelFetch(CloudFaces, gl_VertexID)"));
|
|
}
|
|
|
|
TEST(RetargetTextureBufferExtensionTest, ExtDriverKeepsWhatSpirvCrossEmitted) {
|
|
EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::ExtensionEXT),
|
|
kBufferTextureShader);
|
|
}
|
|
|
|
// ES 3.2 needs no directive at all, and SPIRV-Cross emits none at ESSL 320 - but a shader
|
|
// that arrived with one anyway must not be rewritten to a name the pass was not asked for.
|
|
TEST(RetargetTextureBufferExtensionTest, CoreAndUnsupportedTiersAreNoOps) {
|
|
EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::CoreEs32),
|
|
kBufferTextureShader);
|
|
EXPECT_EQ(RetargetTextureBufferExtension(kBufferTextureShader, Tier::None),
|
|
kBufferTextureShader);
|
|
}
|
|
|
|
// The name is only the subject of a rewrite where it is the subject of an #extension
|
|
// directive. A shader that merely mentions it - in a comment SPIRV-Cross carried through, or
|
|
// in an identifier - is not an extension request and must come out byte-identical.
|
|
TEST(RetargetTextureBufferExtensionTest, OnlyExtensionDirectivesAreRewritten) {
|
|
const String source = R"(#version 310 es
|
|
// GL_EXT_texture_buffer is what this shader would need
|
|
precision highp float;
|
|
uniform highp float GL_EXT_texture_buffer_lookalike;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = vec4(GL_EXT_texture_buffer_lookalike);
|
|
}
|
|
)";
|
|
EXPECT_EQ(RetargetTextureBufferExtension(source, Tier::ExtensionOES), source);
|
|
}
|
|
|
|
// The dangerous collision, and the one the directive check alone does NOT catch:
|
|
// GL_EXT_texture_buffer is a strict prefix of GL_EXT_texture_buffer_object, a different and
|
|
// real extension that SPIRV-Cross emits from the same Dim=Buffer branch on its legacy-desktop
|
|
// path. Rewriting it would turn a valid request into one for a GL_OES_texture_buffer_object
|
|
// that does not exist. Only an identifier-boundary check saves this, so it gets its own test
|
|
// with the lookalike on a genuine #extension line.
|
|
TEST(RetargetTextureBufferExtensionTest, ALongerExtensionSharingThePrefixIsNotRewritten) {
|
|
const String source = R"(#version 310 es
|
|
#extension GL_EXT_texture_buffer_object : require
|
|
precision highp float;
|
|
void main() {}
|
|
)";
|
|
EXPECT_EQ(RetargetTextureBufferExtension(source, Tier::ExtensionOES), source);
|
|
|
|
// And when both appear, exactly the exact-match one moves.
|
|
const String mixed = R"(#version 310 es
|
|
#extension GL_EXT_texture_buffer_object : require
|
|
#extension GL_EXT_texture_buffer : require
|
|
precision highp float;
|
|
void main() {}
|
|
)";
|
|
const String out = RetargetTextureBufferExtension(mixed, Tier::ExtensionOES);
|
|
EXPECT_TRUE(Contains(out, "#extension GL_EXT_texture_buffer_object : require")) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_OES_texture_buffer : require")) << out;
|
|
EXPECT_EQ(CountOf(out, "GL_OES_texture_buffer_object"), 0u) << out;
|
|
}
|
|
|
|
// Whitespace between '#' and the keyword is legal in GLSL, and a shader carrying several
|
|
// extension directives must have exactly the one retargeted.
|
|
TEST(RetargetTextureBufferExtensionTest, SpacedDirectiveIsRewrittenAndNeighboursAreLeftAlone) {
|
|
const String source = R"(#version 310 es
|
|
# extension GL_EXT_texture_buffer : require
|
|
#extension GL_EXT_shader_io_blocks : require
|
|
precision highp float;
|
|
void main() {}
|
|
)";
|
|
const String out = RetargetTextureBufferExtension(source, Tier::ExtensionOES);
|
|
EXPECT_TRUE(Contains(out, "# extension GL_OES_texture_buffer : require")) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_EXT_shader_io_blocks : require"))
|
|
<< "an unrelated extension must survive untouched:\n" << out;
|
|
EXPECT_EQ(CountOf(out, "GL_OES_texture_buffer"), 1u);
|
|
}
|
|
|
|
// Interpolation is only ever consumed at a fragment input, but an ES linker still compares the
|
|
// two sides of EVERY stage interface and rejects a program whose producer says `flat` and whose
|
|
// consumer does not. SPIRV-Cross prints `flat` on a vertex output and a geometry input of
|
|
// integer type and on nothing else, so a program with tessellation in the middle came out
|
|
// mismatched at both ends of the tessellator - "output vs_tcs_result interpolation mismatch
|
|
// with other stage" on Adreno, and a program that fails to link is a draw that paints nothing.
|
|
TEST(ForceFlatIntegerVaryingsTest, TessellationStagesGetTheQualifierOnBothSides) {
|
|
const String tessControl = R"(#version 320 es
|
|
layout(vertices = 1) out;
|
|
layout(location = 0) in uint vs_tcs_result[];
|
|
layout(location = 0) out uint tcs_tes_result[1];
|
|
void main() { tcs_tes_result[gl_InvocationID] = vs_tcs_result[gl_InvocationID]; }
|
|
)";
|
|
const String control = ForceFlatIntegerVaryings(tessControl, GL_TESS_CONTROL_SHADER);
|
|
EXPECT_TRUE(Contains(control, "layout(location = 0) flat in uint vs_tcs_result[];")) << control;
|
|
EXPECT_TRUE(Contains(control, "layout(location = 0) flat out uint tcs_tes_result[1];")) << control;
|
|
|
|
const String tessEval = R"(#version 320 es
|
|
layout(isolines, point_mode) in;
|
|
layout(location = 0) in uint tcs_tes_result[];
|
|
layout(location = 0) out uint tes_gs_result;
|
|
void main() { tes_gs_result = tcs_tes_result[0]; }
|
|
)";
|
|
const String eval = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
|
|
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat in uint tcs_tes_result[];")) << eval;
|
|
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat out uint tes_gs_result;")) << eval;
|
|
}
|
|
|
|
// The two ends the tessellation stages have to meet: what a vertex shader and a geometry shader
|
|
// already emitted before this pass learned about tessellation at all. Pinned here so the two
|
|
// sides cannot drift apart again.
|
|
TEST(ForceFlatIntegerVaryingsTest, TheStagesAroundTessellationAreUnchanged) {
|
|
const String vertex = R"(#version 320 es
|
|
layout(location = 0) out uint vs_tcs_result;
|
|
void main() { vs_tcs_result = 1u; }
|
|
)";
|
|
EXPECT_TRUE(Contains(ForceFlatIntegerVaryings(vertex, GL_VERTEX_SHADER),
|
|
"layout(location = 0) flat out uint vs_tcs_result;"));
|
|
|
|
const String geometry = R"(#version 320 es
|
|
layout(points) in;
|
|
layout(triangle_strip, max_vertices = 4) out;
|
|
layout(location = 0) in uint tes_gs_result[1];
|
|
layout(location = 0) out uint gs_fs_result;
|
|
void main() { gs_fs_result = tes_gs_result[0]; EmitVertex(); }
|
|
)";
|
|
const String gs = ForceFlatIntegerVaryings(geometry, GL_GEOMETRY_SHADER);
|
|
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat in uint tes_gs_result[1];")) << gs;
|
|
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat out uint gs_fs_result;")) << gs;
|
|
}
|
|
|
|
// Non-integer interfaces keep whatever interpolation they were given: adding `flat` to a float
|
|
// varying would turn a smoothly interpolated value into a per-provoking-vertex constant, which
|
|
// is a rendering change, not a linker one.
|
|
TEST(ForceFlatIntegerVaryingsTest, FloatVaryingsAreNotTouched) {
|
|
const String tessEval = R"(#version 320 es
|
|
layout(isolines, point_mode) in;
|
|
layout(location = 1) in vec2 tcs_tes_coord[];
|
|
layout(location = 1) out vec2 tes_gs_coord;
|
|
void main() { tes_gs_coord = tcs_tes_coord[0]; }
|
|
)";
|
|
const String out = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
|
|
EXPECT_TRUE(Contains(out, "layout(location = 1) in vec2 tcs_tes_coord[];")) << out;
|
|
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
|
|
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
|
|
}
|
|
|
|
// --- image format qualifier completion ---------------------------------------------------------
|
|
//
|
|
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
|
|
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
|
|
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
|
|
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
|
|
|
|
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
|
|
// with GL_R8UI bound to its unit.
|
|
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
|
)";
|
|
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
|
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
|
|
}
|
|
|
|
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
|
|
// exactly the reason this pass exists.
|
|
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
|
|
const String source = R"(#version 320 es
|
|
uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
|
|
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
|
|
}
|
|
|
|
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
|
|
// puts a declared image in the map, and the pass must not depend on that being true.
|
|
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
|
EXPECT_EQ(out, source) << out;
|
|
}
|
|
|
|
// Only the named uniform. A second image in the same shader - format-less because the pass
|
|
// declined it, or because its unit holds nothing - must be left exactly as it is.
|
|
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 0) uniform writeonly highp uimage2D named;
|
|
layout(binding = 1) uniform writeonly highp uimage2D other;
|
|
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
|
|
)";
|
|
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
|
|
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
|
|
}
|
|
|
|
// The format the pass writes has to survive the two passes that run after it, or nothing was
|
|
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
|
|
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
|
out = SplitReadWriteImageUniforms(out);
|
|
out = RemoveLayoutBinding(out);
|
|
EXPECT_TRUE(Contains(out, "r8ui")) << out;
|
|
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
|
|
}
|
|
|
|
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
|
|
const String withImage = R"(#version 320 es
|
|
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
|
|
|
|
const String withoutImage = R"(#version 320 es
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main() { mg_FragColor = vec4(1.0); }
|
|
)";
|
|
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
|
|
}
|
|
|
|
// --- GL_NV_image_formats directive --------------------------------------------------------------
|
|
|
|
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
|
const String source = R"(#version 320 es
|
|
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
const String out = RequestExtendedImageFormats(source, true);
|
|
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
|
|
}
|
|
|
|
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
|
|
// compile error, so the caller's "not needed" answer has to be honoured exactly.
|
|
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
|
|
}
|
|
|
|
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
|
const String source = R"(#version 320 es
|
|
#extension GL_NV_image_formats : require
|
|
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
const String out = RequestExtendedImageFormats(source, true);
|
|
EXPECT_EQ(out, source);
|
|
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
|
|
}
|
|
|
|
// --- GL_OES_viewport_array directive -------------------------------------------------------------
|
|
|
|
// SPIRV-Cross prints gl_ViewportIndex bare and requests nothing for it, and ESSL has no core
|
|
// spelling at any version - so without this directive the stage fails to compile, the program is
|
|
// marked unusable and every draw made with it silently renders nothing.
|
|
TEST(RequestViewportArrayExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
|
const String source = R"(#version 320 es
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
)";
|
|
const String out = RequestViewportArrayExtension(source, true);
|
|
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_OES_viewport_array : require\n")) << out;
|
|
}
|
|
|
|
// Never speculatively: ARM's compiler hard-errors on an `#extension` naming a string the driver
|
|
// does not advertise, so the caller's "not needed" answer has to be honoured exactly. A driver
|
|
// without the extension gets the LowerViewportIndexPass fallback instead.
|
|
TEST(RequestViewportArrayExtensionTest, NotNeededMeansNotEmitted) {
|
|
const String source = R"(#version 320 es
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
)";
|
|
EXPECT_EQ(RequestViewportArrayExtension(source, false), source);
|
|
}
|
|
|
|
TEST(RequestViewportArrayExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
|
const String source = R"(#version 320 es
|
|
#extension GL_OES_viewport_array : require
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
)";
|
|
const String out = RequestViewportArrayExtension(source, true);
|
|
EXPECT_EQ(out, source);
|
|
EXPECT_EQ(CountOf(out, "GL_OES_viewport_array"), 1u) << out;
|
|
}
|
|
|
|
// The two image directives and this one share the insertion point, so a shader that needs both
|
|
// must end up with both - and with #version still first.
|
|
TEST(RequestViewportArrayExtensionTest, CoexistsWithTheImageFormatDirective) {
|
|
const String source = R"(#version 320 es
|
|
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
)";
|
|
const String out = RequestViewportArrayExtension(RequestExtendedImageFormats(source, true), true);
|
|
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
|
}
|