mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +09:00
[Feat] (DirectGLES, Config, ShaderTranspiler): route gl_ViewportIndex on Espryt by replaying a draw per distinct viewport state
This commit is contained in:
@@ -47,6 +47,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
|
||||
@@ -300,6 +310,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
|
||||
@@ -5493,7 +5623,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 ||
|
||||
@@ -5517,11 +5666,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
|
||||
@@ -6233,7 +6384,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);
|
||||
|
||||
@@ -6272,7 +6456,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 =
|
||||
@@ -6427,8 +6618,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);
|
||||
@@ -6489,6 +6685,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,
|
||||
@@ -6691,6 +6910,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;
|
||||
@@ -6893,6 +7125,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 {
|
||||
|
||||
Reference in New Issue
Block a user