mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
1520 lines
77 KiB
C++
1520 lines
77 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>
|
|
|
|
#include <limits>
|
|
|
|
using namespace MobileGL;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_ARRAY_ELEMENT_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_READONLY_ALIAS_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_SPLIT_READ_ALIAS_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::PointSizeExtensionName;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestPointSizeExtension;
|
|
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;
|
|
}
|
|
|
|
// The pass tags the name of every declaration it rewrites with the REPAIR it applied, so the
|
|
// expectations have to spell the tag that matches how the fixture uses the image.
|
|
String RoAlias(const String& name) { return String(IMAGE_READONLY_ALIAS_PREFIX) + name; }
|
|
String WoAlias(const String& name) { return String(IMAGE_WRITEONLY_ALIAS_PREFIX) + name; }
|
|
String RwAlias(const String& name) { return String(IMAGE_SPLIT_READ_ALIAS_PREFIX) + name; }
|
|
// The writeonly half is minted from the ALREADY access-tagged name, so it carries both.
|
|
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
|
String SplitWriteAlias(const String& name) { return WriteAlias(RwAlias(name)); }
|
|
// The scalar RemapImageArrayElementUnits declares for one element of a split image array.
|
|
String Elem(const String& name, Int element) {
|
|
return String(IMAGE_ARRAY_ELEMENT_PREFIX) + name + "_" + std::to_string(element);
|
|
}
|
|
} // 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 " +
|
|
RwAlias("goku") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " +
|
|
SplitWriteAlias("goku") + ";"))
|
|
<< out;
|
|
|
|
// The load goes to the readonly half, the store to the writeonly one, and neither is called
|
|
// what the application called it any more.
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ","));
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ","));
|
|
EXPECT_FALSE(Contains(out, "imageStore(goku,"));
|
|
EXPECT_FALSE(Contains(out, "imageLoad(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 " +
|
|
RoAlias("trunks") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("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 " + WoAlias("gohan") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("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 needs no REPAIR - but it still needs the rename.
|
|
// The input to this pass is SPIRV-Cross output, not application source, and SPIRV-Cross picks
|
|
// `readonly` or `writeonly` from the accesses of the stage it is emitting, so "already qualified"
|
|
// says nothing about whether the other stages spell it the same way. The qualifiers must survive
|
|
// untouched; only the identifier changes.
|
|
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreRenamedButNotRequalified) {
|
|
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)));
|
|
}
|
|
)";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 0, rgba8) uniform readonly highp image2D " +
|
|
RoAlias("reader") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " +
|
|
WoAlias("writer") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("writer") + ",")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("reader") + ",")) << out;
|
|
// Neither declaration is doubled and neither gains a qualifier it did not have: this is a
|
|
// rename, not a repair.
|
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX)) << out;
|
|
EXPECT_EQ(CountOf(out, "coherent"), 0u) << out;
|
|
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
|
|
}
|
|
|
|
// A declaration carrying BOTH qualifiers is a spelling no per-stage access analysis produces, so
|
|
// it came from the application and reads the same in every stage. Nothing to rename.
|
|
TEST(SplitReadWriteImageUniformsTest, ADeclarationQualifiedBothWaysIsLeftCompletelyAlone) {
|
|
const String source = R"(#version 320 es
|
|
layout(binding = 0, rgba8) uniform readonly writeonly highp image2D inert;
|
|
void main()
|
|
{
|
|
highp ivec2 size = imageSize(inert);
|
|
if (size.x < 0) discard;
|
|
}
|
|
)";
|
|
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 " +
|
|
RwAlias("gohan") + "[3];"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " +
|
|
SplitWriteAlias("gohan") + "[3];"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("gohan") + "[1],"));
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("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. Both are renamed.
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D " +
|
|
RwAlias("goku") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " +
|
|
SplitWriteAlias("goku") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " +
|
|
WoAlias("goku_hd") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("goku_hd") + ","));
|
|
EXPECT_FALSE(Contains(out, SplitWriteAlias("goku") + "_hd"));
|
|
EXPECT_FALSE(Contains(out, SplitWriteAlias("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 " + RwAlias("goku") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(
|
|
Contains(out, "uniform writeonly coherent restrict highp image2D " + SplitWriteAlias("goku") + ";"))
|
|
<< out;
|
|
// ...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 " + RwAlias("goku") + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("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 " + WoAlias("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(" + SplitWriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
|
<< out;
|
|
EXPECT_TRUE(
|
|
Contains(out, "imageStore(" + SplitWriteAlias("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 " + WoAlias("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(" + RwAlias("gohan") +
|
|
"[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();"))
|
|
<< out;
|
|
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
|
|
}
|
|
|
|
// The split is the one thing that makes a stage declare MORE image uniforms than the application
|
|
// did, and MobileGL keeps advertising GL_MAX_*_IMAGE_UNIFORMS unadjusted (lowering it would fail
|
|
// basic-api and NotSupported-out every case that only uses readonly/writeonly images). So the
|
|
// count has to be reportable, or a link failure caused by the doubling looks like a driver
|
|
// mystery - which is what KHR-GL4x.shader_image_load_store.multiple-uniforms will hit the moment
|
|
// the format work stops masking it.
|
|
TEST(SplitReadWriteImageUniformsTest, TheSplitCountIsReportedToTheCaller) {
|
|
const String twoSplits = R"(#version 320 es
|
|
layout(binding = 0, rgba8) uniform highp image2D goku;
|
|
layout(binding = 1, rgba16f) uniform highp image2D gohan;
|
|
layout(binding = 2, rgba8) uniform highp image2D storeOnly;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
imageStore(gohan, ivec2(0), imageLoad(gohan, ivec2(0)));
|
|
imageStore(storeOnly, ivec2(0), vec4(0.0));
|
|
}
|
|
)";
|
|
Uint splitCount = 99u;
|
|
SplitReadWriteImageUniforms(twoSplits, &splitCount);
|
|
EXPECT_EQ(splitCount, 2u) << "only the read+write pair counts; the store-only repair adds no uniform";
|
|
|
|
// Every early return has to write the count too, or a caller reads whatever was there before.
|
|
const String noImages = R"(#version 320 es
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = vec4(1.0);
|
|
}
|
|
)";
|
|
splitCount = 99u;
|
|
SplitReadWriteImageUniforms(noImages, &splitCount);
|
|
EXPECT_EQ(splitCount, 0u);
|
|
}
|
|
|
|
// 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 " +
|
|
RoAlias("sizeOnly") + ";"))
|
|
<< out;
|
|
// The rename has to reach imageSize too, or the declaration and its only use stop agreeing.
|
|
EXPECT_TRUE(Contains(out, "imageSize(" + RoAlias("sizeOnly") + ")")) << out;
|
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
|
}
|
|
|
|
// Neither minted name may land on an identifier the shader already uses - and there are two of
|
|
// them now, the access-tagged name of the repaired declaration and the writeonly half built on
|
|
// top of it. Both collisions are exercised at once.
|
|
TEST(SplitReadWriteImageUniformsTest, AliasNamesAvoidExistingIdentifiers) {
|
|
const String stageCollision = RwAlias("taken");
|
|
const String writeCollision = SplitWriteAlias("taken");
|
|
const String source = "#version 320 es\n"
|
|
"layout(binding = 6, rgba8) uniform highp image2D taken;\n"
|
|
"highp vec4 " +
|
|
stageCollision + ";\nhighp vec4 " + writeCollision +
|
|
";\nvoid main()\n{\n"
|
|
" imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + " +
|
|
stageCollision + " + " + writeCollision + ");\n}\n";
|
|
const String out = SplitReadWriteImageUniforms(source);
|
|
|
|
EXPECT_FALSE(Contains(out, "image2D " + stageCollision + ";")) << out;
|
|
EXPECT_FALSE(Contains(out, "image2D " + writeCollision + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "image2D " + stageCollision + "X;")) << out;
|
|
EXPECT_TRUE(Contains(out, "image2D " + writeCollision + "X;")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + writeCollision + "X,")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + stageCollision + "X,")) << out;
|
|
// ...and the globals that forced the suffix are still themselves.
|
|
EXPECT_TRUE(Contains(out, "highp vec4 " + stageCollision + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "highp vec4 " + writeCollision + ";")) << out;
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
)";
|
|
// Declining means declining EVERYTHING: no qualifier, and no rename either. A rename that
|
|
// moved the declaration but not the use inside helper() would be a compile error rather than
|
|
// the wrong-but-compiling shader this pass refuses to guess at.
|
|
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);
|
|
}
|
|
|
|
// The defect the rename exists for. The pass sees ONE stage at a time and picks the memory
|
|
// qualifier from the accesses in THAT stage, so a vertex shader that only stores and a fragment
|
|
// shader that only loads the same image came out `writeonly g_image` and `readonly g_image` -
|
|
// two declarations of one uniform name that GLSL requires to be identical. Adreno merges them
|
|
// and silently discards the vertex-stage stores (advanced-memory-dependentInvocation reads back
|
|
// the untouched zeros, with LINK_STATUS = 1 and an empty driver log). Tagging by the repair
|
|
// leaves nothing to merge.
|
|
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageDifferentlyGetDifferentNames) {
|
|
const String vertexSource = R"(#version 320 es
|
|
layout(binding = 0, rgba32f) uniform coherent highp image2D g_image;
|
|
void main()
|
|
{
|
|
imageStore(g_image, ivec2(0), vec4(1.0));
|
|
gl_Position = vec4(0.0);
|
|
}
|
|
)";
|
|
const String fragmentSource = R"(#version 320 es
|
|
layout(binding = 0, rgba32f) uniform coherent highp image2D g_image;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = imageLoad(g_image, ivec2(0));
|
|
}
|
|
)";
|
|
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
|
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
|
|
|
const String vsName = WoAlias("g_image");
|
|
const String fsName = RoAlias("g_image");
|
|
EXPECT_NE(vsName, fsName);
|
|
EXPECT_TRUE(Contains(vsOut, "uniform writeonly coherent highp image2D " + vsName + ";")) << vsOut;
|
|
EXPECT_TRUE(Contains(fsOut, "uniform readonly coherent highp image2D " + fsName + ";")) << fsOut;
|
|
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
|
|
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
|
|
// The whole point: after the rewrite the two stages no longer declare a common name, so
|
|
// there is nothing for a linker to merge and mis-qualify.
|
|
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
|
|
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
|
|
// Both bindings are untouched - the image unit is still the same one.
|
|
EXPECT_TRUE(Contains(vsOut, "binding = 0"));
|
|
EXPECT_TRUE(Contains(fsOut, "binding = 0"));
|
|
}
|
|
|
|
// The same defect, in the shape it actually reaches the driver in. SPIRV-Cross emits the access
|
|
// qualifier ITSELF whenever the stage only loads or only stores, so the declaration arrives here
|
|
// already legal - and this pass used to skip it on exactly that ground, leaving the vertex stage's
|
|
// `coherent writeonly g_image` and the fragment stage's `coherent readonly g_image` sharing one
|
|
// name. That is the pair a raw-ES probe on the Adreno 830 reproduces with no MobileGL in the
|
|
// process: the fragment stage reads back the untouched zeros
|
|
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation's [1,0,0,0.2]), and
|
|
// renaming either half fixes it. This is the emitted text of that test, verbatim.
|
|
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedDifferentlyGetDifferentNames) {
|
|
const String vertexSource = R"(#version 320 es
|
|
layout(binding = 1, rgba32f) uniform coherent writeonly highp image2D g_image;
|
|
void main()
|
|
{
|
|
imageStore(g_image, ivec2(0), vec4(2.0));
|
|
gl_Position = vec4(0.0);
|
|
}
|
|
)";
|
|
const String fragmentSource = R"(#version 320 es
|
|
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = imageLoad(g_image, ivec2(0));
|
|
}
|
|
)";
|
|
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
|
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
|
|
|
const String vsName = WoAlias("g_image");
|
|
const String fsName = RoAlias("g_image");
|
|
EXPECT_NE(vsName, fsName);
|
|
EXPECT_TRUE(Contains(vsOut, "uniform coherent writeonly highp image2D " + vsName + ";")) << vsOut;
|
|
EXPECT_TRUE(Contains(fsOut, "uniform coherent readonly highp image2D " + fsName + ";")) << fsOut;
|
|
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
|
|
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
|
|
// Nothing left for a linker to merge and mis-qualify...
|
|
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
|
|
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
|
|
// ...and the image unit is still the one the application asked for.
|
|
EXPECT_TRUE(Contains(vsOut, "binding = 1")) << vsOut;
|
|
EXPECT_TRUE(Contains(fsOut, "binding = 1")) << fsOut;
|
|
}
|
|
|
|
// ...and the budget half of it: two stages SPIRV-Cross qualified the SAME way must still land on
|
|
// one shared name, or every stage that names the image spends an image location of its own.
|
|
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedAlikeShareOneName) {
|
|
const String stage = R"(#version 320 es
|
|
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
mg_FragColor = imageLoad(g_image, ivec2(0));
|
|
}
|
|
)";
|
|
const String first = SplitReadWriteImageUniforms(stage);
|
|
const String second = SplitReadWriteImageUniforms(stage);
|
|
EXPECT_EQ(first, second);
|
|
EXPECT_TRUE(Contains(first, "uniform coherent readonly highp image2D " + RoAlias("g_image") + ";"))
|
|
<< first;
|
|
}
|
|
|
|
// The other side of that coin, and the one a per-STAGE tag got wrong. Two stages that use the
|
|
// image the same way emit byte-identical declarations, so they must arrive at ONE shared name:
|
|
// Adreno allocates an image LOCATION per distinct uniform, and giving each stage its own name
|
|
// multiplied a program's image-uniform count by the number of stages that mention it - which is
|
|
// how the five stages of KHR-GL43.shading_language_420pack.binding_images_texture_type_* went
|
|
// from 6 image uniforms to 30 and drew "Error: Image Image location or component exceeds max
|
|
// allowed." out of the Adreno 830 linker, with LINK_STATUS = TRUE already published by the
|
|
// frontend and every draw silently doing nothing.
|
|
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageAlikeShareOneName) {
|
|
const String vertexSource = R"(#version 320 es
|
|
layout(binding = 1, rgba8) uniform highp image2D goku;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
gl_Position = vec4(0.0);
|
|
}
|
|
)";
|
|
const String fragmentSource = R"(#version 320 es
|
|
layout(binding = 1, rgba8) uniform highp image2D goku;
|
|
layout(location = 0) out highp vec4 mg_FragColor;
|
|
void main()
|
|
{
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
mg_FragColor = vec4(0.0);
|
|
}
|
|
)";
|
|
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
|
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
|
|
|
// One name, arrived at independently by two different stages, so the linker merges them
|
|
// back into the single image uniform the application declared.
|
|
for (const String& out : {vsOut, fsOut}) {
|
|
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("goku") + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ",")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ",")) << out;
|
|
}
|
|
}
|
|
|
|
// One tag per repair, all three distinct, and each a legal identifier stem.
|
|
TEST(SplitReadWriteImageUniformsTest, EveryAccessTagIsDistinct) {
|
|
const String prefixes[] = {String(IMAGE_READONLY_ALIAS_PREFIX), String(IMAGE_WRITEONLY_ALIAS_PREFIX),
|
|
String(IMAGE_SPLIT_READ_ALIAS_PREFIX), String(IMAGE_WRITE_ALIAS_PREFIX)};
|
|
Vector<String> seenPrefixes;
|
|
for (const String& prefix : prefixes) {
|
|
// A GLSL identifier may not contain "__" (GLSL ES 3.20 3.7), and the prefix is glued
|
|
// straight onto a name that may itself start with '_'.
|
|
EXPECT_EQ(prefix.find("__"), String::npos) << prefix;
|
|
for (const String& seen : seenPrefixes) {
|
|
EXPECT_NE(seen, prefix) << prefix;
|
|
// Nor may one be a prefix of another: the write half is minted on top of an
|
|
// already-tagged name, so a shared stem would let two repairs collide.
|
|
EXPECT_NE(prefix.rfind(seen, 0), 0u) << prefix << " vs " << seen;
|
|
}
|
|
seenPrefixes.push_back(prefix);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// RemapImageArrayElementUnits
|
|
//
|
|
// ES takes an image unit only from layout(binding=N), and one declaration carries one of them,
|
|
// so an image array's elements land on N, N+1, N+2, ... Desktop GL lets an application point
|
|
// each element wherever it likes with glUniform1i, which ES makes an INVALID_OPERATION on an
|
|
// image uniform - there is no API side to fix, so the emitted text has to carry it.
|
|
|
|
namespace {
|
|
// The advanced-sso-simple shape: a four-element image array on units 0, 2, 4, 6. The
|
|
// subscripts are literals because LegalizeResourceArrayIndexingForEssl has already folded
|
|
// the conformance case's `for (int i = 0; i < g_image.length(); ++i)` - ESSL forbids a
|
|
// non-constant image-array subscript outright, so a loop counter never reaches this pass.
|
|
const char* const kSsoImageArrayFS = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
|
void main()
|
|
{
|
|
imageStore(g_image[0], ivec2(gl_FragCoord.xy), vec4(1.0));
|
|
imageStore(g_image[1], ivec2(gl_FragCoord.xy), vec4(1.0));
|
|
imageStore(g_image[2], ivec2(gl_FragCoord.xy), vec4(1.0));
|
|
imageStore(g_image[3], ivec2(gl_FragCoord.xy), vec4(1.0));
|
|
}
|
|
)";
|
|
|
|
ImageArrayUnitPlan Plan(const String& name, const Vector<Int>& units) {
|
|
ImageArrayUnitPlan plan;
|
|
plan.name = name;
|
|
plan.units = units;
|
|
return plan;
|
|
}
|
|
} // namespace
|
|
|
|
// The defect, end to end. Elements 0..3 need units 0, 2, 4, 6, so the array becomes four scalars
|
|
// carrying those four bindings. Before this, the single stamped binding sent the four elements to
|
|
// units 0, 1, 2, 3.
|
|
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsSplitIntoOneScalarPerElement) {
|
|
Vector<String> declined;
|
|
const String out =
|
|
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, &declined);
|
|
|
|
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
|
const Int units[4] = {0, 2, 4, 6};
|
|
for (Int element = 0; element < 4; ++element) {
|
|
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = " + std::to_string(units[element]) +
|
|
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
|
<< out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", element) + ", ivec2(gl_FragCoord.xy)"))
|
|
<< out;
|
|
}
|
|
// The array is gone entirely; nothing may still address units 0,1,2,3 through it.
|
|
EXPECT_FALSE(Contains(out, "image2D g_image[4];")) << out;
|
|
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
|
// Exactly the four image uniforms the application declared - what the earlier widening cost
|
|
// was the whole SPAN, seven here, which is the budget failure mode this shape removes.
|
|
EXPECT_EQ(CountOf(out, "image2D "), 4u) << out;
|
|
}
|
|
|
|
// The other program of the same conformance case: units 1, 3, 5, 7 in the application's own
|
|
// element ORDER, which is what carries the assignment, so it must NOT be sorted or rebased.
|
|
TEST(RemapImageArrayElementUnitsTest, EachElementCarriesTheUnitTheApplicationGaveIt) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 3) uniform writeonly highp image2D g_image[4];
|
|
void main()
|
|
{
|
|
imageStore(g_image[0], ivec2(0), vec4(2.0));
|
|
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
|
}
|
|
)";
|
|
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {3, 1, 7, 5})});
|
|
const Int units[4] = {3, 1, 7, 5};
|
|
for (Int element = 0; element < 4; ++element) {
|
|
EXPECT_TRUE(Contains(out, "binding = " + std::to_string(units[element]) +
|
|
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
|
<< out;
|
|
}
|
|
// Only elements 0 and 3 are ever accessed; elements 1 and 2 are declared and unused, because
|
|
// the reflection says the array has four of them.
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0)")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 3) + ", ivec2(0)")) << out;
|
|
}
|
|
|
|
// Consecutive-from-element-zero is exactly what ESSL does unaided, so the emitted text of an
|
|
// ordinary image shader must come out byte-identical. The caller filters these; the pass must
|
|
// not depend on that.
|
|
TEST(RemapImageArrayElementUnitsTest, ConsecutiveUnitsAreLeftCompletelyAlone) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 2) uniform writeonly highp image2D g_image[3];
|
|
void main()
|
|
{
|
|
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {2, 3, 4})}), source);
|
|
// ...and so is a plan for an array this stage does not declare at all: the reflection is
|
|
// program-wide, the pass runs per stage.
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("other_image", {0, 4})}), source);
|
|
}
|
|
|
|
// A subscript that is not a literal names no element, so there is no scalar to rewrite it to.
|
|
// It should never arrive - LegalizeResourceArrayIndexingForEssl runs first and ESSL rejects the
|
|
// shape outright - but if one does, guessing an element would only change WHICH unit the access
|
|
// reaches wrongly. Decline, loudly, and change nothing.
|
|
TEST(RemapImageArrayElementUnitsTest, ANonLiteralSubscriptIsDeclinedAndNamed) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
|
void main()
|
|
{
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
imageStore(g_image[i], ivec2(gl_FragCoord.xy), vec4(1.0));
|
|
}
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 2, 4, 6})}, &declined), source);
|
|
ASSERT_EQ(declined.size(), 1u);
|
|
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
|
|
|
// A literal that is out of the reflected range is the same class of mismatch.
|
|
const String outOfRange = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
imageStore(g_image[5], ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
Vector<String> outOfRangeDeclined;
|
|
EXPECT_EQ(RemapImageArrayElementUnits(outOfRange, {Plan("g_image", {0, 5})}, &outOfRangeDeclined),
|
|
outOfRange);
|
|
ASSERT_EQ(outOfRangeDeclined.size(), 1u);
|
|
}
|
|
|
|
// A uint subscript IS a literal element index. SPIRV-Cross prints an index in the type SPIR-V
|
|
// gave it and LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
|
// index it replaced, so an array walked by anything unsigned - a `uint` loop counter, or
|
|
// anything derived from gl_LocalInvocationIndex, which is uint by definition - reaches this pass
|
|
// spelled `g_image[0u]`. Refusing the `u` declined the array and left every element on the
|
|
// consecutive units one binding hands out, silently.
|
|
TEST(RemapImageArrayElementUnitsTest, AUintSubscriptIsStillALiteralElementIndex) {
|
|
const String source = R"(#version 320 es
|
|
layout(local_size_x = 1) in;
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[3];
|
|
void main()
|
|
{
|
|
imageStore(g_image[0u], ivec2(0), vec4(1.0));
|
|
imageStore(g_image[2U], ivec2(0), vec4(2.0));
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined);
|
|
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
|
|
|
EXPECT_TRUE(Contains(out, "binding = 0) uniform writeonly highp image2D " + Elem("g_image", 0) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "binding = 4) uniform writeonly highp image2D " + Elem("g_image", 1) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "binding = 8) uniform writeonly highp image2D " + Elem("g_image", 2) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0), vec4(1.0))")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 2) + ", ivec2(0), vec4(2.0))")) << out;
|
|
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
|
}
|
|
|
|
// ...and the suffix is not a licence to accept anything else that ends in one: `iu` is not a
|
|
// literal, and neither is a bare `u`.
|
|
TEST(RemapImageArrayElementUnitsTest, ASuffixAloneDoesNotMakeAnExpressionALiteral) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
highp int iu = 1;
|
|
imageStore(g_image[iu], ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4})}, &declined), source);
|
|
ASSERT_EQ(declined.size(), 1u);
|
|
}
|
|
|
|
// A use the pass cannot see a subscript on has no element index to rewrite, so splitting the
|
|
// array out from under it would leave it naming a declaration that no longer exists. Decline,
|
|
// loudly, and change nothing.
|
|
TEST(RemapImageArrayElementUnitsTest, AUseWithoutASubscriptIsDeclined) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
|
void helper();
|
|
void main()
|
|
{
|
|
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
|
helper(g_image);
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 5})}, &declined), source);
|
|
ASSERT_EQ(declined.size(), 1u);
|
|
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
|
}
|
|
|
|
// The reflection and the emitted text have to be talking about the same array. If they are not,
|
|
// the pass has misidentified something and must not rewrite on a guess.
|
|
TEST(RemapImageArrayElementUnitsTest, AnExtentThatDisagreesWithTheReflectionIsDeclined) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined), source);
|
|
ASSERT_EQ(declined.size(), 1u);
|
|
}
|
|
|
|
// The two passes that run after it have to see the split declarations and keep their bindings: an
|
|
// ES image unit cannot be assigned through the API, so the qualifier is the only mechanism there
|
|
// is, and an element that is both read and written is split again into a pair that must BOTH
|
|
// carry that element's own unit.
|
|
TEST(RemapImageArrayElementUnitsTest, TheSplitElementsSurviveTheLaterImagePasses) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
|
}
|
|
)";
|
|
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 6})});
|
|
out = SplitReadWriteImageUniforms(out);
|
|
out = RemoveLayoutBinding(out);
|
|
|
|
// Element 0 is only ever loaded and element 1 only ever stored, so neither is split into a
|
|
// pair - but each keeps the unit the application gave it, which the array could not express.
|
|
EXPECT_TRUE(Contains(out, "binding = 4")) << out;
|
|
EXPECT_TRUE(Contains(out, "binding = 6")) << out;
|
|
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RoAlias(Elem("g_image", 0)) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WoAlias(Elem("g_image", 1)) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias(Elem("g_image", 1)) + ", ivec2(0), imageLoad(" +
|
|
RoAlias(Elem("g_image", 0)) + ", ivec2(0)))"))
|
|
<< out;
|
|
// Nothing is left addressing the array.
|
|
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
|
}
|
|
|
|
// The same element both read and written IS split into a coherent pair, and both halves have to
|
|
// inherit that element's binding - the shape the widening used to have to carry on an array.
|
|
TEST(RemapImageArrayElementUnitsTest, AnElementThatIsBothReadAndWrittenIsSplitWithItsOwnBinding) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
imageStore(g_image[1], ivec2(0), imageLoad(g_image[1], ivec2(0)));
|
|
imageStore(g_image[0], ivec2(0), vec4(0.0));
|
|
}
|
|
)";
|
|
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})});
|
|
out = SplitReadWriteImageUniforms(out);
|
|
out = RemoveLayoutBinding(out);
|
|
|
|
// Element 1 sits on unit 9, and both halves of its split pair say so.
|
|
EXPECT_EQ(CountOf(out, "binding = 9"), 2u) << out;
|
|
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias(Elem("g_image", 1)) + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WriteAlias(RwAlias(Elem("g_image", 1))) + ";"))
|
|
<< out;
|
|
EXPECT_EQ(CountOf(out, "binding = 4"), 1u) << out;
|
|
}
|
|
|
|
// The gap that let a per-STAGE image rename reach production: every fixture above declares an
|
|
// image ARRAY, and the regression it caused was in the SCALAR images sitting next to one. A
|
|
// scalar with an explicit binding has to come out of the whole chain still on ITS OWN unit,
|
|
// still spelled once, and named the same thing every stage would name it - it is the array that
|
|
// needs repairing, not its neighbour.
|
|
TEST(RemapImageArrayElementUnitsTest, AScalarImageWithItsOwnBindingIsUntouchedByTheArrayRepair) {
|
|
const String source = R"(#version 320 es
|
|
layout(rgba8, binding = 7) uniform highp image2D goku;
|
|
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
|
void main()
|
|
{
|
|
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
|
}
|
|
)";
|
|
Vector<String> declined;
|
|
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})}, &declined);
|
|
EXPECT_TRUE(declined.empty());
|
|
// The array pass may only ever touch the arrays it was handed a plan for.
|
|
EXPECT_TRUE(Contains(out, "layout(rgba8, binding = 7) uniform highp image2D goku;")) << out;
|
|
|
|
out = SplitReadWriteImageUniforms(out);
|
|
out = RemoveLayoutBinding(out);
|
|
|
|
// Unit 7 exactly twice - the two halves of the scalar's own split pair - and nothing has
|
|
// moved it onto one of the array's units.
|
|
EXPECT_EQ(CountOf(out, "binding = 7"), 2u) << out;
|
|
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
|
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + SplitWriteAlias("goku") + ";")) << out;
|
|
// ...and no per-stage tag anywhere: the name a scalar gets is a function of how this text
|
|
// uses it, so every stage that uses it the same way keeps ONE shared uniform (Adreno spends
|
|
// an image location per distinct one).
|
|
EXPECT_FALSE(Contains(out, "mg_imageVs_")) << out;
|
|
EXPECT_FALSE(Contains(out, "mg_imageFs_")) << out;
|
|
EXPECT_FALSE(Contains(out, "mg_imageCs_")) << out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
// --- tessellation / geometry gl_PointSize directive ---------------------------------------------
|
|
//
|
|
// ESSL 320 makes the tessellation and geometry STAGES core and still leaves gl_PointSize out of
|
|
// their gl_PerVertex entirely - it is only there under EXT/OES_tessellation_point_size resp.
|
|
// EXT/OES_geometry_point_size. SPIRV-Cross only ever sees a SPIR-V BuiltIn PointSize decoration
|
|
// and prints the identifier bare, so without this directive the stage fails to compile with
|
|
// "`gl_PointSize' undeclared", which takes the WHOLE program to program 0: the draw renders
|
|
// nothing and glBeginTransformFeedback on that program is rejected outright, so a capture of
|
|
// anything at all off it silently comes back empty. That is the shape of the 108 conformance
|
|
// bodies (36 per API tree) in tessellation_control_to_tessellation_evaluation.gl_MaxPatch-
|
|
// Vertices_Position_PointSize whose point_mode half puts gl_PointSize in the patch.
|
|
|
|
TEST(PointSizeExtensionNameTest, NamesBothSpellingsOfBothExtensions) {
|
|
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
|
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, true), "GL_EXT_tessellation_point_size");
|
|
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, true), "GL_OES_tessellation_point_size");
|
|
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, false), "GL_EXT_geometry_point_size");
|
|
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, false), "GL_OES_geometry_point_size");
|
|
}
|
|
|
|
// The two extensions are separate and neither implies the other, so the tessellation answer must
|
|
// never be handed to a geometry stage or the other way round - an `#extension` naming a string
|
|
// the driver does not advertise is itself a compile error on a strict compiler.
|
|
TEST(PointSizeExtensionNameTest, NoTierMeansNoDirective) {
|
|
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
|
EXPECT_EQ(PointSizeExtensionName(Tier::None, true), nullptr);
|
|
EXPECT_EQ(PointSizeExtensionName(Tier::None, false), nullptr);
|
|
}
|
|
|
|
TEST(RequestPointSizeExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
|
const String source = R"(#version 320 es
|
|
layout(triangles, point_mode, cw, equal_spacing) in;
|
|
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
|
|
)";
|
|
const String out = RequestPointSizeExtension(source, "GL_EXT_tessellation_point_size");
|
|
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_EXT_tessellation_point_size : require\n")) << out;
|
|
}
|
|
|
|
// The nullptr contract, and the reason it exists: a driver that advertises neither spelling gets
|
|
// NOTHING added rather than a directive it would reject on top of the error it already has.
|
|
TEST(RequestPointSizeExtensionTest, ANullNameMeansNotEmitted) {
|
|
const String source = R"(#version 320 es
|
|
layout(triangles, point_mode, cw, equal_spacing) in;
|
|
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
|
|
)";
|
|
EXPECT_EQ(RequestPointSizeExtension(source, nullptr), source);
|
|
}
|
|
|
|
TEST(RequestPointSizeExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
|
const String source = R"(#version 320 es
|
|
#extension GL_OES_tessellation_point_size : require
|
|
layout(triangles, point_mode, cw, equal_spacing) in;
|
|
void main() { gl_PointSize = 5.0; }
|
|
)";
|
|
const String out = RequestPointSizeExtension(source, "GL_OES_tessellation_point_size");
|
|
EXPECT_EQ(out, source);
|
|
EXPECT_EQ(CountOf(out, "GL_OES_tessellation_point_size"), 1u) << out;
|
|
}
|
|
|
|
// Shares its insertion point with the viewport-array and image-format directives, so a stage
|
|
// needing more than one must end up with all of them and with #version still first.
|
|
TEST(RequestPointSizeExtensionTest, CoexistsWithTheOtherHeaderDirectives) {
|
|
const String source = R"(#version 320 es
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main() { gl_ViewportIndex = 1; gl_PointSize = 2.0; EmitVertex(); }
|
|
)";
|
|
const String out = RequestPointSizeExtension(RequestViewportArrayExtension(source, true),
|
|
"GL_EXT_geometry_point_size");
|
|
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_EXT_geometry_point_size : require\n")) << out;
|
|
}
|
|
|
|
// --- pass-through tessellation control stage --------------------------------------------------
|
|
//
|
|
// Desktop GL makes the tessellation control stage optional and takes the levels from
|
|
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL; ES 3.2 has neither, and rejects a
|
|
// program that has an evaluation stage without a control stage - with an EMPTY info log. The
|
|
// synthesized stage is what stands in, and it has to MIRROR its two neighbours' gl_PerVertex
|
|
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
|
|
// ES link error against a program that has nothing else wrong with it.
|
|
|
|
namespace {
|
|
const FloatVec4 kDefaultOuter(1.0f, 1.0f, 1.0f, 1.0f);
|
|
const FloatVec2 kDefaultInner(1.0f, 1.0f);
|
|
} // namespace
|
|
|
|
// The synthesized stage mirrors its neighbours' gl_PerVertex, so it can be the thing that
|
|
// declares gl_PointSize - and in ESSL a redeclaration is exactly as illegal as a reference
|
|
// without the extension. The directive has to survive being applied to its output.
|
|
TEST(RequestPointSizeExtensionTest, CoversAMirroredPassthroughControlStage) {
|
|
const String out = RequestPointSizeExtension(
|
|
BuildPassthroughTessControlEssl(320, 4, " highp vec4 gl_Position; highp float gl_PointSize; ",
|
|
" highp vec4 gl_Position; highp float gl_PointSize; ", kDefaultOuter,
|
|
kDefaultInner),
|
|
"GL_EXT_tessellation_point_size");
|
|
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_point_size : require\n")) << out;
|
|
EXPECT_TRUE(Contains(out, "float gl_PointSize")) << out;
|
|
}
|
|
|
|
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
|
|
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
|
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
|
|
EXPECT_TRUE(Contains(out,
|
|
"gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;"))
|
|
<< out;
|
|
// All six, unconditionally: writing a level the evaluation stage's domain does not use is
|
|
// legal and ignored, and it saves the generator from having to know the domain.
|
|
for (const char* level : {"gl_TessLevelOuter[0]", "gl_TessLevelOuter[1]", "gl_TessLevelOuter[2]",
|
|
"gl_TessLevelOuter[3]", "gl_TessLevelInner[0]", "gl_TessLevelInner[1]"}) {
|
|
EXPECT_TRUE(Contains(out, String(level) + " = 1.0;")) << level << "\n" << out;
|
|
}
|
|
// Nothing redeclared when the neighbours redeclared nothing - the driver's own built-in
|
|
// gl_in/gl_out is then what both sides agree on, and redeclaring is what would break it.
|
|
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
|
|
}
|
|
|
|
// glPatchParameterfv's state is compiled INTO this stage: ES has no PATCH_DEFAULT_*_LEVEL and no
|
|
// entry point to forward it to, so a generator that ignored these arguments would tessellate every
|
|
// control-stage-less program at level 1 whatever the application asked for.
|
|
TEST(PassthroughTessControlEsslTest, BakesTheDefaultTessLevelsIn) {
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 3.0f, 4.0f, 5.0f),
|
|
FloatVec2(6.5f, 7.25f));
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 2.0;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 3.0;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[2] = 4.0;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[3] = 5.0;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 6.5;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[1] = 7.25;")) << out;
|
|
}
|
|
|
|
// Every level literal carries a decimal point even when the value is integral: ESSL reads
|
|
// `gl_TessLevelOuter[0] = 1;` as an int assigned to a float and refuses to compile the stage,
|
|
// which would take the whole program down with it.
|
|
TEST(PassthroughTessControlEsslTest, SpellsIntegralLevelsAsFloatLiterals) {
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 2.0f, 2.0f, 2.0f),
|
|
FloatVec2(2.0f, 2.0f));
|
|
EXPECT_FALSE(Contains(out, "= 2;")) << out;
|
|
}
|
|
|
|
// glPatchParameterfv accepts any float, NaN and infinity included, and GL 4.6 core 11.2.2
|
|
// discards a patch ONLY when a relevant outer level is <= 0 - everything else is clamped into
|
|
// [1, MAX_TESS_GEN_LEVEL]. So the three non-finite inputs do not share one answer: NaN is
|
|
// unspecified and 0.0 is the safe reading, -inf really does discard, and +inf must tessellate at
|
|
// the maximum. Baking 0.0 for +inf inverted "as finely as possible" into "draw nothing".
|
|
TEST(PassthroughTessControlEsslTest, NonFiniteLevelsFollowTheDiscardRule) {
|
|
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
|
|
const Float infinity = std::numeric_limits<Float>::infinity();
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
|
|
FloatVec4(notANumber, -infinity, infinity, 1.0f),
|
|
FloatVec2(notANumber, 1.0f));
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 0.0;")) << out;
|
|
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[2] = 0.0;"))
|
|
<< "a positive infinity clamps to GL_MAX_TESS_GEN_LEVEL, not to a discarded patch" << out;
|
|
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 0.0;")) << out;
|
|
EXPECT_FALSE(Contains(out, "nan")) << out;
|
|
EXPECT_FALSE(Contains(out, "inf")) << out;
|
|
}
|
|
|
|
// A level below the old six-decimal format's resolution is still a POSITIVE level, which GL clamps
|
|
// to 1 and draws; rendering it as "0.000000" discarded the patch instead.
|
|
TEST(PassthroughTessControlEsslTest, TinyPositiveLevelsDoNotFlushToZero) {
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
|
|
FloatVec4(1e-7f, 1.0f, 1.0f, 1.0f),
|
|
FloatVec2(1.0f, 1.0f));
|
|
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
|
|
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.000000;")) << out;
|
|
}
|
|
|
|
// ES 3.1 reaches tessellation only through the extension; the caller has already established
|
|
// that the driver runs the evaluation stage at all, so the only question is the spelling.
|
|
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
|
const String out = BuildPassthroughTessControlEssl(310, 3, "", "", kDefaultOuter, kDefaultInner);
|
|
EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
|
|
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
|
|
}
|
|
|
|
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
|
|
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
|
|
const String outMembers = " highp vec4 gl_Position; ";
|
|
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers, kDefaultOuter, kDefaultInner);
|
|
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
|
|
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
|
|
}
|
|
|
|
TEST(ExtractPerVertexBlockMembersTest, ReadsEitherDirectionAndOnlyThatDirection) {
|
|
const String essl = R"(#version 320 es
|
|
in gl_PerVertex { highp vec4 gl_Position; } gl_in[gl_MaxPatchVertices];
|
|
out gl_PerVertex { highp vec4 gl_Position; highp float gl_PointSize; } gl_out[];
|
|
void main() {}
|
|
)";
|
|
const auto inMembers = ExtractPerVertexBlockMembers(essl, true);
|
|
ASSERT_TRUE(inMembers.has_value()) << essl;
|
|
EXPECT_TRUE(Contains(*inMembers, "gl_Position")) << *inMembers;
|
|
EXPECT_FALSE(Contains(*inMembers, "gl_PointSize"))
|
|
<< "the `in` block must not pick up the `out` block's members: " << *inMembers;
|
|
|
|
const auto outMembers = ExtractPerVertexBlockMembers(essl, false);
|
|
ASSERT_TRUE(outMembers.has_value()) << essl;
|
|
EXPECT_TRUE(Contains(*outMembers, "gl_PointSize")) << *outMembers;
|
|
}
|
|
|
|
// A shader that does not redeclare the block must report nothing, so the generator leaves the
|
|
// driver's built-in declaration alone rather than inventing one.
|
|
TEST(ExtractPerVertexBlockMembersTest, ReportsNothingWhenTheBlockIsNotRedeclared) {
|
|
const String essl = R"(#version 320 es
|
|
layout(quads) in;
|
|
void main() { gl_Position = gl_in[0].gl_Position; }
|
|
)";
|
|
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
|
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, false).has_value()) << essl;
|
|
}
|
|
|
|
// "min" ends in "in" and "layout" ends in "out": the direction keyword has to be a whole token
|
|
// immediately before the block name, or an unrelated identifier would be read as a redeclaration.
|
|
TEST(ExtractPerVertexBlockMembersTest, DoesNotMatchAnIdentifierEndingInTheKeyword) {
|
|
const String essl = R"(#version 320 es
|
|
struct fin gl_PerVertex { highp vec4 gl_Position; };
|
|
void main() {}
|
|
)";
|
|
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
|
}
|