mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Merge] (DirectGLES, ShaderTranspiler, Config): land the viewport-array routing emulation
This commit is contained in:
@@ -212,6 +212,19 @@ namespace MobileGL::MG_Config {
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
// it is ON even where the driver advertises GL_OES_viewport_array, because that
|
||||
// extension only ever gave the SHADER a compilable name: MobileGL has never
|
||||
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
|
||||
// and nothing else), so on an extension-capable driver every index rasterized as
|
||||
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
|
||||
// the pre-emulation path, extension passthrough where it exists and
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -195,6 +195,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.ViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -3267,6 +3267,188 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
||||
}
|
||||
|
||||
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
||||
// See the block comment in Managers.h for what this is and why. Here is the state half: one
|
||||
// replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one
|
||||
// viewport / one scissor / one depth range and telling the fragment gate which indices it
|
||||
// serves.
|
||||
namespace ViewportRoutingImpl {
|
||||
// One replay pass: the state to push, and the set of gl_ViewportIndex values whose
|
||||
// fragments this pass is allowed to keep.
|
||||
struct RoutingPass {
|
||||
IntVec4 viewport{};
|
||||
IntVec4 scissorBox{};
|
||||
FloatVec2 depthRange{};
|
||||
Bool scissorTest = false;
|
||||
Uint32 indexMask = 0;
|
||||
};
|
||||
|
||||
static constexpr Uint32 kAllViewportsMask =
|
||||
(RenderStateParameters::MAX_VIEWPORTS >= 32)
|
||||
? 0xFFFFFFFFu
|
||||
: ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u);
|
||||
|
||||
// The plan for the draw currently being issued. A file-scope buffer rather than a return
|
||||
// value because Begin/Apply/End are three calls around a draw the caller writes, and a
|
||||
// fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant,
|
||||
// which is a property of the call sites and not an accident: every wrap in this file and
|
||||
// in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside
|
||||
// another - and a multi-draw tier that replayed its whole loop would be nesting.
|
||||
static Array<RoutingPass, RenderStateParameters::MAX_VIEWPORTS> g_passes{};
|
||||
static Uint g_passCount = 0;
|
||||
static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr;
|
||||
|
||||
// What index `i` actually rasterizes against, resolved exactly the way SyncRenderState
|
||||
// resolves index 0 - including both substitutions it makes, which are not cosmetic:
|
||||
//
|
||||
// * a viewport of zero extent means "the application has never called glViewport", and
|
||||
// GL's initial viewport is the whole surface, which the frontend cannot spell before
|
||||
// a surface exists;
|
||||
// * a scissor rectangle is read through the WRITTEN flag and not through its extent,
|
||||
// because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment"
|
||||
// and is byte-identical to the never-written default that means the opposite.
|
||||
//
|
||||
// Resolving them here rather than deferring to SyncRenderState is what makes the grouping
|
||||
// below correct: two indices that differ only in a field that resolves to the same
|
||||
// rectangle really do rasterize identically and must share one pass.
|
||||
static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index,
|
||||
Int surfaceWidth, Int surfaceHeight) {
|
||||
RoutingPass pass;
|
||||
const FloatVec4& viewport = parameters.Viewports[index];
|
||||
pass.viewport = IntVec4(static_cast<Int>(std::lround(viewport.x())),
|
||||
static_cast<Int>(std::lround(viewport.y())),
|
||||
static_cast<Int>(std::lround(viewport.z())),
|
||||
static_cast<Int>(std::lround(viewport.w())));
|
||||
if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) {
|
||||
pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
||||
}
|
||||
pass.scissorBox = parameters.ScissorBoxes[index];
|
||||
if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 &&
|
||||
surfaceHeight > 0) {
|
||||
pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
||||
}
|
||||
pass.depthRange = parameters.DepthRanges[index];
|
||||
pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
||||
return pass;
|
||||
}
|
||||
|
||||
static Bool SameState(const RoutingPass& a, const RoutingPass& b) {
|
||||
return a.viewport == b.viewport && a.scissorBox == b.scissorBox &&
|
||||
a.depthRange == b.depthRange && a.scissorTest == b.scissorTest;
|
||||
}
|
||||
} // namespace ViewportRoutingImpl
|
||||
|
||||
Uint BeginViewportRoutingPasses() {
|
||||
using namespace ViewportRoutingImpl;
|
||||
g_passCount = 1;
|
||||
g_routedProgram = nullptr;
|
||||
|
||||
// The whole emulation behind one static load, for every application that has never built
|
||||
// a program writing gl_ViewportIndex - which is all of them but the conformance suite.
|
||||
// Without it every draw in the process would pay GetCurrentBackendProgram's chain of
|
||||
// frontend lookups for an answer that cannot change.
|
||||
if (!g_anyProgramRoutesViewportIndex) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto* program = GetCurrentBackendProgram();
|
||||
if (program == nullptr || !program->RoutesViewportIndex()) {
|
||||
return 1;
|
||||
}
|
||||
g_routedProgram = program;
|
||||
// The gate reads zero until something writes it, and a zero mask discards every fragment.
|
||||
// So this is not an optimization that can be skipped in the one-pass case - it is what
|
||||
// keeps a routing program drawing at all.
|
||||
program->SetViewportPassMask(kAllViewportsMask);
|
||||
|
||||
// Replaying multiplies every side effect the vertex and geometry stages have, and the
|
||||
// fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform
|
||||
// feedback records per emitted primitive, so a replayed draw would write its vertices N
|
||||
// times; rasterizer discard means there are no fragments to gate at all, so replaying
|
||||
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
|
||||
// open gate, i.e. to the pre-emulation behaviour, rather than to wrong data.
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
||||
surfaceWidth = 0;
|
||||
surfaceHeight = 0;
|
||||
}
|
||||
|
||||
Uint count = 0;
|
||||
for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) {
|
||||
const RoutingPass resolved =
|
||||
ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight);
|
||||
Uint existing = 0;
|
||||
for (; existing < count; ++existing) {
|
||||
if (SameState(g_passes[existing], resolved)) break;
|
||||
}
|
||||
if (existing == count) {
|
||||
g_passes[count] = resolved;
|
||||
++count;
|
||||
}
|
||||
g_passes[existing].indexMask |= (1u << index);
|
||||
}
|
||||
|
||||
// One group is the overwhelmingly common case - it is what glViewport, glScissor and
|
||||
// glDepthRange leave behind, because ARB_viewport_array defines all three as writing
|
||||
// EVERY index. The mask is already open and index 0's state is what SyncRenderState
|
||||
// pushed, so there is nothing to replay and nothing to restore.
|
||||
if (count <= 1) {
|
||||
g_passCount = 1;
|
||||
return 1;
|
||||
}
|
||||
g_passCount = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
void ApplyViewportRoutingPass(Uint pass) {
|
||||
using namespace ViewportRoutingImpl;
|
||||
if (pass >= g_passCount || g_routedProgram == nullptr) {
|
||||
return;
|
||||
}
|
||||
const RoutingPass& entry = g_passes[pass];
|
||||
g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(),
|
||||
entry.viewport.w());
|
||||
g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(),
|
||||
entry.scissorBox.w());
|
||||
// ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a
|
||||
// per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has
|
||||
// no per-viewport scissor toggle either and has to widen a disabled index's rectangle to
|
||||
// the whole framebuffer instead); here the rectangle stays honest.
|
||||
entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y());
|
||||
g_routedProgram->SetViewportPassMask(entry.indexMask);
|
||||
}
|
||||
|
||||
void EndViewportRoutingPasses(Uint passCount) {
|
||||
using namespace ViewportRoutingImpl;
|
||||
if (passCount <= 1) {
|
||||
// Nothing was pushed and the mask is already open; leaving the shadow alone here is
|
||||
// what keeps a non-routing draw at exactly its previous cost.
|
||||
g_routedProgram = nullptr;
|
||||
return;
|
||||
}
|
||||
if (g_routedProgram != nullptr) {
|
||||
// Any draw that reaches the driver without going through a replay - an internal blit,
|
||||
// or a path this emulation has not been taught about - must not inherit the last
|
||||
// pass's mask and paint nothing.
|
||||
g_routedProgram->SetViewportPassMask(kAllViewportsMask);
|
||||
}
|
||||
g_routedProgram = nullptr;
|
||||
g_passCount = 0;
|
||||
// The viewport, scissor, scissor-test enable and depth range now on the ES context belong
|
||||
// to the last replay pass, and the shadow SyncRenderState diffs against does not know it.
|
||||
// A full resync is the honest repair and costs one state push on the next draw, which
|
||||
// only a viewport-routing workload ever pays.
|
||||
RenderStateImpl::InvalidateSyncedRenderState();
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
@@ -3324,7 +3506,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
SetCurrentBaseVertex(cmd.baseVertex);
|
||||
}
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -3337,9 +3521,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
SetCurrentBaseVertex(cmd.baseVertex);
|
||||
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
|
||||
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
|
||||
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
|
||||
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
|
||||
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
|
||||
});
|
||||
}
|
||||
}
|
||||
SetCurrentDrawID(0);
|
||||
@@ -3378,7 +3564,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
}
|
||||
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -3389,9 +3577,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
|
||||
static_cast<GLsizei>(cmd.count),
|
||||
static_cast<GLsizei>(cmd.instanceCount));
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
|
||||
static_cast<GLsizei>(cmd.count),
|
||||
static_cast<GLsizei>(cmd.instanceCount));
|
||||
});
|
||||
}
|
||||
}
|
||||
SetCurrentDrawID(0);
|
||||
@@ -3617,7 +3807,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
g_GLESFuncs.glDrawElements(mode, count, type, indices);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, count, type, indices);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
@@ -3633,7 +3825,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first, count);
|
||||
}
|
||||
}
|
||||
g_GLESFuncs.glDrawArrays(mode, first, count);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawArrays(mode, first, count);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
@@ -3644,7 +3838,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
@@ -3670,7 +3866,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
|
||||
});
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
}
|
||||
@@ -3907,14 +4105,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||
});
|
||||
}
|
||||
|
||||
// True when the driver will apply baseInstance to the vertex fetch itself, in which case the
|
||||
@@ -3937,12 +4139,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
basevertex, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
ForEachViewportRoutingPass([&] {
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
basevertex, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
@@ -3952,7 +4156,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
@@ -3962,19 +4168,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
ForEachViewportRoutingPass([&] {
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
});
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
@@ -4006,18 +4216,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
ForEachViewportRoutingPass([&] {
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
});
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
|
||||
@@ -49,6 +49,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
|
||||
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
|
||||
|
||||
// See the block comment on ForEachViewportRoutingPass in Managers.h. Auto is ON, including on
|
||||
// a driver that advertises GL_OES_viewport_array: that extension gives the shader a name, not
|
||||
// the driver fifteen more rectangles to rasterize against, and nothing in MobileGL has ever
|
||||
// programmed the indexed state it would need.
|
||||
Bool ViewportArrayEmulationEnabled() {
|
||||
return MG_Config::Features.ViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
Bool g_anyProgramRoutesViewportIndex = false;
|
||||
|
||||
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
|
||||
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
|
||||
// points. Those slots are taken from the TOP of the range downwards - below the one
|
||||
@@ -302,6 +312,126 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return source;
|
||||
}
|
||||
|
||||
// ---- gl_ViewportIndex routing emulation, ESSL half ---------------------------------------
|
||||
//
|
||||
// LowerViewportIndexPass has already turned the BuiltIn ViewportIndex OUTPUT into a plain
|
||||
// Private global, so SPIRV-Cross printed `int mg_ViewportIndex;` at file scope and the stage
|
||||
// still stores the index the application asked for - it just goes nowhere. The two passes
|
||||
// below give it somewhere to go WITHOUT naming a builtin the language does not have: the
|
||||
// producing stage's global becomes an ordinary flat varying, and the fragment stage gets a
|
||||
// gate that discards every fragment whose primitive routed to a viewport the current replay
|
||||
// pass is not drawing. DirectGLES.cpp's ForEachViewportRoutingPass is the other half - it
|
||||
// re-issues the draw once per distinct viewport state with the real
|
||||
// glViewport/glScissor/glDepthRangef pushed for it and this uniform set to the set of
|
||||
// indices that state serves.
|
||||
//
|
||||
// FLAT is semantics, not performance: GL takes a primitive's viewport index from its
|
||||
// PROVOKING VERTEX, which is exactly what flat interpolation delivers, so a primitive whose
|
||||
// vertices carry different indices routes the way the spec says with no extra machinery.
|
||||
//
|
||||
// NO layout(location = N) on either side, deliberately. The two stages are transpiled
|
||||
// independently and neither can see the other's location assignment: the producing stage
|
||||
// knows its own outputs, the fragment stage only the subset it consumes, and a number derived
|
||||
// from either can disagree with the other. Leaving both unqualified hands the assignment to
|
||||
// the driver's linker, which then matches them BY NAME - the ordinary GLSL rule, and the only
|
||||
// one that needs no cross-stage channel. The cost is one varying slot, which a program
|
||||
// already at GL_MAX_VARYING_VECTORS cannot spare.
|
||||
constexpr const char* VIEWPORT_INDEX_VARYING_NAME = "mg_ViewportIndex";
|
||||
constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask";
|
||||
constexpr const char* VIEWPORT_GATED_ENTRY_POINT_NAME = "mg_ViewportGatedMain";
|
||||
constexpr const char* ESSL_ENTRY_POINT_SIGNATURE = "void main()";
|
||||
static_assert(RenderStateParameters::MAX_VIEWPORTS == 16,
|
||||
"the fragment gate below spells the index clamp as `& 15` and the pass mask as a "
|
||||
"16-bit int; both follow MAX_VIEWPORTS and have to be respelled with it");
|
||||
|
||||
// Producing stage (vertex / tessellation evaluation / geometry - the three GL lets write the
|
||||
// builtin). Returns whether the demoted global was found and promoted, which is also the
|
||||
// answer to "does this program route viewports at all".
|
||||
Bool PromoteViewportIndexGlobalToVarying(String& source) {
|
||||
// The same shape PromoteDrawParameterGlobalsToUniforms matches, and for the same reason:
|
||||
// SPIRV-Cross prints the demoted global with or without a precision qualifier depending
|
||||
// on what the module carried. Only a declaration that starts its own line may be
|
||||
// rewritten - `mg_ViewportIndex = gl_InvocationID;` in the body contains the name too and
|
||||
// has to be left exactly as it is.
|
||||
const String declared = String(VIEWPORT_INDEX_VARYING_NAME) + ";";
|
||||
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
|
||||
const String declaration = String(declPrefix) + declared;
|
||||
const SizeT pos = source.find(declaration);
|
||||
if (pos == String::npos) {
|
||||
continue;
|
||||
}
|
||||
// Column 0 of its own line is what separates the declaration from the tail of any
|
||||
// other declaration or expression that ends in the same name.
|
||||
if (pos != 0 && source[pos - 1] != '\n') {
|
||||
continue;
|
||||
}
|
||||
source.replace(pos, declaration.size(),
|
||||
String("flat out highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fragment stage. Returns false when the stage has no entry point to gate onto, which the
|
||||
// caller reports: the program still links and still renders, it just renders every index
|
||||
// with the first replay pass's state - i.e. it degrades to the pre-emulation behaviour
|
||||
// rather than to a black screen.
|
||||
Bool InjectViewportIndexPassGate(String& source) {
|
||||
// Built beside the input and swapped in only on success, so a stage this pass declines
|
||||
// reaches the driver exactly as it arrived rather than half-rewritten.
|
||||
// A fragment stage that READS gl_ViewportIndex has no ESSL spelling for it either -
|
||||
// LowerViewportIndexPass deliberately demotes only OUTPUTS, because a demoted INPUT would
|
||||
// answer from an undefined Private global. Now that the routing varying exists and
|
||||
// carries the real per-primitive value, that read has somewhere honest to go.
|
||||
String gated = ReplaceIdentifier(source, "gl_ViewportIndex", VIEWPORT_INDEX_VARYING_NAME);
|
||||
|
||||
const SizeT entryPos = gated.find(ESSL_ENTRY_POINT_SIGNATURE);
|
||||
if (entryPos == String::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Declarations go immediately before the entry point rather than after #version: that
|
||||
// position is already past every #extension directive (which must precede any other
|
||||
// token) and past everything the body can name, so it can invalidate neither.
|
||||
//
|
||||
// Renaming the entry point rather than splicing a prologue into its body keeps the
|
||||
// application's code byte-identical, including an early `return`.
|
||||
String preamble = String("flat in highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";\n";
|
||||
preamble += String("uniform highp int ") + VIEWPORT_PASS_MASK_UNIFORM_NAME + ";\n";
|
||||
preamble += String("void ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "()";
|
||||
gated.replace(entryPos, std::strlen(ESSL_ENTRY_POINT_SIGNATURE), preamble);
|
||||
|
||||
// `& 15` clamps the shift operand into range for MAX_VIEWPORTS = 16. GL leaves an index
|
||||
// outside [0, MAX_VIEWPORTS) undefined, but an ESSL shift by >= 32 is undefined in a way
|
||||
// that can take the whole draw with it, so the emulation picks a defined answer instead.
|
||||
//
|
||||
// The mask, not an equality test against a pass number: viewport indices whose whole
|
||||
// state tuple is identical share ONE replay pass (see BeginViewportRoutingPasses), and
|
||||
// the overwhelmingly common case - every index still holding what glViewport broadcast -
|
||||
// is then a single pass with every bit set, i.e. a gate that discards nothing and a draw
|
||||
// that is issued exactly once.
|
||||
//
|
||||
// PERFORMANCE NOTE: a fragment shader containing `discard` cannot take the early-Z fast
|
||||
// path on a tiler, so a routed draw pays late-Z on top of its N replay passes. Accepted
|
||||
// deliberately: this runs only for a program that writes gl_ViewportIndex, and that is
|
||||
// why the gate is injected per program rather than into every fragment shader.
|
||||
gated += "\n";
|
||||
gated += String(ESSL_ENTRY_POINT_SIGNATURE) + "\n";
|
||||
gated += "{\n";
|
||||
gated += String(" if (((") + VIEWPORT_PASS_MASK_UNIFORM_NAME + " >> (" +
|
||||
VIEWPORT_INDEX_VARYING_NAME + " & 15)) & 1) == 0)\n";
|
||||
gated += " {\n";
|
||||
gated += " discard;\n";
|
||||
gated += " }\n";
|
||||
gated += " else\n";
|
||||
gated += " {\n";
|
||||
gated += String(" ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "();\n";
|
||||
gated += " }\n";
|
||||
gated += "}\n";
|
||||
source = std::move(gated);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The transpile pipeline invents image binding numbers: when the GL source declares
|
||||
// an image uniform without layout(binding), glslang auto-assigns one (desktop GL
|
||||
// allows that and lets the app pick the unit with glUniform1i, which ES forbids on
|
||||
@@ -5583,7 +5713,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// keep the two in step.
|
||||
const Int advertisedMaxSamples =
|
||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
||||
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
|
||||
// by default (MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
|
||||
// reason to keep the builtin: it only ever gave the SHADER a compilable name, while
|
||||
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
|
||||
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
|
||||
// rasterized every index as index 0 exactly like a driver without it. Lowering here
|
||||
// is what lets the ESSL passes downstream turn the builtin into the flat varying the
|
||||
// replay gates on.
|
||||
//
|
||||
// Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the
|
||||
// geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation
|
||||
// evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass
|
||||
// declines anyway, and a compute stage has none - so arming those two only ever
|
||||
// bought them the shared probe's BuildModule for nothing.
|
||||
const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER ||
|
||||
glShaderType == GL_TESS_EVALUATION_SHADER ||
|
||||
glShaderType == GL_GEOMETRY_SHADER;
|
||||
const Bool viewportLoweringArmed =
|
||||
stageCanWriteViewportIndex &&
|
||||
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
|
||||
const Bool sampleClampArmed =
|
||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||
@@ -5607,11 +5756,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
||||
!loweredViewportSpirv.empty()) {
|
||||
effectiveSpirv = &loweredViewportSpirv;
|
||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
||||
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
||||
"global; every invocation renders into viewport 0.",
|
||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core "
|
||||
"spelling for. The builtin was demoted to a plain global; %s.",
|
||||
m_backendProgramId,
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
ViewportArrayEmulationEnabled()
|
||||
? "the ESSL passes below promote it to a routing varying"
|
||||
: "every invocation renders into viewport 0");
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
||||
@@ -6323,7 +6474,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
|
||||
|
||||
// The stage order the loop below walks, with every FRAGMENT stage moved to the end.
|
||||
// The viewport-routing gate is the reason: whether a fragment stage needs one is a
|
||||
// question about the OTHER stages ("does any of them still write gl_ViewportIndex?"),
|
||||
// and the honest, free answer to it is the promotion the producing stage's own text
|
||||
// pass just performed. Answering it any other way costs a BuildModule per
|
||||
// pre-rasterization stage of every program - the parse the shared SpirvGateFeatures
|
||||
// probe exists to avoid. Nothing else in the loop is order-sensitive: the two
|
||||
// passthrough-tessellation sources it captures are a vertex and an evaluation stage,
|
||||
// and the three sets it accumulates are unions.
|
||||
Vector<SizeT> stageOrder;
|
||||
stageOrder.reserve(linkedStages.size());
|
||||
for (SizeT index = 0; index < linkedStages.size(); ++index) {
|
||||
if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index);
|
||||
}
|
||||
for (SizeT index = 0; index < linkedStages.size(); ++index) {
|
||||
if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index);
|
||||
}
|
||||
// Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text
|
||||
// pass turned into a varying; read by the fragment stage to decide whether to inject
|
||||
// the gate that consumes it.
|
||||
Bool programRoutesViewportIndex = false;
|
||||
// No fragment stage, no gate - and without a gate the promotion below would only add
|
||||
// an output nothing can read. That is not merely useless: in a separable program
|
||||
// pipeline the fragment stage lives in a DIFFERENT program, which never saw this
|
||||
// build and cannot be given a gate, so promoting there would hang an unmatched
|
||||
// varying off a program to buy nothing. Both cases keep the pre-emulation behaviour,
|
||||
// which is what a program with no fragment stage had anyway.
|
||||
const Bool programHasFragmentStage =
|
||||
std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) !=
|
||||
linkedStages.end();
|
||||
const Bool viewportEmulationForThisProgram =
|
||||
ViewportArrayEmulationEnabled() && programHasFragmentStage;
|
||||
|
||||
for (const SizeT index : stageOrder) {
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]);
|
||||
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
|
||||
|
||||
@@ -6362,7 +6546,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
|
||||
esslKeyInputs.spirv = &spirvCode;
|
||||
esslKeyInputs.shaderType = glShaderType;
|
||||
esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray;
|
||||
// The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it.
|
||||
// Duplicated rather than shared because the two live on opposite sides of the
|
||||
// memo boundary - and a key that disagrees with the pass it is keying is the one
|
||||
// failure mode of this cache that renders wrong pixels instead of being slow.
|
||||
esslKeyInputs.viewportIndexLoweringArmed =
|
||||
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER ||
|
||||
glShaderType == GL_GEOMETRY_SHADER) &&
|
||||
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
|
||||
esslKeyInputs.supportsNoperspectiveInterpolation =
|
||||
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
|
||||
esslKeyInputs.supportsExtendedImageFormats =
|
||||
@@ -6517,8 +6708,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// name; a driver without the extension took the LowerViewportIndexPass fallback
|
||||
// above and its source no longer names the builtin at all, so the two are mutually
|
||||
// exclusive by construction. Read `source` BEFORE it is moved from.
|
||||
const Bool needsViewportArrayExtension = g_GLESCapabilities.SupportsViewportArray &&
|
||||
source.find("gl_ViewportIndex") != String::npos;
|
||||
// The routing emulation is the third way this can be reached and the only one
|
||||
// that needs no directive: it renames the fragment stage's read onto the varying
|
||||
// the producing stage now writes, a few passes below.
|
||||
const Bool needsViewportArrayExtension =
|
||||
g_GLESCapabilities.SupportsViewportArray &&
|
||||
!(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) &&
|
||||
source.find("gl_ViewportIndex") != String::npos;
|
||||
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
@@ -6579,6 +6775,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe());
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
||||
// The two halves of the gl_ViewportIndex routing emulation, next to the draw-
|
||||
// parameter promotion because they are the same shape: a builtin ESSL cannot
|
||||
// spell, demoted to a plain global by a SPIR-V pass, given a real interface here.
|
||||
// BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to
|
||||
// the top land above the declarations these inject; AFTER
|
||||
// ForceFlatIntegerVaryings, which matches only declarations carrying a
|
||||
// layout(...) qualifier and so cannot touch either of them.
|
||||
if (viewportEmulationForThisProgram) {
|
||||
if (glShaderType == GL_FRAGMENT_SHADER) {
|
||||
if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) {
|
||||
// MGLOG_E, unlatched, like the transpile- and compile-failure
|
||||
// diagnostics around it: the program still links and still draws, so
|
||||
// nothing else in the process will ever say that its viewport routing
|
||||
// silently collapsed back to one rectangle.
|
||||
MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no "
|
||||
"entry point to gate, so the routing cannot be emulated: every "
|
||||
"index will rasterize against viewport 0. State program ID: %u.",
|
||||
m_backendProgramId, stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
} else if (PromoteViewportIndexGlobalToVarying(source)) {
|
||||
programRoutesViewportIndex = true;
|
||||
}
|
||||
}
|
||||
source = ForceSupporterOutput(source);
|
||||
source = ClampNormFallbackOutputs(std::move(source), glShaderType,
|
||||
m_snormFallbackClampOutputMask,
|
||||
@@ -6781,6 +7000,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BASE_VERTEX_UNIFORM_NAME);
|
||||
m_baseInstanceWordIndexUniformLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
|
||||
// Asked of the DRIVER rather than remembered from the injection, deliberately: the
|
||||
// gate is only real if the uniform survived compilation and linking, and this is the
|
||||
// one question whose answer covers both. A gate the driver optimized away would
|
||||
// otherwise leave the draw path replaying passes whose mask reaches nothing, which
|
||||
// renders every index's primitives in every pass.
|
||||
m_viewportPassMaskUniformLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME);
|
||||
if (m_viewportPassMaskUniformLocation >= 0) {
|
||||
// Sticky, and never cleared on a relink: it only ever short-circuits a per-draw
|
||||
// check, so being late to go false costs a pointer compare and being late to go
|
||||
// true would cost correctness.
|
||||
g_anyProgramRoutesViewportIndex = true;
|
||||
}
|
||||
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
|
||||
// SSBO blocks after compile); record it so draws bind the indirect buffer there.
|
||||
m_indirectParamsBinding = -1;
|
||||
@@ -6983,6 +7215,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast<GLint>(drawId));
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const {
|
||||
if (m_viewportPassMaskUniformLocation < 0) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast<GLint>(indexMask));
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
|
||||
@@ -21,6 +21,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
||||
|
||||
// The ESSL half of the gl_ViewportIndex routing emulation, in the order a program's stages
|
||||
// meet it. Both are pure String -> String rewrites over what SPIRV-Cross emitted once
|
||||
// LowerViewportIndexPass has demoted the builtin to the plain global `mg_ViewportIndex`.
|
||||
//
|
||||
// The producing stage's global becomes an ordinary flat varying; true when there was one to
|
||||
// promote, which is also the answer to "does this program route viewports at all".
|
||||
Bool PromoteViewportIndexGlobalToVarying(String& source);
|
||||
// The fragment stage grows a matching flat input, the mg_ViewportPassMask uniform the draw
|
||||
// path writes, and a wrapper entry point that discards every fragment whose primitive routed
|
||||
// to an index the current replay pass is not drawing. False when the stage has no entry point
|
||||
// to wrap, which leaves the program renderable but unrouted.
|
||||
Bool InjectViewportIndexPassGate(String& source);
|
||||
|
||||
// Whether a vertex shader may declare a storage block at all, given what the host driver
|
||||
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
|
||||
// global purely so the decision can be tested without one.
|
||||
@@ -113,6 +126,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// link.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||
|
||||
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
||||
//
|
||||
// GLES has ONE viewport, ONE scissor rectangle and ONE depth range; GL 4.1 has sixteen of
|
||||
// each, selected per primitive by gl_ViewportIndex. There is no ES entry point to program the
|
||||
// other fifteen with (GL_OES_viewport_array exists but Adreno 830 does not have it, verified
|
||||
// three ways), so the only way to rasterize a primitive against index i's rectangle is to
|
||||
// make index i's rectangle THE viewport for the duration of a draw - which means issuing the
|
||||
// draw once per distinct viewport state and letting the fragment stage throw away the
|
||||
// primitives that belong to the other indices (the gate Managers.cpp injects).
|
||||
//
|
||||
// Indices whose whole state tuple (viewport rectangle, scissor rectangle, scissor-test enable,
|
||||
// depth range) is identical share ONE pass, so the overwhelmingly common case - every index
|
||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
||||
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
||||
//
|
||||
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// restores the pre-emulation path as a negative control.
|
||||
Bool ViewportArrayEmulationEnabled();
|
||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||
// true; it exists so that BeginViewportRoutingPasses - which runs on every draw of every
|
||||
// workload - can answer with one static load in the case that matters, which is every
|
||||
// application that has never heard of gl_ViewportIndex.
|
||||
extern Bool g_anyProgramRoutesViewportIndex;
|
||||
// Number of times the current draw has to be issued. Always >= 1, and exactly 1 - with no
|
||||
// state touched - whenever the current program does not route viewports, whenever every
|
||||
// configured index shares one state, and whenever replaying would multiply a side effect the
|
||||
// fragment gate cannot undo (transform feedback, rasterizer discard). Also seeds the pass
|
||||
// mask uniform for that single-pass case, so a gated fragment shader never runs against the
|
||||
// zero every GLSL uniform starts at - which would discard the whole draw.
|
||||
Uint BeginViewportRoutingPasses();
|
||||
// Push pass `pass`'s viewport / scissor / scissor-test / depth range onto the ES context and
|
||||
// set the gate mask to the indices it serves. Only called when the count above exceeds 1.
|
||||
void ApplyViewportRoutingPass(Uint pass);
|
||||
// Restore the gate mask and mark the render-state shadow dirty, so the next ordinary draw
|
||||
// re-pushes index 0's state. Takes the count so it can do nothing at all in the common case.
|
||||
void EndViewportRoutingPasses(Uint passCount);
|
||||
|
||||
// Issue one draw, replayed once per viewport-routing pass. Every application-visible draw
|
||||
// entry point wraps its native glDraw* call in this; the internal blit and clear helpers
|
||||
// deliberately do not, because they bind their own programs, which never route.
|
||||
template <typename IssueDraw>
|
||||
inline void ForEachViewportRoutingPass(IssueDraw&& issue) {
|
||||
const Uint passCount = BeginViewportRoutingPasses();
|
||||
for (Uint pass = 0; pass < passCount; ++pass) {
|
||||
if (passCount > 1) {
|
||||
ApplyViewportRoutingPass(pass);
|
||||
}
|
||||
issue();
|
||||
}
|
||||
EndViewportRoutingPasses(passCount);
|
||||
}
|
||||
|
||||
template <typename StateObject, typename BackendObject>
|
||||
class StateBackendObjectRegistry {
|
||||
public:
|
||||
@@ -1200,6 +1265,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||
// uniform write, and only such a program needs the reset after one.
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
// Which viewport indices the next draw's fragments may keep, one bit each. Written
|
||||
// once per replay pass; see ForEachViewportRoutingPass.
|
||||
void SetViewportPassMask(Uint32 indexMask) const;
|
||||
// True when this build injected the fragment-stage viewport gate, i.e. when a
|
||||
// pre-rasterization stage routes by gl_ViewportIndex AND the fragment stage can act
|
||||
// on it. The uniform is the honest test for both halves: it exists only where the
|
||||
// gate was injected, and the gate is injected only where a stage routes.
|
||||
Bool RoutesViewportIndex() const { return m_viewportPassMaskUniformLocation >= 0; }
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
@@ -1316,6 +1389,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int m_drawIdUniformLocation = -1;
|
||||
Int m_baseVertexUniformLocation = -1;
|
||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||
Int m_viewportPassMaskUniformLocation = -1;
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||
|
||||
@@ -414,14 +414,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||
if (batched) {
|
||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||
drawcount, 0);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||
drawcount, 0);
|
||||
});
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
});
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
@@ -442,8 +446,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
});
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
@@ -515,8 +521,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||
// application passed for this sub-draw.
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
});
|
||||
cursor += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
@@ -870,7 +878,9 @@ void main() {
|
||||
if (flattened.indexCount != 0) {
|
||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,15 +30,23 @@
|
||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||
// it matters.
|
||||
// BOTH BACKENDS RUN EVERY CASE, by two completely different routes, which is the point of
|
||||
// keeping them in one file. DirectVulkan declares sixteen viewports on the pipeline and lets the
|
||||
// hardware route. DirectGLES has one viewport, one scissor rectangle and one depth range and no
|
||||
// gl_ViewportIndex at all, so it EMULATES: the builtin becomes a flat varying, the fragment stage
|
||||
// gets a gate, and the draw is replayed once per distinct viewport state (Managers.h,
|
||||
// ForEachViewportRoutingPass). Every assertion below is about pixels, so it cannot tell the two
|
||||
// apart - which is exactly what has to be true.
|
||||
//
|
||||
// DirectVulkan skips when the device lacks the multiViewport feature - Vulkan then forbids a
|
||||
// pipeline from declaring more than one viewport at all, which is a device limit and not a
|
||||
// MobileGL bug; lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do
|
||||
// run where it matters.
|
||||
//
|
||||
// The last case is the negative control for the emulation and runs on DirectGLES only: it builds
|
||||
// the SAME program with the emulation switched off and requires the routing to collapse onto
|
||||
// viewport 0. Without it every assertion above could be satisfied by a backend that happened to
|
||||
// be right for some other reason, and the emulation's own switch would be untested.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
@@ -47,6 +55,10 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
|
||||
// to know which backend it is running on.
|
||||
#include <Config.h>
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -142,13 +154,6 @@ void main() { fragColor = gl_FragCoord.z; }
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||
"rendering half of KHR-GL43.viewport_array.";
|
||||
}
|
||||
|
||||
GLint maxViewports = 0;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||
@@ -520,11 +525,78 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// --- 4. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
|
||||
// --- 4. the negative control for the DirectGLES emulation -----------------------------
|
||||
//
|
||||
// Deliberately NOT a ViewportArrayScenario case, because it must run on DirectGLES - the
|
||||
// backend that got it wrong - and that fixture skips there. It needs none of the routing:
|
||||
// one viewport, one scissor rectangle, no geometry stage.
|
||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
||||
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
|
||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
||||
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
|
||||
// (and the failure signature KHR-GL43.viewport_array reported on it), pinned here so that
|
||||
// (a) the three cases above are known to be testing the emulation and not the weather,
|
||||
// and (b) the switch itself has a test.
|
||||
//
|
||||
// DirectGLES only: the flag steers nothing on DirectVulkan, which routes natively.
|
||||
TEST_F(ViewportArrayScenario, WithoutTheEmulationEveryIndexCollapsesOntoViewportZero) {
|
||||
if (Gl().BackendName() != "DirectGLES") {
|
||||
GTEST_SKIP() << "the emulation switch is a DirectGLES concern; DirectVulkan routes "
|
||||
"gl_ViewportIndex natively and ignores it";
|
||||
}
|
||||
|
||||
// The feature table is a process-global and this fixture shares its context with every
|
||||
// other scenario in the process, so the restore is not optional.
|
||||
struct ScopedEmulationOff {
|
||||
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
|
||||
MobileGL::MG_Config::Features.ViewportArrayEmulation =
|
||||
MobileGL::MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
|
||||
MobileGL::MG_Config::QuirkOverride saved;
|
||||
};
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
|
||||
GLuint unroutedProgram = 0;
|
||||
{
|
||||
const ScopedEmulationOff scopedEmulationOff;
|
||||
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
|
||||
// that carries this flag, so reusing m_program would just replay the routed build.
|
||||
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
||||
glUseProgram(unroutedProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
// Cell (0, 0) IS viewport 0's rectangle, so it is the one cell an unrouted draw paints
|
||||
// with something. Everything it holds comes from the last geometry invocation.
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, 0, 0), kViewportCount - 1)
|
||||
<< "with the emulation off, viewport 0's rectangle must hold the LAST invocation's "
|
||||
"index - if it holds 0 the routing is still happening and this control proves "
|
||||
"nothing";
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
if (x == 0 && y == 0) continue;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||
<< "cell (" << x << ", " << y << ") is outside viewport 0's rectangle and an "
|
||||
<< "unrouted draw cannot reach it";
|
||||
}
|
||||
}
|
||||
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(unroutedProgram);
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// --- 5. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
|
||||
//
|
||||
// Deliberately NOT a ViewportArrayScenario case, because that fixture's geometry stage
|
||||
// routes and this claim needs none of it: one viewport, one scissor rectangle, no
|
||||
// geometry stage - and it has to hold identically whether or not anything routes.
|
||||
//
|
||||
// glScissor(0, 0, 0, 0) is legal GL meaning "the scissor test rejects every fragment",
|
||||
// but it is byte-identical to the all-zero rectangle a context starts with, whose meaning
|
||||
|
||||
@@ -32,6 +32,23 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
ViewportIndexRoutingTest
|
||||
ViewportIndexRoutingTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ViewportIndexRoutingTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
ViewportIndexRoutingTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ViewportIndexRoutingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/ViewportIndexRoutingTest.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 ESSL half of the gl_ViewportIndex routing emulation (MG_Backend/DirectGLES/Managers.cpp).
|
||||
// GLES has one viewport, one scissor rectangle and one depth range where GL 4.1 has sixteen of
|
||||
// each selected per primitive, and the target device has no GL_OES_viewport_array to borrow, so
|
||||
// DirectGLES turns the builtin into an ordinary flat varying and gives the fragment stage a gate
|
||||
// the draw path replays against.
|
||||
//
|
||||
// Both passes are pure String -> String over what SPIRV-Cross emits once LowerViewportIndexPass
|
||||
// has demoted the builtin, so no GL context and no driver: the shapes they have to survive - and
|
||||
// the ones they must refuse - can be pinned here rather than only on a device. What they cannot
|
||||
// pin is that the routing produces the right pixels; that is
|
||||
// MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp, which runs the same claim through both
|
||||
// backends.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
|
||||
using MobileGL::Bool;
|
||||
using MobileGL::String;
|
||||
using MobileGL::MG_Backend::DirectGLES::InjectViewportIndexPassGate;
|
||||
using MobileGL::MG_Backend::DirectGLES::PromoteViewportIndexGlobalToVarying;
|
||||
|
||||
namespace {
|
||||
Bool Contains(const String& haystack, const String& needle) {
|
||||
return haystack.find(needle) != String::npos;
|
||||
}
|
||||
|
||||
// What SPIRV-Cross hands the backend for a geometry stage after LowerViewportIndexPass has
|
||||
// demoted gl_ViewportIndex: a plain file-scope global the shader still writes and which, until
|
||||
// this pass runs, nothing anywhere reads.
|
||||
constexpr const char* kLoweredGeometryShader = R"(#version 320 es
|
||||
layout(invocations = 16, points) in;
|
||||
layout(max_vertices = 4, triangle_strip) out;
|
||||
|
||||
layout(location = 0) flat out int gsIndex;
|
||||
int mg_ViewportIndex;
|
||||
|
||||
void main()
|
||||
{
|
||||
gsIndex = gl_InvocationID;
|
||||
mg_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentShader = R"(#version 320 es
|
||||
precision mediump float;
|
||||
precision highp int;
|
||||
|
||||
layout(location = 0) flat in int gsIndex;
|
||||
layout(location = 0) out highp vec4 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(float(gsIndex));
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// The promotion itself. The declaration becomes an interface variable and the STORE is left
|
||||
// exactly where it was - the pass must not touch the body, because the body is the application's.
|
||||
TEST(ViewportIndexRoutingTest, TheDemotedGlobalBecomesAFlatVarying) {
|
||||
String source = kLoweredGeometryShader;
|
||||
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source)) << source;
|
||||
|
||||
EXPECT_TRUE(Contains(source, "flat out highp int mg_ViewportIndex;")) << source;
|
||||
EXPECT_FALSE(Contains(source, "\nint mg_ViewportIndex;")) << source;
|
||||
EXPECT_TRUE(Contains(source, " mg_ViewportIndex = gl_InvocationID;")) << source;
|
||||
}
|
||||
|
||||
// FLAT is the semantics and not a hint: GL takes a primitive's viewport index from its provoking
|
||||
// vertex, and flat interpolation is what delivers that. An interpolated integer would not even
|
||||
// compile in ESSL, so losing the qualifier fails loudly - but silently losing it to a `smooth`
|
||||
// rewrite somewhere downstream would route by whichever vertex the rasterizer felt like.
|
||||
TEST(ViewportIndexRoutingTest, ThePromotedVaryingIsFlatAndCarriesNoExplicitLocation) {
|
||||
String source = kLoweredGeometryShader;
|
||||
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source));
|
||||
|
||||
const size_t declPos = source.find("flat out highp int mg_ViewportIndex;");
|
||||
ASSERT_NE(declPos, String::npos) << source;
|
||||
// No layout(location = N): the two stages are transpiled independently and cannot agree on a
|
||||
// number, so the varying is matched by NAME. A location that appeared here would have to
|
||||
// appear identically in the fragment stage, which nothing can guarantee.
|
||||
const size_t lineStart = source.rfind('\n', declPos);
|
||||
const String declLine = source.substr(lineStart + 1, declPos - lineStart - 1);
|
||||
EXPECT_EQ(declLine, "") << "the declaration must start its own line, with no layout qualifier";
|
||||
}
|
||||
|
||||
// A precision-qualified declaration is the same declaration. SPIRV-Cross prints one or the other
|
||||
// depending on what the module carried, and a pass that only matched the bare form would leave
|
||||
// half the drivers unrouted while reporting success.
|
||||
TEST(ViewportIndexRoutingTest, APrecisionQualifiedDeclarationIsPromotedToo) {
|
||||
String source = "#version 320 es\nhighp int mg_ViewportIndex;\nvoid main() { mg_ViewportIndex = 3; }\n";
|
||||
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source)) << source;
|
||||
EXPECT_TRUE(Contains(source, "flat out highp int mg_ViewportIndex;")) << source;
|
||||
}
|
||||
|
||||
// A stage that never routed must come out byte-identical, because every stage of every program on
|
||||
// this backend goes through the pass.
|
||||
TEST(ViewportIndexRoutingTest, AStageWithoutTheGlobalIsUntouched) {
|
||||
const String before = kFragmentShader;
|
||||
String source = before;
|
||||
EXPECT_FALSE(PromoteViewportIndexGlobalToVarying(source));
|
||||
EXPECT_EQ(source, before);
|
||||
}
|
||||
|
||||
// The one shape that would silently break a shader: a name that ends in mg_ViewportIndex but is
|
||||
// not the declaration. Only a declaration starting its own line may be rewritten.
|
||||
TEST(ViewportIndexRoutingTest, ADeclarationThatIsNotAtLineStartIsRefused) {
|
||||
const String before = "#version 320 es\nuniform highp int mg_ViewportIndex;\nvoid main() {}\n";
|
||||
String source = before;
|
||||
EXPECT_FALSE(PromoteViewportIndexGlobalToVarying(source));
|
||||
EXPECT_EQ(source, before);
|
||||
}
|
||||
|
||||
// The fragment gate. Three things have to be true at once: the varying and the uniform are
|
||||
// declared, the application's entry point survives under a new name, and the new entry point
|
||||
// discards on a mask miss and calls the old one otherwise.
|
||||
TEST(ViewportIndexRoutingTest, TheFragmentGateWrapsTheEntryPoint) {
|
||||
String source = kFragmentShader;
|
||||
ASSERT_TRUE(InjectViewportIndexPassGate(source)) << source;
|
||||
|
||||
EXPECT_TRUE(Contains(source, "flat in highp int mg_ViewportIndex;")) << source;
|
||||
EXPECT_TRUE(Contains(source, "uniform highp int mg_ViewportPassMask;")) << source;
|
||||
EXPECT_TRUE(Contains(source, "void mg_ViewportGatedMain()")) << source;
|
||||
EXPECT_TRUE(Contains(source, "discard;")) << source;
|
||||
EXPECT_TRUE(Contains(source, "mg_ViewportGatedMain();")) << source;
|
||||
// The application's body is not edited, only renamed.
|
||||
EXPECT_TRUE(Contains(source, " fragColor = vec4(float(gsIndex));")) << source;
|
||||
// Exactly one entry point remains, and it is the wrapper.
|
||||
EXPECT_EQ(source.find("void main()"), source.rfind("void main()")) << source;
|
||||
}
|
||||
|
||||
// The shift operand has to be clamped. GL leaves a gl_ViewportIndex outside [0, MAX_VIEWPORTS)
|
||||
// undefined and the emulation is free to pick anything, but an ESSL shift by >= 32 is undefined
|
||||
// in a way that can take the whole draw with it - so the gate must not be able to reach one.
|
||||
TEST(ViewportIndexRoutingTest, TheGateClampsTheShiftIntoRange) {
|
||||
String source = kFragmentShader;
|
||||
ASSERT_TRUE(InjectViewportIndexPassGate(source));
|
||||
EXPECT_TRUE(Contains(source, "mg_ViewportPassMask >> (mg_ViewportIndex & 15)")) << source;
|
||||
}
|
||||
|
||||
// A fragment stage that READS gl_ViewportIndex has no ESSL spelling for it either, and the
|
||||
// routing varying is exactly the value it wanted. This is the only place the read can be repaired
|
||||
// - LowerViewportIndexPass deliberately demotes outputs only, because a demoted input would
|
||||
// answer from an undefined global.
|
||||
TEST(ViewportIndexRoutingTest, AFragmentStageReadOfTheBuiltinIsRedirectedOntoTheVarying) {
|
||||
String source = R"(#version 320 es
|
||||
precision highp int;
|
||||
layout(location = 0) out highp vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(float(gl_ViewportIndex));
|
||||
}
|
||||
)";
|
||||
ASSERT_TRUE(InjectViewportIndexPassGate(source)) << source;
|
||||
EXPECT_FALSE(Contains(source, "gl_ViewportIndex")) << source;
|
||||
EXPECT_TRUE(Contains(source, "fragColor = vec4(float(mg_ViewportIndex));")) << source;
|
||||
}
|
||||
|
||||
// A stage the pass declines must reach the driver exactly as it arrived, not half-rewritten.
|
||||
// The caller logs the decline and the program still renders - unrouted, which is the old
|
||||
// behaviour - so a partially edited source here would turn a degradation into a broken shader.
|
||||
TEST(ViewportIndexRoutingTest, AStageWithNoEntryPointIsDeclinedWithoutBeingEdited) {
|
||||
const String before = "#version 320 es\nprecision highp int;\nhighp int f() { return gl_ViewportIndex; }\n";
|
||||
String source = before;
|
||||
EXPECT_FALSE(InjectViewportIndexPassGate(source));
|
||||
EXPECT_EQ(source, before);
|
||||
}
|
||||
@@ -135,7 +135,7 @@ void main() {
|
||||
EsslTranslationKeyInputs inputs;
|
||||
inputs.spirv = &spirv;
|
||||
inputs.shaderType = GL_FRAGMENT_SHADER;
|
||||
inputs.supportsViewportArray = false;
|
||||
inputs.viewportIndexLoweringArmed = false;
|
||||
inputs.supportsNoperspectiveInterpolation = false;
|
||||
inputs.maxColorTextureSamples = 4;
|
||||
inputs.maxIntegerSamples = 1;
|
||||
@@ -855,8 +855,8 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
}
|
||||
{ // arms LowerViewportIndexForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.supportsViewportArray = true;
|
||||
variants.emplace_back("supportsViewportArray", BuildEsslTranslationKey(v));
|
||||
v.viewportIndexLoweringArmed = true;
|
||||
variants.emplace_back("viewportIndexLoweringArmed", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms EmulateNoPerspectiveForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
@@ -1008,7 +1008,7 @@ TEST_F(TranslationCacheTest, L2RunsTheEmitterOncePerDistinctKey) {
|
||||
// ... and a gate that only steers the SPIR-V pass chain still moves the key, so the
|
||||
// emitter runs again even though this stand-in ignores the bit.
|
||||
inputs = BaselineEsslInputs(spirv);
|
||||
inputs.supportsViewportArray = true;
|
||||
inputs.viewportIndexLoweringArmed = true;
|
||||
(void)translate(inputs);
|
||||
EXPECT_EQ(emitCount, 3);
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
TranslationKeyBuilder builder;
|
||||
AppendCommonKeyPrefix(builder, kEsslKeyTag);
|
||||
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
|
||||
builder.Value(static_cast<Uint8>(inputs.viewportIndexLoweringArmed));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
|
||||
builder.Value(inputs.maxColorTextureSamples);
|
||||
|
||||
@@ -556,7 +556,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// * the GL stage enum - three passes are stage-gated (draw parameters and
|
||||
// array vertex inputs on vertex, fragment-output index legalization on
|
||||
// fragment);
|
||||
// * SupportsViewportArray - arms LowerViewportIndexForEssl;
|
||||
// * the viewport-index lowering arming bit - GL_OES_viewport_array's absence OR the
|
||||
// routing emulation being on - which arms LowerViewportIndexForEssl;
|
||||
// * the four sample ceilings (color / integer / depth / advertised) - both
|
||||
// ARM ClampMultisampleFetchesForEssl and PARAMETERIZE it;
|
||||
// * SupportsNoperspectiveInterpolation - arms EmulateNoPerspectiveForEssl;
|
||||
@@ -583,7 +584,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// armed by nothing, so the SPIR-V already in this key covers them completely.
|
||||
//
|
||||
// THE TEST FOR THAT CLAIM IS NOT THE SIGNATURE. LowerViewportIndexForEssl is equally
|
||||
// module-only to look at, yet SupportsViewportArray is in this key because that bit ARMS
|
||||
// module-only to look at, yet the arming bit is in this key because it ARMS
|
||||
// it at the call site. So a new pass needs BOTH checks - what it takes, and what decides
|
||||
// whether it runs - before "no key material" is a conclusion rather than an assumption.
|
||||
// Note also where an application-authored value can hide: the atomic-counter
|
||||
@@ -612,7 +613,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
GLenum shaderType = 0;
|
||||
|
||||
// --- driver capability bits that arm or steer a pass ---
|
||||
Bool supportsViewportArray = false;
|
||||
// Whether LowerViewportIndexForEssl runs on this module. NOT the raw
|
||||
// GL_OES_viewport_array bit any more: the routing emulation arms the pass even where the
|
||||
// extension exists (Config.h, ViewportArrayEmulation), so the extension alone no longer
|
||||
// decides, and a key carrying only it would serve a module lowered under one setting to a
|
||||
// link made under the other.
|
||||
Bool viewportIndexLoweringArmed = false;
|
||||
Bool supportsNoperspectiveInterpolation = false;
|
||||
// GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage
|
||||
// image whose format GLSL ES core cannot spell in the core format that carries it and
|
||||
|
||||
Reference in New Issue
Block a user