mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix, Test] (DirectGLES): reach an image array's non-consecutive units by widening the array over their span
This commit is contained in:
@@ -5357,6 +5357,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
// Every image ARRAY whose elements the application did NOT leave on units consecutive from
|
||||
// element zero - the only shape ESSL can spell, since an image unit there comes solely
|
||||
// from the one layout(binding=N) an array declaration carries. Desktop GL assigns them
|
||||
// per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform,
|
||||
// so there is nothing to fix at the API end and the emitted text has to carry it
|
||||
// (RemapImageArrayElementUnits). Empty for every program that does not do this, which is
|
||||
// very nearly all of them - one walk of the reflection and no allocation in that case.
|
||||
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
Vector<ImageArrayUnitPlan> plans;
|
||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
// Reflection repeats the array's "g_image[0]" spelling at EVERY location the array
|
||||
// spans, so only the location that name resolves back to is the array itself.
|
||||
if (stateProgramObject.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
|
||||
const String baseName = ImageUniformBaseName(name);
|
||||
if (baseName == name) continue; // a scalar image: one binding says it all
|
||||
|
||||
ImageArrayUnitPlan plan;
|
||||
plan.name = baseName;
|
||||
for (Uint element = loc; element <= maxUniformLoc &&
|
||||
stateProgramObject.UniformLocationsAliasSameUniform(
|
||||
static_cast<Int>(loc), static_cast<Int>(element));
|
||||
++element) {
|
||||
plan.units.push_back(stateProgramObject.GetUniformSamplerOrImageUnitIndex(element));
|
||||
}
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
Bool consecutive = true;
|
||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
||||
if (plan.units[element] != plan.units[0] + static_cast<Int>(element)) {
|
||||
consecutive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// What ESSL does unaided is already right; leaving these out is what keeps the
|
||||
// emitted text of every ordinary image shader byte-identical to before.
|
||||
if (consecutive) continue;
|
||||
plans.push_back(Move(plan));
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
|
||||
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
|
||||
Uint64 signature = 0;
|
||||
@@ -5854,6 +5900,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"different bound formats; left format-less.",
|
||||
conflicted.c_str(), stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
// ...and once more for image ARRAYS whose per-element units are not consecutive, which
|
||||
// ESSL has no way to express in one declaration. Program-wide, like the bake, and read
|
||||
// from the same snapshot of the reflection; the per-stage rewrite happens below.
|
||||
const Vector<ImageArrayUnitPlan> nonConsecutiveImageArrays =
|
||||
CollectNonConsecutiveImageArrayPlans(*stateProgramObject);
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -6184,6 +6235,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// strip, so both halves of a split image inherit the format.
|
||||
source = BakeImageFormatQualifiers(std::move(source),
|
||||
imageFormatBake.esslFormatQualifierByUniformName);
|
||||
// An image ARRAY whose elements do not sit on consecutive units cannot be spelled
|
||||
// by the single layout(binding=N) the rebind above stamped: ESSL gives element k
|
||||
// the unit N+k and there is no glUniform1i to correct it with. Widen the array to
|
||||
// cover the span and route its subscripts through a constant offset table. AFTER
|
||||
// the rebind and the format bake, both of which look the array up by its GL
|
||||
// uniform name and need the binding already there; BEFORE the read+write split,
|
||||
// so both halves inherit the widened extent and the rebased binding.
|
||||
if (!nonConsecutiveImageArrays.empty()) {
|
||||
Vector<String> declinedImageArrays;
|
||||
source = RemapImageArrayElementUnits(
|
||||
source, nonConsecutiveImageArrays,
|
||||
AdvertisedStageImageUniformLimit(shader->GetShaderStage()), &declinedImageArrays);
|
||||
for (const auto& declined : declinedImageArrays) {
|
||||
// MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics
|
||||
// around it: this is the "linked, drew, produced wrong numbers, said
|
||||
// nothing" shape that cost earlier waves whole days, and one line per
|
||||
// declined array is bounded by program count. There is no honest GL answer
|
||||
// to give instead - the frontend has already reported LINK_STATUS = true.
|
||||
MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach "
|
||||
"from one declaration, so this stage will read and write the WRONG units. State "
|
||||
"program ID: %u, stage: %s.",
|
||||
declined.c_str(), stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
}
|
||||
// Wedged between those two on purpose:
|
||||
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
|
||||
// both halves of a split image is already the frontend texture unit (and so
|
||||
|
||||
@@ -968,6 +968,222 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The digits of an array extent, or -1 for "not a plain literal size".
|
||||
Int ParseArrayExtent(const String& text) {
|
||||
if (text.empty()) return -1;
|
||||
Int value = 0;
|
||||
for (const char c : text) {
|
||||
if (c < '0' || c > '9') return -1;
|
||||
value = value * 10 + (c - '0');
|
||||
if (value > 4096) return -1; // no image array is anywhere near this
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
const Int stageImageUniformBudget, Vector<String>* outDeclined) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (outDeclined != nullptr) outDeclined->clear();
|
||||
if (plans.empty() || glslCode.find("image") == String::npos) return glslCode;
|
||||
|
||||
// Same declaration shape as the split pass reads, with the array extent captured.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]*)\s*\])?\s*;)");
|
||||
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
|
||||
|
||||
struct StageImageDecl {
|
||||
String name;
|
||||
String layout;
|
||||
String qualifiers;
|
||||
String type;
|
||||
Int elementCount = 1;
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
};
|
||||
// Every image declaration in the stage, because the budget test below is about the
|
||||
// stage's total and not about this one array.
|
||||
Vector<StageImageDecl> decls;
|
||||
Int stageImageUniforms = 0;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
StageImageDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
decl.qualifiers = NormalizeDeclarationSpacing(match[2].str());
|
||||
decl.type = match[3].str();
|
||||
decl.name = match[4].str();
|
||||
decl.elementCount = match[5].matched ? ParseArrayExtent(match[5].str()) : 1;
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
stageImageUniforms += decl.elementCount > 0 ? decl.elementCount : 1;
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenNames;
|
||||
for (const ImageArrayUnitPlan& plan : plans) {
|
||||
const auto decline = [&](const char* why) {
|
||||
if (outDeclined != nullptr) outDeclined->push_back(plan.name + ": " + why);
|
||||
};
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
const StageImageDecl* decl = nullptr;
|
||||
for (const auto& candidate : decls) {
|
||||
if (candidate.name == plan.name) {
|
||||
decl = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (decl == nullptr) {
|
||||
// Absent from this stage entirely is the normal outcome - the reflection is
|
||||
// program-wide and this pass runs per stage. Named but not RECOGNIZED is not:
|
||||
// it means the declaration is spelled in some shape the regex above does not
|
||||
// read, and staying quiet about that is how the wrong units got shipped.
|
||||
if (ContainsIdentifier(glslCode, plan.name)) {
|
||||
decline("the stage names it but declares it in a shape this pass cannot read");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (decl->elementCount < 0 || static_cast<SizeT>(decl->elementCount) != plan.units.size()) {
|
||||
decline("the emitted array extent disagrees with the reflected element count");
|
||||
continue;
|
||||
}
|
||||
|
||||
Int minUnit = plan.units[0];
|
||||
Int maxUnit = plan.units[0];
|
||||
Bool consecutive = true;
|
||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
||||
const Int unit = plan.units[element];
|
||||
if (unit < 0) {
|
||||
minUnit = -1;
|
||||
break;
|
||||
}
|
||||
if (unit != plan.units[0] + static_cast<Int>(element)) consecutive = false;
|
||||
minUnit = std::min(minUnit, unit);
|
||||
maxUnit = std::max(maxUnit, unit);
|
||||
}
|
||||
if (minUnit < 0) {
|
||||
decline("an element has no image unit");
|
||||
continue;
|
||||
}
|
||||
// Already exactly what ESSL would do on its own. The caller filters these out;
|
||||
// repeating the test here keeps the pass correct on its own terms.
|
||||
if (consecutive) continue;
|
||||
|
||||
const Int spanSize = maxUnit - minUnit + 1;
|
||||
// The array has to COVER every unit from the lowest to the highest, because ESSL
|
||||
// hands an array's elements consecutive units and nothing else can move them.
|
||||
// The elements in between are declared and never accessed; what they cost is
|
||||
// image-uniform budget, so that is what is checked.
|
||||
if (stageImageUniformBudget > 0 &&
|
||||
stageImageUniforms - decl->elementCount + spanSize > stageImageUniformBudget) {
|
||||
decline("the units are spread too far apart to cover within this stage's "
|
||||
"GL_MAX_*_IMAGE_UNIFORMS");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Every use has to be `name[...]`, or the element index has nowhere to be
|
||||
// rewritten and the array cannot be widened underneath it.
|
||||
struct Subscript {
|
||||
SizeT open;
|
||||
SizeT close;
|
||||
};
|
||||
Vector<Subscript> subscripts;
|
||||
Bool everyUseIsSubscripted = true;
|
||||
for (SizeT pos = glslCode.find(plan.name); pos != String::npos;
|
||||
pos = glslCode.find(plan.name, pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue;
|
||||
const SizeT after = pos + plan.name.size();
|
||||
if (after < glslCode.size() && IsImagePassIdentifierChar(glslCode[after])) continue;
|
||||
if (pos >= decl->declStart && pos < decl->declStart + decl->declLength) {
|
||||
continue; // the declaration's own name
|
||||
}
|
||||
const SizeT open = glslCode.find_first_not_of(" \t\r\n", after);
|
||||
if (open == String::npos || glslCode[open] != '[') {
|
||||
everyUseIsSubscripted = false;
|
||||
break;
|
||||
}
|
||||
Int depth = 0;
|
||||
SizeT scan = open;
|
||||
for (; scan < glslCode.size(); ++scan) {
|
||||
if (glslCode[scan] == '[') {
|
||||
++depth;
|
||||
} else if (glslCode[scan] == ']' && --depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scan >= glslCode.size() || open + 1 >= scan) {
|
||||
everyUseIsSubscripted = false;
|
||||
break;
|
||||
}
|
||||
subscripts.push_back({open, scan});
|
||||
}
|
||||
if (!everyUseIsSubscripted) {
|
||||
decline("it is reached by something other than a subscript, so there is no element "
|
||||
"index to rewrite");
|
||||
continue;
|
||||
}
|
||||
|
||||
const String mapName =
|
||||
MakeImageAliasName(IMAGE_UNIT_MAP_PREFIX, plan.name, glslCode, takenNames);
|
||||
takenNames.push_back(mapName);
|
||||
|
||||
// The widened declaration, rebased on the lowest unit...
|
||||
String layout = decl->layout;
|
||||
const String bindingText = "binding = " + std::to_string(minUnit);
|
||||
if (std::regex_search(layout, bindingValueRegex)) {
|
||||
layout = std::regex_replace(layout, bindingValueRegex, bindingText);
|
||||
} else {
|
||||
layout = bindingText + (layout.empty() ? String() : ", " + layout);
|
||||
}
|
||||
String replacement = "layout(" + layout + ") uniform ";
|
||||
if (!decl->qualifiers.empty()) {
|
||||
replacement += decl->qualifiers;
|
||||
replacement += ' ';
|
||||
}
|
||||
replacement += decl->type + " " + plan.name + "[" + std::to_string(spanSize) + "];";
|
||||
// ...and the table that turns the application's element index into the offset of
|
||||
// the unit that element was actually assigned. A const array is a constant
|
||||
// expression when it is indexed by one, so a shader whose subscripts are literals
|
||||
// keeps constant subscripts; and when the subscript is a loop counter the lookup
|
||||
// stays dynamically uniform, which is what GLSL ES 3.20 requires of an image
|
||||
// array index.
|
||||
const SizeT elementCount = plan.units.size();
|
||||
replacement += "\nconst highp int " + mapName + "[" + std::to_string(elementCount) + "] = int[" +
|
||||
std::to_string(elementCount) + "](";
|
||||
for (SizeT element = 0; element < elementCount; ++element) {
|
||||
if (element != 0) replacement += ", ";
|
||||
replacement += std::to_string(plan.units[element] - minUnit);
|
||||
}
|
||||
replacement += ");";
|
||||
edits.push_back({decl->declStart, decl->declLength, Move(replacement)});
|
||||
|
||||
// `name[EXPR]` -> `name[<map>[EXPR]]`, by insertion, so EXPR itself is untouched
|
||||
// however it is spelled.
|
||||
for (const Subscript& subscript : subscripts) {
|
||||
edits.push_back({subscript.open + 1, 0, mapName + "["});
|
||||
edits.push_back({subscript.close, 0, "]"});
|
||||
}
|
||||
}
|
||||
if (edits.empty()) return glslCode;
|
||||
|
||||
// Back to front, so an earlier edit's offsets stay valid. Two inserts never share an
|
||||
// offset: a subscript's opening and closing brackets are distinct positions and a
|
||||
// declaration is replaced whole.
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
||||
String result = glslCode;
|
||||
for (const ImageSourceEdit& edit : edits) {
|
||||
result.replace(edit.start, edit.length, edit.text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String SplitReadWriteImageUniforms(const String& glslCode, GLenum shaderType, Uint* outSplitCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
|
||||
@@ -235,6 +235,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the const element->unit-offset table RemapImageArrayElementUnits declares
|
||||
// next to a widened image array; the suffix is the array's own name.
|
||||
constexpr const char* IMAGE_UNIT_MAP_PREFIX = "mg_imageUnitMap_";
|
||||
// One image ARRAY whose elements the application pointed at units that are not
|
||||
// consecutive-from-element-zero.
|
||||
struct ImageArrayUnitPlan {
|
||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||
Vector<Int> units; // the frontend image unit element k has to reach
|
||||
};
|
||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||
// wrong value and three were never written, with no GL error and no link log. The same
|
||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||
// array has no API side to fix.
|
||||
//
|
||||
// Repaired by WIDENING the array to cover every unit from the lowest it needs to the
|
||||
// highest, rebasing its binding on the lowest, and routing every subscript through a
|
||||
// `const highp int` table of per-element offsets. The elements in between are declared
|
||||
// and never accessed. A const array indexed by a constant expression IS one, so literal
|
||||
// subscripts stay literal; and a table lookup on a dynamically uniform index is itself
|
||||
// dynamically uniform, which is what GLSL ES 3.20 asks of an image array index - so this
|
||||
// works whether SPIRV-Cross unrolled the application's loop over the array or not. That
|
||||
// is the reason for the table rather than one scalar declaration per element: scalars
|
||||
// cannot be selected by a non-constant index at all.
|
||||
//
|
||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||
// caller to report - when the span will not fit in `stageImageUniformBudget`
|
||||
// (GL_MAX_<stage>_IMAGE_UNIFORMS; <= 0 means "cannot say", and then it is not enforced),
|
||||
// when the emitted extent disagrees with the reflection, or when the array is reached by
|
||||
// anything other than a subscript. Silence was the whole defect here, so a decline must
|
||||
// be audible.
|
||||
//
|
||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||
// SplitReadWriteImageUniforms (so both halves of a split inherit the widened extent and
|
||||
// the rebased binding) and RemoveLayoutBinding (which is what preserves image bindings).
|
||||
// Like them, it is downstream of the L2 shader-translation memo, so the per-program units
|
||||
// it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Int stageImageUniformBudget, Vector<String>* outDeclined = nullptr);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already stage-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
|
||||
@@ -127,14 +127,24 @@ void main()
|
||||
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||
// spelling in a single declaration and cannot be expressed at all without splitting
|
||||
// the array into one declaration per element and rewriting every use of it.
|
||||
// spelling in a single declaration.
|
||||
//
|
||||
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
||||
// storage-block rebinding cases: the defect is per-backend and the frontend
|
||||
// mechanism these cases exist for - per-element units surviving the trip to the
|
||||
// pipeline composite - is fully exercised on Magma.
|
||||
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
// RemapImageArrayElementUnits repairs it by WIDENING the array to cover every unit
|
||||
// from the lowest it needs to the highest and routing each subscript through a const
|
||||
// offset table. What that costs is image-uniform budget: units 0..6 need SEVEN
|
||||
// fragment image uniforms where the application declared four. So the gate is the
|
||||
// budget, not the backend - a driver that cannot afford the widening declines it
|
||||
// (and says so at ERROR level) rather than addressing the wrong units silently.
|
||||
// DirectVulkan has no such constraint and needs no widening at all.
|
||||
bool PerElementImageUnitsAreHonoured() const {
|
||||
if (Gl().BackendName() == "DirectVulkan") return true;
|
||||
GLint maxFragmentImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
// The widest span either of the two fragment programs below needs: 0..6 and 1..7.
|
||||
return maxFragmentImageUniforms >= 7;
|
||||
}
|
||||
|
||||
// The scenarios below need image load/store at all; a driver without it should skip
|
||||
// rather than fail.
|
||||
@@ -164,7 +174,8 @@ void main()
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
GTEST_SKIP() << "fewer than 7 fragment image uniforms: the widening that covers a "
|
||||
"non-consecutive image array does not fit";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
@@ -283,8 +294,12 @@ void main()
|
||||
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
|
||||
// here are consecutive on purpose, so on Espryt this would exercise nothing the case
|
||||
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
|
||||
// it deliberately never triggers.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@ using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_STAGE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_UNIT_MAP_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageStageAliasPrefix;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
||||
@@ -47,6 +50,7 @@ namespace {
|
||||
// The writeonly half is minted from the ALREADY stage-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(StageAlias(name)); }
|
||||
String UnitMap(const String& name) { return String(IMAGE_UNIT_MAP_PREFIX) + name; }
|
||||
} // namespace
|
||||
|
||||
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
||||
@@ -502,6 +506,170 @@ TEST(SplitReadWriteImageUniformsTest, EveryStageTagIsDistinct) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// 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, written
|
||||
// through a loop counter (which is how SPIRV-Cross emits the conformance case's
|
||||
// `for (int i = 0; i < g_image.length(); ++i)` when it does not unroll it).
|
||||
const char* const kSsoImageArrayFS = 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));
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
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 is widened to cover
|
||||
// units 0..6 and every subscript is routed through the offset table. Before this, the single
|
||||
// stamped binding sent the four elements to units 0, 1, 2, 3.
|
||||
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsWidenTheArrayAndRouteEverySubscript) {
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, 8, &declined);
|
||||
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
// Seven elements from binding 0, i.e. units 0..6 - the span the four assigned units need.
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[7];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[4] = int[4](0, 2, 4, 6);")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("g_image") + "[i]], ivec2(gl_FragCoord.xy)")) << out;
|
||||
// The original four-element extent is gone; nothing may still address units 0,1,2,3.
|
||||
EXPECT_FALSE(Contains(out, "image2D g_image[4];")) << out;
|
||||
}
|
||||
|
||||
// The other program of the same conformance case: units 1, 3, 5, 7, so the binding rebases onto
|
||||
// the LOWEST unit rather than staying on element [0]'s.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheBindingRebasesOntoTheLowestUnitInTheSpan) {
|
||||
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})}, 8);
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = 1) uniform writeonly highp image2D g_image[7];")) << out;
|
||||
// Offsets from the new base, in the application's element order - the order is what carries
|
||||
// the assignment, so it must NOT be sorted.
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[4] = int[4](2, 0, 6, 4);")) << out;
|
||||
// A literal subscript stays a constant expression: a const array indexed by one is one.
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("g_image") + "[0]], ivec2(0)")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("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})}, 8), 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})}, 8), source);
|
||||
}
|
||||
|
||||
// Widening costs image-uniform budget, and a stage that cannot afford it must be told so rather
|
||||
// than silently addressing the wrong units - the exact silence this whole pass exists to end.
|
||||
TEST(RemapImageArrayElementUnitsTest, ASpanThatExceedsTheStageBudgetIsDeclinedAndNamed) {
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, 4, &declined);
|
||||
EXPECT_EQ(out, String(kSsoImageArrayFS)) << out;
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
|
||||
// A budget of "cannot say" (the ES side reports no limit for this stage) must not be read as
|
||||
// a budget of zero - that would decline every array on a driver that simply does not answer.
|
||||
Vector<String> unknownBudget;
|
||||
const String repaired =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, -1, &unknownBudget);
|
||||
EXPECT_TRUE(unknownBudget.empty());
|
||||
EXPECT_TRUE(Contains(repaired, "image2D g_image[7];")) << repaired;
|
||||
}
|
||||
|
||||
// A use the pass cannot see a subscript on has no element index to rewrite, so widening the
|
||||
// array underneath it would change which unit it reaches. 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})}, 8, &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})}, 16, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// The two passes that run after it have to see the widened declaration and keep its binding: an
|
||||
// ES image unit cannot be assigned through the API, so the qualifier is the only mechanism there
|
||||
// is, and a read+write array is split into two declarations that must BOTH be the widened one.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheWidenedArraySurvivesTheLaterImagePasses) {
|
||||
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})}, 8);
|
||||
out = SplitReadWriteImageUniforms(out, kStage);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Both halves, both widened to the three units 4..6, and both still bound.
|
||||
EXPECT_EQ(CountOf(out, "binding = 4"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + StageAlias("g_image") + "[3];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + SplitWriteAlias("g_image") + "[3];")) << out;
|
||||
// The offset table is untouched by the rename - it is not an image uniform - and both halves
|
||||
// still route their subscripts through it.
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[2] = int[2](0, 2);")) << out;
|
||||
EXPECT_TRUE(Contains(out, SplitWriteAlias("g_image") + "[" + UnitMap("g_image") + "[1]]")) << out;
|
||||
EXPECT_TRUE(Contains(out, StageAlias("g_image") + "[" + UnitMap("g_image") + "[0]]")) << out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// RetargetTextureBufferExtension
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user