mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Merge] (ShaderTranspiler, GLState, DirectGLES): land dev GL43 wave2/wave3 under the translation cache
This commit is contained in:
@@ -368,6 +368,31 @@ namespace MobileGL {
|
||||
return m_transformFeedbackGeometryCaptureDraws;
|
||||
}
|
||||
|
||||
// Conditional rendering (GL 4.6 core 10.9). `discard` is the verdict already
|
||||
// resolved from the query object at glBeginConditionalRender - the predicate is
|
||||
// read ONCE there, not per command, because GL specifies the block against the
|
||||
// result available at Begin and re-reading it would let a query that is still
|
||||
// being written change the answer mid-block.
|
||||
void BeginConditionalRender(GLuint queryId, GLenum mode, Bool discard) {
|
||||
m_conditionalRenderActive = true;
|
||||
m_conditionalRenderQuery = queryId;
|
||||
m_conditionalRenderMode = mode;
|
||||
m_conditionalRenderDiscards = discard;
|
||||
}
|
||||
void EndConditionalRender() {
|
||||
m_conditionalRenderActive = false;
|
||||
m_conditionalRenderQuery = 0;
|
||||
m_conditionalRenderMode = GL_NONE;
|
||||
m_conditionalRenderDiscards = false;
|
||||
}
|
||||
Bool IsConditionalRenderActive() const { return m_conditionalRenderActive; }
|
||||
GLuint GetConditionalRenderQuery() const { return m_conditionalRenderQuery; }
|
||||
// Whether the commands GL 4.6 core 10.9 makes conditional are being discarded
|
||||
// right now. False whenever no block is open, so a caller needs no second test.
|
||||
Bool ConditionalRenderDiscardsCommands() const {
|
||||
return m_conditionalRenderActive && m_conditionalRenderDiscards;
|
||||
}
|
||||
|
||||
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
||||
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
||||
// binding points are object state, but the context keeps exactly one live
|
||||
@@ -466,6 +491,13 @@ namespace MobileGL {
|
||||
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
|
||||
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
|
||||
|
||||
// Conditional rendering. Context state, not object state: GL 4.6 core 10.9 allows
|
||||
// exactly one block open at a time and no object owns it.
|
||||
Bool m_conditionalRenderActive = false;
|
||||
Bool m_conditionalRenderDiscards = false;
|
||||
GLuint m_conditionalRenderQuery = 0;
|
||||
GLenum m_conditionalRenderMode = GL_NONE;
|
||||
|
||||
// Everything a transform feedback object owns while it is NOT the bound one.
|
||||
struct TransformFeedbackObjectState {
|
||||
struct SavedBufferBinding {
|
||||
|
||||
@@ -129,6 +129,180 @@ namespace {
|
||||
return element;
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters
|
||||
// may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past
|
||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the
|
||||
// Vulkan-relaxed parse never reaches - vkRelaxedRemapUniformVariable folds the atomic_uint
|
||||
// into a synthesized storage block and returns from declareVariable() before fixOffset()
|
||||
// runs, clearing explicitOffset on the way ("xxTODO: use logic from fixOffset()"). Two
|
||||
// counters declared at the same binding AND the same offset therefore linked cleanly.
|
||||
//
|
||||
// The offsets themselves survive that lowering (reflection and the SPIR-V generator both
|
||||
// honour layoutOffset), so the check belongs here, over the same model the GL queries answer
|
||||
// from. Returns the info-log line for an illegal layout, empty for a legal one.
|
||||
static MobileGL::String ValidateAtomicCounterLayout(glslang::TProgram& reflection) {
|
||||
using MobileGL::Bool;
|
||||
using MobileGL::Int;
|
||||
using MobileGL::SizeT;
|
||||
using MobileGL::String;
|
||||
using MobileGL::Vector;
|
||||
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
|
||||
|
||||
const Int blockCount = reflection.getNumUniformBlocks();
|
||||
if (blockCount <= 0) return {};
|
||||
const SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
|
||||
Vector<Bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
|
||||
Bool anyCounterBlock = false;
|
||||
for (Int i = 0; i < blockCount; ++i) {
|
||||
const auto& block = reflection.getUniformBlock(i);
|
||||
isCounterBlock[static_cast<SizeT>(i)] =
|
||||
block.name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
|
||||
anyCounterBlock = anyCounterBlock || isCounterBlock[static_cast<SizeT>(i)];
|
||||
}
|
||||
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
|
||||
|
||||
struct CounterSpan {
|
||||
Int offset = 0;
|
||||
Int size = 0;
|
||||
String name;
|
||||
};
|
||||
Vector<Vector<CounterSpan>> spansByBlock(static_cast<SizeT>(blockCount));
|
||||
const Int uniformCount = reflection.getNumUniformVariables();
|
||||
for (Int i = 0; i < uniformCount; ++i) {
|
||||
const auto& uniform = reflection.getUniform(i);
|
||||
const Int owner = uniform.index;
|
||||
if (owner < 0 || owner >= blockCount || !isCounterBlock[static_cast<SizeT>(owner)]) continue;
|
||||
const Int offset = uniform.offset;
|
||||
if (offset < 0) continue; // no offset recorded; nothing to compare
|
||||
Int elements = uniform.size > 1 ? uniform.size : 1;
|
||||
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isArray()) {
|
||||
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
|
||||
}
|
||||
const Int size = elements * static_cast<Int>(sizeof(MobileGL::Uint32));
|
||||
if (offset % 4 != 0) {
|
||||
return std::format("Atomic counter '{}' is declared at offset {}, which is not a multiple of 4.",
|
||||
uniform.name, offset);
|
||||
}
|
||||
if (offset > Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE - size) {
|
||||
return std::format("Atomic counter '{}' ends at byte {}, past the {}-byte "
|
||||
"GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.",
|
||||
uniform.name, offset + size, Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||
}
|
||||
auto& spans = spansByBlock[static_cast<SizeT>(owner)];
|
||||
for (const CounterSpan& existing : spans) {
|
||||
if (offset < existing.offset + existing.size && existing.offset < offset + size) {
|
||||
return std::format("Atomic counters '{}' and '{}' share a binding and overlap at byte offset {}.",
|
||||
existing.name, uniform.name, std::max(offset, existing.offset));
|
||||
}
|
||||
}
|
||||
spans.push_back({offset, size, uniform.name});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6: LinkProgram FAILS when a stage's count of active image uniforms exceeds
|
||||
// GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT,COMPUTE}_IMAGE_UNIFORMS, or
|
||||
// when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing enforced it: glslang carries
|
||||
// those numbers in TBuiltInResource only so gl_Max*ImageUniforms can expand from them, and
|
||||
// its linker never counts uniforms against them - so a program declaring one image uniform
|
||||
// more than the limit linked cleanly and then rendered nothing.
|
||||
//
|
||||
// The limits are the ones glGetIntegerv answers (MG_Impl/GLImpl/Getter/GL_Getter.cpp), the
|
||||
// hardcoded tessellation zeros included: a program may not exceed a limit the implementation
|
||||
// advertises, whatever the driver underneath would have taken.
|
||||
//
|
||||
// Counts the APPLICATION's image uniforms. The DirectGLES read/write split emits a second
|
||||
// declaration for an image a stage both reads and writes (MG_Backend/DirectGLES/Utils.h), but
|
||||
// that happens in the backend after this link, and counting the expanded set here would
|
||||
// reject programs that are legal by the numbers GL advertises. Returns the info-log line for
|
||||
// a program over a limit, empty for one within them.
|
||||
static MobileGL::String ValidateImageUniformLimits(
|
||||
glslang::TProgram& reflection, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
using MobileGL::Array;
|
||||
using MobileGL::Int;
|
||||
using MobileGL::SizeT;
|
||||
using MobileGL::UnorderedMap;
|
||||
|
||||
static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
|
||||
EShLangGeometry, EShLangFragment, EShLangCompute};
|
||||
static constexpr const char* kLimitNames[] = {
|
||||
"GL_MAX_VERTEX_IMAGE_UNIFORMS", "GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS",
|
||||
"GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS", "GL_MAX_GEOMETRY_IMAGE_UNIFORMS",
|
||||
"GL_MAX_FRAGMENT_IMAGE_UNIFORMS", "GL_MAX_COMPUTE_IMAGE_UNIFORMS"};
|
||||
constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]);
|
||||
const Int limits[kStageCount] = {env.params.MaxVertexImageUniforms,
|
||||
0,
|
||||
0,
|
||||
env.params.MaxGeometryImageUniforms,
|
||||
env.params.MaxFragmentImageUniforms,
|
||||
env.params.MaxComputeImageUniforms};
|
||||
|
||||
// Reflection spells an image ARRAY one of two ways, and which one it picks depends on how
|
||||
// the shader indexed it: a variable index makes glslang expand the array into one entry
|
||||
// per element ("u_image[0]".."u_image[8]", each carrying the ELEMENT type), while an
|
||||
// array never dereferenced at all stays a single entry carrying the array type. One
|
||||
// program can even produce both spellings for the same array. So neither counting entries
|
||||
// nor trusting the declared size is right on its own - they are reconciled per declared
|
||||
// name with a max, which is exact for either spelling and cannot double-count the mixture.
|
||||
struct ImageUse {
|
||||
Int entries = 0; // reflection entries seen for this name in this stage
|
||||
Int declared = 0; // largest element count any of them declared
|
||||
};
|
||||
UnorderedMap<MobileGL::String, Array<ImageUse, kStageCount>> useByName;
|
||||
|
||||
const Int uniformCount = reflection.getNumUniformVariables();
|
||||
for (Int i = 0; i < uniformCount; ++i) {
|
||||
const auto& uniform = reflection.getUniform(i);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type == nullptr || !type->isImage()) continue;
|
||||
// An array occupies one image unit per element; an unsized one (never indexed, so
|
||||
// never more than the single element glslang kept) counts as one.
|
||||
Int elements = uniform.size > 1 ? uniform.size : 1;
|
||||
if (type->isArray()) {
|
||||
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
|
||||
}
|
||||
// `stages` is the set of stages that REFERENCE the uniform, which is exactly what GL
|
||||
// counts: an image declared in two stages costs a unit in each, and one no stage
|
||||
// reads is not active at all and costs nothing.
|
||||
Array<ImageUse, kStageCount>* use = nullptr;
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
if ((static_cast<unsigned>(uniform.stages) & (1u << static_cast<unsigned>(kStages[stage]))) == 0) {
|
||||
continue;
|
||||
}
|
||||
// The one insert this uniform performs, so the reference survives the rest of the
|
||||
// stage loop - a flat hash map relocates on insert, never on read.
|
||||
if (use == nullptr) {
|
||||
use = &useByName[StripArrayElementSuffix(uniform.name)];
|
||||
}
|
||||
++(*use)[stage].entries;
|
||||
(*use)[stage].declared = std::max((*use)[stage].declared, elements);
|
||||
}
|
||||
}
|
||||
|
||||
Int counts[kStageCount] = {};
|
||||
for (const auto& entry : useByName) {
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared);
|
||||
}
|
||||
}
|
||||
|
||||
Int combined = 0;
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
combined += counts[stage];
|
||||
if (counts[stage] > limits[stage]) {
|
||||
return std::format("This program uses {} active image uniforms in one stage, more than the {} "
|
||||
"{} allows.",
|
||||
counts[stage], limits[stage], kLimitNames[stage]);
|
||||
}
|
||||
}
|
||||
if (combined > env.params.MaxCombinedImageUniforms) {
|
||||
return std::format("This program uses {} active image uniforms across its stages, more than the {} "
|
||||
"GL_MAX_COMBINED_IMAGE_UNIFORMS allows.",
|
||||
combined, env.params.MaxCombinedImageUniforms);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
||||
const auto* type = output.getType();
|
||||
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
||||
@@ -795,6 +969,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String atomicCounterError = ValidateAtomicCounterLayout(*artifacts.program);
|
||||
!atomicCounterError.empty()) {
|
||||
artifacts.infoLog = Move(atomicCounterError);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env);
|
||||
!imageUniformError.empty()) {
|
||||
artifacts.infoLog = Move(imageUniformError);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
|
||||
// Blocks first: global-UBO membership drives the uniform filter below. The
|
||||
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
|
||||
@@ -847,7 +1037,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
||||
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
|
||||
// survives the relaxed parse (and mapIO auto-assigns the rest).
|
||||
constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd;
|
||||
//
|
||||
// "no effective location yet". Deliberately OUTSIDE the location space rather than
|
||||
// glslang::TQualifier::layoutLocationEnd, which is the first location past the pool and
|
||||
// therefore only one off a legal one - a sentinel that sits at the boundary it guards has
|
||||
// to be re-proved safe every time the ceiling moves, and glslang uses that same value for
|
||||
// "this opaque uniform has no location" as well.
|
||||
constexpr Uint kNoLocation = ~static_cast<Uint>(0);
|
||||
// The ceiling glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS) advertises, which is what the
|
||||
// allocator below has to honour: locations 0..kMaxUniformLocations-1 and no others.
|
||||
constexpr Uint kMaxUniformLocations = static_cast<Uint>(ProgramObject::MAX_UNIFORM_LOCATIONS);
|
||||
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
|
||||
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
|
||||
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
|
||||
@@ -884,13 +1083,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
|
||||
}
|
||||
}
|
||||
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) {
|
||||
// glslang parks "no location" at layoutLocationEnd, which is a real location in this
|
||||
// table's numbering - test for it explicitly rather than letting it through as one.
|
||||
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque() &&
|
||||
uniform.layoutLocation() != glslang::TQualifier::layoutLocationEnd) {
|
||||
effectiveLocation[i] = uniform.layoutLocation();
|
||||
}
|
||||
if (locationIsSourceExplicit[i] &&
|
||||
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kNoLocation) {
|
||||
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kMaxUniformLocations) {
|
||||
// Config A rejected out-of-range explicit locations at parse; keep them
|
||||
// from growing the location table unboundedly.
|
||||
// from growing the location table unboundedly. Stated against the advertised
|
||||
// GL_MAX_UNIFORM_LOCATIONS, because that is the rule being enforced (GL 4.6 core
|
||||
// 7.6.1): an array whose LAST element passes the ceiling is a link error even
|
||||
// though its base compiled fine.
|
||||
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
|
||||
effectiveLocation[i]);
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
@@ -898,12 +1103,55 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
|
||||
Int requiredUniformLocations = 0;
|
||||
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: an explicit location is RESERVED
|
||||
// whether or not the uniform turned out to be active. The dead default-block uniforms
|
||||
// filtered out of glUniformIndexToTProgram above are invisible to every GL query - which
|
||||
// is correct - but their locations must still be kept out of the implicit allocator's
|
||||
// reach, or an implicit uniform is handed a location the source already claimed.
|
||||
//
|
||||
// Deliberately NOT written into artifacts.uniformLocations or uniformIndexInTProgram:
|
||||
// glGetUniformLocation must keep answering -1 for a dead uniform, and a location no
|
||||
// application can legally obtain must not become writable through glUniform*. The
|
||||
// occupancy therefore lives in its own bitset, built once the table has been sized.
|
||||
Vector<Pair<Uint, Int>> deadExplicitReservations;
|
||||
Int deadReservedLocationCount = 0;
|
||||
for (Int i = 0; i < tProgramUniformCount; i++) {
|
||||
if (artifacts.tProgramUniformIndexToGl[i] >= 0) continue; // GL-visible: handled above
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
if (!isGlobalUboMember(uniform) || uniform.stages != 0) continue;
|
||||
const Int* explicitLocation = findExplicitLocation(uniform.name);
|
||||
if (explicitLocation == nullptr) continue;
|
||||
|
||||
const Uint location = static_cast<Uint>(*explicitLocation);
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
if (location + static_cast<Uint>(locationSpan) > kMaxUniformLocations) {
|
||||
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
|
||||
location);
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
deadExplicitReservations.emplace_back(location, locationSpan);
|
||||
deadReservedLocationCount += locationSpan;
|
||||
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||
MGLOG_D("ProgramObject %u: Reflection - inactive uniform '%s' reserves locations %u..%u without "
|
||||
"becoming GL-visible",
|
||||
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
|
||||
}
|
||||
|
||||
Int requiredUniformLocations = deadReservedLocationCount;
|
||||
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
|
||||
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
|
||||
// named-block members, which take a slot in this allocator's table (an implementation
|
||||
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
|
||||
// fail a link the spec allows.
|
||||
Int defaultBlockLocationDemand = deadReservedLocationCount;
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
auto& uniform = artifacts.program->getUniform(i);
|
||||
const Uint location = effectiveLocation[i];
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
requiredUniformLocations += locationSpan;
|
||||
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
|
||||
if (location != kNoLocation) {
|
||||
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||
}
|
||||
@@ -916,6 +1164,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
|
||||
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength);
|
||||
|
||||
// GL 4.6 core 7.6.1: explicit, implicit and reserved-but-inactive default-block uniforms
|
||||
// all draw from the one GL_MAX_UNIFORM_LOCATIONS pool, and a program asking for more than
|
||||
// the implementation advertises FAILS TO LINK
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
|
||||
// A single uniform whose own span passes the ceiling was already rejected above; this is
|
||||
// the aggregate half of the same rule.
|
||||
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
|
||||
artifacts.infoLog =
|
||||
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
|
||||
"GL_MAX_UNIFORM_LOCATIONS is {}.",
|
||||
defaultBlockLocationDemand, kMaxUniformLocations);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
|
||||
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
|
||||
"adjusting",
|
||||
@@ -930,6 +1194,27 @@ namespace MobileGL::MG_State::GLState {
|
||||
glslang::TQualifier::layoutLocationEnd);
|
||||
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1);
|
||||
|
||||
// Occupancy for the inactive explicit uniforms collected above: a set bit means "the
|
||||
// source claimed this location", which is enough to keep the two implicit passes off it
|
||||
// without making the location reachable through any GL entry point. A location the
|
||||
// fallback grow path mints later is past this bitset by construction (every reservation
|
||||
// was folded into maxUniformLocation before the table was sized), so the lookup treats
|
||||
// out-of-range as free rather than resizing in lockstep.
|
||||
// Left empty - and unallocated - when nothing reserved anything, which is every program in
|
||||
// the shader-pack corpus; the lookup below reads an empty bitset as "nothing is reserved".
|
||||
Vector<Bool> reservedLocation;
|
||||
if (!deadExplicitReservations.empty()) {
|
||||
reservedLocation.assign(artifacts.maxUniformLocation + 1, false);
|
||||
for (const auto& [reservedBase, reservedSpan] : deadExplicitReservations) {
|
||||
for (Int element = 0; element < reservedSpan; ++element) {
|
||||
reservedLocation[reservedBase + element] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto locationIsReserved = [&reservedLocation](SizeT location) {
|
||||
return location < reservedLocation.size() && reservedLocation[location];
|
||||
};
|
||||
|
||||
Vector<int> unallocatedUniformIndex;
|
||||
|
||||
// Pass 1: source-explicit locations. These are API contract
|
||||
@@ -974,7 +1259,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
|
||||
for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
|
||||
spanIsFree =
|
||||
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd;
|
||||
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd &&
|
||||
!locationIsReserved(location + element);
|
||||
}
|
||||
if (!spanIsFree) {
|
||||
artifacts.uniformLocations[uniform.name] = kNoLocation;
|
||||
@@ -1006,7 +1292,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
|
||||
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
|
||||
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
|
||||
glslang::TQualifier::layoutLocationEnd;
|
||||
glslang::TQualifier::layoutLocationEnd &&
|
||||
!locationIsReserved(locNeedle + element);
|
||||
}
|
||||
if (!hasRoom) continue;
|
||||
// Found a vacant location at locNeedle
|
||||
@@ -1239,7 +1526,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr;
|
||||
artifacts.atomicCounterCount = program.getNumAtomicCounters();
|
||||
for (Uint dim = 0; dim < 3u; ++dim) {
|
||||
artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast<Int>(dim));
|
||||
}
|
||||
|
||||
@@ -615,15 +615,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
|
||||
Int ProgramObject::GetFragmentDataLocation(const char* name) {
|
||||
if (!Artifacts().program || !name) return -1;
|
||||
// Answered from the OWNED pipe-output snapshot, not from Artifacts().program. The live
|
||||
// TProgram is null on a translation-cache L1 hit - that is the entire point of the memo
|
||||
// - and it is also null for any program that never linked. The old `if
|
||||
// (!Artifacts().program) return -1` guard silently produced the never-linked answer for
|
||||
// a perfectly good cached program, so glGetFragDataLocation returned -1 for every
|
||||
// fragment output of it. The empty snapshot gives the never-linked case the same -1
|
||||
// without needing the guard at all.
|
||||
if (!name) return -1;
|
||||
|
||||
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
|
||||
const Int outputCount = Artifacts().program->getNumPipeOutputs();
|
||||
for (Int index = 0; index < outputCount; ++index) {
|
||||
const auto& output = Artifacts().program->getPipeOutput(index);
|
||||
for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
|
||||
if (output.name != name) continue;
|
||||
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast<Int>(explicitLocation->second);
|
||||
return static_cast<Int>(output.layoutLocation());
|
||||
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
|
||||
return static_cast<Int>(explicitLocation->second);
|
||||
}
|
||||
return output.location;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
class ProgramObject {
|
||||
public:
|
||||
// GL_MAX_UNIFORM_LOCATIONS: locations 0 .. MAX_UNIFORM_LOCATIONS-1 are the whole legal
|
||||
// range (GL 4.6 core 7.6.1 / ARB_explicit_uniform_location). Shared with GL_Getter rather
|
||||
// than spelled twice, because the link and the query must agree exactly - the CTS declares
|
||||
// a uniform at the advertised value minus one and expects it to link
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-max).
|
||||
//
|
||||
// Tied to glslang's own ceiling and NOT raisable past it: ParseHelper rejects
|
||||
// `layout(location = N)` for N >= TQualifier::layoutLocationEnd at COMPILE time, so
|
||||
// layoutLocationEnd - 1 is the largest location any shader in this stack can declare -
|
||||
// which makes exactly layoutLocationEnd locations, 0 .. layoutLocationEnd - 1, the pool.
|
||||
// Advertising more would promise a location no shader could name. Comfortably above the
|
||||
// 1024 GL 4.3 requires.
|
||||
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
|
||||
|
||||
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
|
||||
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||
// AST, so a POD covers the whole surface exactly.
|
||||
@@ -761,9 +775,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
|
||||
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
|
||||
// throw ahead of it stopped killing the run first.
|
||||
Int GetActiveAtomicCounterCount() const {
|
||||
return Artifacts().atomicCounterCount;
|
||||
}
|
||||
Int GetActiveAttributesCount() const {
|
||||
return static_cast<Int>(Artifacts().pipeInputReflection.size());
|
||||
}
|
||||
@@ -1002,7 +1013,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||
// separate-programs-tess-control).
|
||||
Bool lastStageIsFragment = false;
|
||||
Int atomicCounterCount = 0;
|
||||
Array<GLuint, 3> computeLocalSize{};
|
||||
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||
// TProgram uniform index.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "ShaderCompileTask.h"
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
|
||||
#include <glslang/Include/PoolAlloc.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
|
||||
namespace {
|
||||
@@ -137,8 +139,21 @@ namespace {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
|
||||
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
|
||||
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
|
||||
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
|
||||
// number the application was told, not against either half of it.
|
||||
static MobileGL::Int MaxShaderStorageBufferBindings(
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
const MobileGL::Int frontendPoints =
|
||||
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
|
||||
if (!env.HasBackend()) return frontendPoints;
|
||||
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
|
||||
}
|
||||
|
||||
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical
|
||||
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
|
||||
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
||||
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
||||
// Deliberately free of any per-object state so the memo is sound.
|
||||
@@ -172,6 +187,13 @@ namespace {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (const std::optional<String> bindingError = FindShaderStorageBindingViolation(
|
||||
result.preprocessedSource, MaxShaderStorageBufferBindings(env))) {
|
||||
result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected;
|
||||
result.infoLog = *bindingError;
|
||||
return result;
|
||||
}
|
||||
|
||||
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
||||
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
||||
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
||||
|
||||
@@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
ComputeLocalSizeRejected,
|
||||
// FindReservedIdentifierViolation rejected it.
|
||||
ReservedIdentifierRejected,
|
||||
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
|
||||
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
||||
ResourceBindingRejected,
|
||||
// The source-only half was clean but glslang rejected the preprocessed source.
|
||||
// Memoizing this saves the parse itself on every later object with that source.
|
||||
ParseFailed,
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace MobileGL {
|
||||
m_dirtyRects.resize(requiredLevelCount);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
@@ -79,6 +80,9 @@ namespace MobileGL {
|
||||
m_compressedFormats[level] = GL_NONE;
|
||||
m_compressedData[level].clear();
|
||||
m_compressedData[level].shrink_to_fit();
|
||||
// Same story for the requested-format tag: a respecified level is whatever this
|
||||
// call asked for, and the compressed entry points re-arm it right afterwards.
|
||||
m_requestedCompressedFormats[level] = GL_NONE;
|
||||
}
|
||||
|
||||
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
|
||||
@@ -110,6 +114,16 @@ namespace MobileGL {
|
||||
return m_compressedData[level].data();
|
||||
}
|
||||
|
||||
void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) {
|
||||
if (level >= m_requestedCompressedFormats.size()) return;
|
||||
m_requestedCompressedFormats[level] = internalFormat;
|
||||
}
|
||||
|
||||
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
|
||||
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
|
||||
return m_requestedCompressedFormats[level];
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
if (levelCount >= m_data.size()) return;
|
||||
|
||||
@@ -120,6 +134,7 @@ namespace MobileGL {
|
||||
m_dirtyRects.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
m_requestedCompressedFormats.resize(levelCount);
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
|
||||
@@ -96,6 +96,18 @@ namespace MobileGL {
|
||||
SizeT GetCompressedByteSize(Uint level) const;
|
||||
const void* MapCompressedData(Uint level) const;
|
||||
|
||||
// The compressed internalformat the application ASKED for, which is not the same
|
||||
// question as the one above: the six generic GL_COMPRESSED_* enums let the
|
||||
// implementation choose, MobileGL chooses uncompressed storage, and the level is
|
||||
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
|
||||
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
|
||||
// points that must refuse a compressed image outright (glClearTexImage /
|
||||
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
|
||||
// recorded separately. Set right after AllocateLevel, which clears it.
|
||||
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
|
||||
// GL_NONE when the level was not requested with a compressed internalformat.
|
||||
GLenum GetRequestedCompressedFormat(Uint level) const;
|
||||
|
||||
protected:
|
||||
// Insert one clamped, non-empty write box, keeping the list disjoint
|
||||
// and bounded (see kMaxDirtyRects).
|
||||
@@ -115,6 +127,7 @@ namespace MobileGL {
|
||||
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
Vector<GLenum> m_requestedCompressedFormats;
|
||||
};
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -111,6 +111,16 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].MapCompressedData(level);
|
||||
}
|
||||
|
||||
void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid");
|
||||
m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat);
|
||||
}
|
||||
|
||||
GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid");
|
||||
return m_storage[targetIndex].GetRequestedCompressedFormat(level);
|
||||
}
|
||||
|
||||
protected:
|
||||
Array<MipmapStorage, TargetCount> m_storage;
|
||||
};
|
||||
|
||||
@@ -250,6 +250,10 @@ namespace MobileGL {
|
||||
return m_contentVersion;
|
||||
}
|
||||
|
||||
Uint64 TextureObjectBase::GetShapeVersion() const {
|
||||
return m_shapeVersion;
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
|
||||
const int slot = mipmapped ? 1 : 0;
|
||||
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
|
||||
@@ -373,6 +377,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel, GLenum internalFormat) {
|
||||
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat);
|
||||
}
|
||||
|
||||
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
|
||||
mipmapLevel);
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
|
||||
if (m_textureStorage.GetLevelCount() == 0) {
|
||||
return {0, 0, 0};
|
||||
|
||||
@@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
|
||||
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
|
||||
virtual Uint64 GetContentVersion() const = 0;
|
||||
// Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level
|
||||
// set, the internal format, the level range (see BumpShapeVersion). Disjoint from the
|
||||
// content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size
|
||||
// without dirtying a single texel, so a backend that keys its "nothing changed since
|
||||
// the last sync" skip on content alone keeps a resource of the OLD size alive.
|
||||
virtual Uint64 GetShapeVersion() const = 0;
|
||||
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
|
||||
// property of the texture's SHAPE - level sizes, level count, level range,
|
||||
// internal format - and never of its texel content, but every draw asks about
|
||||
@@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetImmutableLevels(Uint levels) override;
|
||||
Uint16 GetTextureParamsVersion() const override;
|
||||
Uint64 GetContentVersion() const override;
|
||||
Uint64 GetShapeVersion() const override;
|
||||
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
|
||||
// Bumps the content version without touching per-level storage-dirty flags. Used when the
|
||||
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
|
||||
@@ -220,6 +227,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
|
||||
// The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
|
||||
// answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
|
||||
// MipmapStorage. Only the entry points GL forbids on a compressed image read it.
|
||||
virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) = 0;
|
||||
// GL_NONE when the level was not requested with a compressed internalformat.
|
||||
virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const = 0;
|
||||
};
|
||||
|
||||
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
|
||||
@@ -286,6 +302,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) override;
|
||||
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
@@ -96,6 +96,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel, GLenum internalFormat) {
|
||||
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat);
|
||||
}
|
||||
|
||||
GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
|
||||
mipmapLevel);
|
||||
}
|
||||
|
||||
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
|
||||
target <= TextureUploadTarget::CubeMapNegativeZ,
|
||||
|
||||
@@ -39,6 +39,10 @@ namespace MobileGL {
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat) override;
|
||||
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
Reference in New Issue
Block a user