[Feat, Fix] (MG_Impl, MG_State, MG_Backend): program interface queries from frontend reflection; Espryt state-shadow reset

Wave 2 of the advertised-extension conformance campaign.

PROGRAM INTERFACE QUERIES (the load-bearing piece). glGetProgramInterfaceiv
and the five glGetProgramResource* entry points were answered by the
BACKENDS - Espryt asked the real driver about SPIRV-Cross-generated ESSL
whose namespace is not the GL one (default-block uniforms live in
MGL_GLOBAL_UBO there), and Magma kept a second, partial reflection that
hardcoded types and diverged from the frontend. Both are now deleted; a new
frontend resource-model layer (ProgramInterface.{h,cpp}) answers every
interface - uniforms, uniform blocks, atomic-counter buffers (recovered
from glslang's synthesized gl_AtomicCounterBlock_<binding> lowering),
buffer variables, shader-storage blocks (classified by TType storage
qualifier since glslang reflects them as uniform blocks), program inputs/
outputs (built-ins' layoutLocationEnd sentinel mapped to -1), and
transform-feedback varyings including the gl_NextBuffer/gl_SkipComponentsN
pseudo-varyings - from the glslang reflection the frontend already trusts
for glGetActiveUniform. Name/index round-tripping, the "[0]" array
spelling, and the GL 4.6 table 7.2 prop/error matrix live in the new layer
only; GetActiveUniform*/GetActiveAttrib* are untouched.

glShaderStorageBlockBinding now takes the interface-layer index (the one
GetProgramResourceIndex returns, with a range check it never had), records
the binding on the program keyed by block NAME - the one coordinate all
three index spaces agree on - and delegates by name across the backend
boundary. Both backends reseed the recorded bindings on their own program
rebuilds, so an unrelated resync can no longer silently revert a rebound
block, and GL_BUFFER_BINDING reports the live binding, not the declared
one. The Espryt delegate applies only to an already-synced twin and can no
longer trigger SyncToBackend from a getter; the sync path's GL query
out-params are initialized and clamped (a load-dependent stack-garbage
Vector size crash caught by the gate, reproduced 3/50 pre-fix, 100/100
post-fix under saturating load).

ESPRYT RENDER-STATE SHADOW RESET (rides along because it shares
DirectGLES.cpp): the render-state shadow is file-static and survives
MobileGL context switches, so GL_FRAMEBUFFER_SRGB (and the whole synced-cap
class) leaked between contexts - the cross-test leakage class the CTS maps
have carried for a week. MakeCurrent now invalidates the shadow like it
already invalidates the program/FBO/buffer caches, and the resync resolves
a never-set scissor box to the current surface instead of pushing the
(0,0,0,0) sentinel verbatim (which scissored everything away - caught by
the retrace gate, bisected to the exact field via a bitmask probe, and
fixed by resolving like the viewport path rather than reverting).

Frontend riders exposed by the layer: glGetUniformLocation resolves
arrays-of-arrays element addressing ("a[2][1]"); transform-feedback capture
accepts element-addressed varying names ("b[1]") and snapshots the request
verbatim for the interface (Magma's decorate pass logs loudly that element
capture is unimplemented there - follow-up).

KNOWN GAPS, documented in code and tests: the 6 subroutines-* cases
(glslang refuses subroutine for SPIR-V; wave 3), the 5 separate-programs-*
cases (glslang's pipe-I/O reflection cannot see a separable non-vertex
stage's own inputs; needs stage-aware output validation first), and
uniform-block-types' per-instance stage masks (not derivable from the
reflection).

Gate: 593/593 unit at default and kill-switch, x10 each, plus the SSB race
case 100/100 under 20-way CPU load; ext caselist Espryt 77.87% -> 80.42%,
Magma 77.58% -> 79.39% (+212 fixed, 0 newly broken); program_interface_query
2/43 -> 31/43 unique on Espryt, 9/43 -> 31/43 on Magma, backends now
byte-identical; KHR-GL45.direct_state_access 370/371 + 371/371 with the 4
sRGB leak victims recovered in cross-test ordering; KHR-GL33 held at
9884/9886; full 39x2 CI retrace with zero wave-attributable failures (the
3 failing newly-added fixtures are bit-identical on the pristine baseline).
This commit is contained in:
BZLZHH
2026-08-09 08:12:30 -04:00
parent 33ff177bb2
commit 3e0460e472
18 changed files with 2515 additions and 790 deletions
+1
View File
@@ -232,6 +232,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
+12 -9
View File
@@ -181,15 +181,18 @@ namespace MobileGL {
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
// the application passes is the frontend interface-query enumeration's, and no
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
// The name is the one coordinate all three agree on, so the frontend resolves the
// index against its own enumeration and each backend maps the name to its own.
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
GLuint storageBlockBinding);
// GL fence sync objects. All entries are optional (may be null); the
// frontend then falls back to always-signaled sync semantics.
// FenceSync may itself return null when the backend cannot create a
@@ -968,12 +968,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
+134 -80
View File
@@ -1422,15 +1422,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool g_hasSyncedRenderState = false;
static RenderStateParameters g_syncedRenderStateParameters;
static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
// The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState);
// an impossible value so the first sync always pushes.
static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
// GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the
// frontend's (desktop-GL default) disabled state down.
static Bool g_syncedSrgbFramebufferWrites = true;
// Set when the shadow below stops describing the real ES context. The ES context
// OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the
// previous context's enable state while the frontend's parameter block AND its
// version counter both restart from defaults. Every "differs from what I last pushed"
// test in SyncRenderState would then agree that nothing needs pushing, and the
// leftover state silently applies to the new context - the class of bug the CTS
// caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole
// block rather than the one cap that happened to be noticed.
static Bool g_forceFullRenderStateResync = true;
void InvalidateSyncedRenderState() {
g_forceFullRenderStateResync = true;
g_hasSyncedRenderState = false;
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
}
void SyncRenderState() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
if (g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) return;
const Bool forceFullPush = g_forceFullRenderStateResync;
g_forceFullRenderStateResync = false;
if (!forceFullPush && g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) {
return;
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
@@ -1478,7 +1501,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
if (tailSpanDirty) {
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
if (parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
if (parameters.cap_mg##Enabled) { \
g_GLESFuncs.glEnable(cap_gl); \
} else { \
@@ -1507,7 +1530,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// into an sRGB colour buffer comes back encoded once too often (the shader's own
// decode on the next fetch then leaves the value one conversion short).
const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (g_GLESCapabilities.SupportsSrgbWriteControl && srgbWrites != g_syncedSrgbFramebufferWrites) {
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB)
: g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB);
g_syncedSrgbFramebufferWrites = srgbWrites;
@@ -1520,7 +1544,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled;
const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled ||
g_syncedRenderStateParameters.PrimitiveRestartEnabled;
if (restart != syncedRestart) {
if (forceFullPush || restart != syncedRestart) {
restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX)
: g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
}
@@ -1556,7 +1580,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool allEnabled = true;
Bool allDisabled = true;
Bool anyCapDirty = false;
Bool anyCapDirty = forceFullPush;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
Bool enabled = targetStates[i].Enabled;
@@ -1581,7 +1605,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
s.Enabled = false;
} else {
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (targetStates[i].Enabled != syncedStates[i].Enabled) {
if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) {
syncedStates[i].Enabled = targetStates[i].Enabled;
syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i)
: g_GLESFuncs.glDisablei(GL_BLEND, i);
@@ -1591,7 +1615,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Bool allFuncsSame = true;
Bool anyFuncDirty = false;
Bool anyFuncDirty = forceFullPush;
const auto& first = targetStates[0];
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
@@ -1630,8 +1654,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& cur = targetStates[i];
auto& syn = syncedStates[i];
if (cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB ||
cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha) {
if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB ||
cur.DstFactorRGB != syn.DstFactorRGB ||
cur.SrcFactorAlpha != syn.SrcFactorAlpha ||
cur.DstFactorAlpha != syn.DstFactorAlpha) {
syn.SrcFactorRGB = cur.SrcFactorRGB;
syn.DstFactorRGB = cur.DstFactorRGB;
syn.SrcFactorAlpha = cur.SrcFactorAlpha;
@@ -1648,7 +1674,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Bool allEquationsSame = true;
Bool anyEquationDirty = false;
Bool anyEquationDirty = forceFullPush;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& cur = targetStates[i];
@@ -1679,7 +1705,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& cur = targetStates[i];
auto& syn = syncedStates[i];
if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) {
if (forceFullPush || cur.ColorEquation != syn.ColorEquation ||
cur.AlphaEquation != syn.AlphaEquation) {
syn.ColorEquation = cur.ColorEquation;
syn.AlphaEquation = cur.AlphaEquation;
@@ -1693,13 +1720,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (tailSpanDirty) { // Depth state
if (parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc));
}
if (parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
}
if (parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
}
}
@@ -1710,16 +1737,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex];
const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK;
if (current.Func != synced.Func || current.Ref != synced.Ref ||
if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref ||
current.ValueMask != synced.ValueMask) {
g_GLESFuncs.glStencilFuncSeparate(
glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func), current.Ref,
current.ValueMask);
}
if (current.WriteMask != synced.WriteMask) {
if (forceFullPush || current.WriteMask != synced.WriteMask) {
g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask);
}
if (current.FailOp != synced.FailOp || current.PassDepthFailOp != synced.PassDepthFailOp ||
if (forceFullPush || current.FailOp != synced.FailOp ||
current.PassDepthFailOp != synced.PassDepthFailOp ||
current.PassDepthPassOp != synced.PassDepthPassOp) {
g_GLESFuncs.glStencilOpSeparate(
glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp),
@@ -1736,7 +1764,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& targetMasks = parameters.ColorMasks;
const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks;
Bool anyDirty = false;
Bool anyDirty = forceFullPush;
Bool allSame = true;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (targetMasks[i] != syncedMasks[i]) anyDirty = true;
@@ -1753,7 +1781,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
: g_GLESFuncs.glColorMaskiOES;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (targetMasks[i] != syncedMasks[i]) {
if (forceFullPush || targetMasks[i] != syncedMasks[i]) {
const BoolVec4& m = targetMasks[i];
colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
ToGLBoolean(m.w()));
@@ -1765,7 +1793,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present.
// Without the extension the mode stays FILL and non-FILL requests are dropped.
if (parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront &&
if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) &&
g_GLESCapabilities.SupportsPolygonMode) {
const auto polygonModeFn =
g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE;
@@ -1774,67 +1802,97 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (tailSpanDirty) { // Clear values
if (parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
const FloatVec4& clearCol = parameters.ClearColor;
g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w());
}
if (parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
g_GLESFuncs.glClearDepthf(parameters.ClearDepth);
}
if (parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
g_GLESFuncs.glClearStencil(static_cast<GLint>(parameters.ClearStencil));
}
if (parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
const FloatVec4& blendColor = parameters.BlendColor;
g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w());
}
}
if (tailSpanDirty) { // Cull face mode
if (parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
if (forceFullPush || parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
const CullFaceMode& cfm = parameters.CullFaceModeSetting;
g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm));
}
if (parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
if (forceFullPush || parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
const FrontFaceMode& ffm = parameters.FrontFaceModeSetting;
g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm));
}
}
if (tailSpanDirty) { // Scissor box
if (parameters.ScissorBox != g_syncedRenderStateParameters.ScissorBox) {
const IntVec4& scissorBox = parameters.ScissorBox;
g_GLESFuncs.glScissor(scissorBox.x(), scissorBox.y(), scissorBox.z(), scissorBox.w());
if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and
// for the same reason: what has to reach the driver is NOT simply the parameter
// field. (0,0,0,0) is where RenderStateParameters::ScissorBox starts and the only
// thing that ever writes it is glScissor, so that value means "the application has
// never called glScissor" - it is not a GL scissor box. GL's initial box is the
// whole window, which the frontend has no way to spell before a surface exists.
// The pre-resync code got away with pushing the field verbatim only by accident:
// the shadow held the same default, the field never compared unequal, and the ES
// context kept its own correct default. Under the forced full push that accident
// is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle, and
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
// loses the terrain and the whole GUI.
IntVec4 backendScissorBox = parameters.ScissorBox;
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
Int surfaceWidth = 0;
Int surfaceHeight = 0;
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
}
}
// Compared against what was actually PUSHED, not against the parameter field, so
// the resolved value and the diff can never disagree.
if (backendScissorBox != g_syncedBackendScissorBox) {
g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(),
backendScissorBox.w());
g_syncedBackendScissorBox = backendScissorBox;
}
}
if (tailSpanDirty) { // Logic op (first field of the tail span)
if (parameters.LogicOp != g_syncedRenderStateParameters.LogicOp) {
// glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a
// plain ES 3.x driver. Before the forced resync it was reached only when an app
// actually set a logic op; now every MakeCurrent would call it, so the null check
// is mandatory rather than defensive.
if (g_GLESFuncs.glLogicOp &&
(forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) {
g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp));
}
}
if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below)
if (parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
if (forceFullPush || parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits) {
g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits);
}
}
if (headSpanDirty) { // Line width
if (parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
g_GLESFuncs.glLineWidth(parameters.LineWidth);
}
}
if (headSpanDirty) { // Point size
if (parameters.PointSize != g_syncedRenderStateParameters.PointSize) {
if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize,
// so the entry point is absent on most drivers; see the glLogicOp note above)
if (g_GLESFuncs.glPointSize &&
(forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) {
g_GLESFuncs.glPointSize(parameters.PointSize);
}
}
if (tailSpanDirty) { // Sample coverage
if (parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
if (forceFullPush || parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) {
g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue,
ToGLBoolean(parameters.SampleCoverageInvert));
@@ -1842,7 +1900,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (tailSpanDirty) { // Sample mask
if (g_GLESFuncs.glSampleMaski && parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue) {
if (g_GLESFuncs.glSampleMaski &&
(forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) {
g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue);
}
}
@@ -5189,50 +5248,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glGetProgramiv(backendProgramId, pname, params);
}
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return;
g_GLESFuncs.glGetProgramInterfaceiv(backendProgramId, programInterface, pname, params);
}
// NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry
// point must never be the thing that BUILDS a backend program.
//
// The frontend has already recorded the rebinding on the program object
// (SetShaderStorageBlockBinding) - that record is what GL_BUFFER_BINDING reports and what
// BackendProgramObjectImpl::SyncToBackend replays onto every driver program it builds. So
// the only work left here is an optimisation: push the change straight onto a driver
// program that is ALREADY built and already current with this link, so the next draw does
// not have to be preceded by a rebuild.
//
// Calling GetBackendProgramId() instead would sync-on-demand from a non-draw entry point,
// i.e. transpile and compile the whole program while the draw-path globals that the ESSL
// is generated against (PrgramImpl::g_fragColorBroadcastCount, the snorm/unorm clamp
// masks - established by SyncCurrentProgram) still hold another program's values. That
// bakes a program against the wrong state, and under CPU load it was also observed to
// fail the driver compile outright. Deferring is spec-fine: a binding only has to take
// effect by the block's next use.
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
if (!storageBlockName) return;
if (!MG_State::pGLContext->ValidateProgramName(program)) return;
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) return;
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return GL_INVALID_INDEX;
return g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, programInterface, name);
}
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return;
g_GLESFuncs.glGetProgramResourceName(backendProgramId, programInterface, index, bufSize, length, name);
}
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return;
g_GLESFuncs.glGetProgramResourceiv(backendProgramId, programInterface, index, propCount, props, bufSize, length,
params);
}
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return -1;
return g_GLESFuncs.glGetProgramResourceLocation(backendProgramId, programInterface, name);
}
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
(void)program;
(void)programInterface;
(void)name;
return -1;
}
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) return;
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, storageBlockIndex, storageBlockBinding);
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
if (!backendProgramSlot || !*backendProgramSlot) return;
auto& backendObj = *backendProgramSlot;
// Not merely "a program id exists": a backend object whose synced link version has
// fallen behind is about to be rebuilt anyway, and its current driver interface is
// the PREVIOUS link's - applying to it could land the binding on an unrelated block.
if (!backendObj->GetBackendProgramId() ||
backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) {
return; // SyncToBackend's reseed will carry it
}
PrgramImpl::ApplyShaderStorageBlockBinding(backendObj->GetBackendProgramId(), storageBlockName,
storageBlockBinding);
}
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
@@ -6778,6 +6828,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::InvalidatePixelBufferBindingCaches();
FramebufferImpl::InvalidateFramebufferBindingCache();
PixelStoreImpl::InvalidatePackStateCache();
// The render-state shadow belongs in this list for the same reason as the ones above:
// it describes the real ES context, which outlives the MobileGL context that is
// becoming current. See InvalidateSyncedRenderState.
RenderStateImpl::InvalidateSyncedRenderState();
// eglSwapInterval requires a current context; a request made while none was
// current (and dropped by the driver) is retried here.
ApplyRequestedSwapInterval();
+1 -9
View File
@@ -92,15 +92,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
Bool MakeCurrent();
+58 -7
View File
@@ -3812,6 +3812,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) {
if (backendProgramId == 0 || blockName.empty()) return false;
if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false;
GLuint driverIndex =
g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str());
if (driverIndex == GL_INVALID_INDEX) {
// An arrayed block is enumerated per element by GL but declared once; the
// generated ESSL carries the bare block name.
const auto bracket = blockName.rfind('[');
if (bracket == String::npos || blockName.back() != ']') return false;
driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK,
blockName.substr(0, bracket).c_str());
if (driverIndex == GL_INVALID_INDEX) return false;
}
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding);
return true;
}
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
const MG_State::GLState::ProgramObject& stateProgramObject) {
const auto& overrides = stateProgramObject.GetShaderStorageBlockBindingOverrides();
if (overrides.empty()) return; // the overwhelming majority of programs
for (const auto& [blockName, binding] : overrides) {
if (binding < 0) continue;
ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast<Uint>(binding));
}
}
void BackendProgramObjectImpl::SyncToBackend(
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
#ifdef TRACY_ENABLE
@@ -3846,9 +3874,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (attachedCount > 0) {
Vector<GLuint> attachedShaders(attachedCount);
GLsizei actualCount;
// Every GL out-param in this function is pre-initialized and every count is
// re-clamped after the query. A driver that returns without writing the
// out-param (no current context, a lost context, a stubbed entry point) would
// otherwise leak an uninitialized stack value straight into a container size
// or a loop bound - which is exactly how this path used to throw
// length_error out of a Vector fill-ctor.
GLsizei actualCount = 0;
g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount,
attachedShaders.data());
actualCount = std::clamp<GLsizei>(actualCount, 0, static_cast<GLsizei>(attachedShaders.size()));
MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId);
for (GLsizei i = 0; i < actualCount; ++i) {
@@ -3986,13 +4021,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
GLint compileStatus;
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed"
// and take the diagnostic path, never as a silent success that attaches an
// uncompiled shader.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
GLint logLength;
GLint logLength = 0;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
Vector<GLchar> log(logLength);
if (logLength < 0) logLength = 0;
// +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator,
// but a driver that reports 0 (or fails the query) must still leave
// log.data() a readable empty C string for the %s below.
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
m_backendProgramUsable = false;
continue;
@@ -4028,14 +4071,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Linking program %u", m_backendProgramId);
g_GLESFuncs.glLinkProgram(m_backendProgramId);
GLint linkStatus;
GLint linkStatus = GL_FALSE;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
if (linkStatus != GL_TRUE) {
GLint logLength;
GLint logLength = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
Vector<GLchar> log(logLength);
if (logLength < 0) logLength = 0;
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data());
log.back() = '\0';
MGLOG_E("Program %u linking failed for %u: %s", stateProgramObject->GetExternalIndex(),
m_backendProgramId, log.data());
} else {
@@ -4068,6 +4113,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
CacheResourceLocations(stateProgramObject);
// AFTER the link, because glShaderStorageBlockBinding needs the driver's linked
// interface. This is the only place Espryt applies a rebinding: the frontend
// record is authoritative and the glShaderStorageBlockBinding entry point itself
// deliberately never forces a program build (see DirectGLES.cpp), so a rebinding
// requested while no backend program existed yet arrives here instead.
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
m_isInitialized = true;
+16
View File
@@ -981,6 +981,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Uint g_lastUsedBackendProgramId;
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
g_backendProgramObjects;
// Points one shader storage block of an ALREADY-LINKED backend program at
// `binding`. `blockName` is the frontend interface-query spelling; the real
// driver's own index for it is looked up here, because the transpiled ESSL's
// block order is not the frontend's. Returns false when the block does not exist
// on the backend program (eliminated as unused, or the driver lacks the entry
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
// record either way.
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
// program that was just built. The frontend record is authoritative (only the
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
// reseed-on-rebuild in BuildProgramResourceCache.
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
const MG_State::GLState::ProgramObject& stateProgramObject);
} // namespace PrgramImpl
namespace SamplerImpl {
@@ -617,12 +617,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
+32 -468
View File
@@ -232,6 +232,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
StorageBlockResource block{};
block.name = blockName;
block.binding = binding->binding;
// glShaderStorageBlockBinding survives every rebuild of this cache: the
// authoritative record of a rebound block lives on the program (it is what
// GL_BUFFER_BINDING reports), and only the shader's declared binding is
// recoverable from the SPIR-V. Without this, any unrelated state-version
// bump would silently revert the block to its declared binding.
const Int rebound = program.GetShaderStorageBlockBindingOverride(blockName);
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
block.dataSize = static_cast<GLint>(binding->block.size);
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
@@ -256,18 +263,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return programObject.get();
}
void CopyResourceName(const String& source, GLsizei bufSize, GLsizei* length, GLchar* name) {
const GLsizei writtenLength = static_cast<GLsizei>(source.size());
if (length) {
*length = writtenLength;
}
if (name && bufSize > 0) {
const GLsizei copyLength = std::min<GLsizei>(bufSize - 1, writtenLength);
std::memcpy(name, source.data(), static_cast<SizeT>(copyLength));
name[copyLength] = '\0';
}
}
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
@@ -288,100 +283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return reinterpret_cast<const Uint8*>(indirect);
}
Vector<GLuint> GetUniformBlockActiveVariables(const MG_State::GLState::ProgramObject& program,
GLuint blockIndex) {
Vector<GLuint> activeVariables;
const Uint uniformCount = program.GetUniformCount();
activeVariables.reserve(uniformCount);
for (Uint uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) {
if (program.GetActiveUniformBlockIndex(uniformIndex) == static_cast<Int>(blockIndex)) {
activeVariables.push_back(uniformIndex);
}
}
return activeVariables;
}
GLuint FindProgramInputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
const Int activeCount = program.GetActiveAttributesCount();
for (Int index = 0; index < activeCount; ++index) {
if (program.GetActiveAttribName(index) == name) {
return static_cast<GLuint>(index);
}
}
return GL_INVALID_INDEX;
}
GLuint FindProgramOutputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
const Int activeCount = program.GetActiveFragmentOutputCount();
for (Int index = 0; index < activeCount; ++index) {
if (program.GetActiveFragmentOutputName(index) == name) {
return static_cast<GLuint>(index);
}
}
return GL_INVALID_INDEX;
}
GLint GetProgramOutputLocation(const MG_State::GLState::ProgramObject& program, const String& name) {
const Int activeCount = program.GetActiveFragmentOutputCount();
for (Int index = 0; index < activeCount; ++index) {
if (program.GetActiveFragmentOutputName(index) == name) {
return program.GetFragmentOutputLocation(index);
}
}
return -1;
}
GLint GetProgramResourceActiveCount(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
const ProgramResourceCache& cache) {
switch (programInterface) {
case GL_SHADER_STORAGE_BLOCK:
return static_cast<GLint>(cache.storageBlocks.size());
case GL_BUFFER_VARIABLE:
return static_cast<GLint>(cache.bufferVariables.size());
case GL_UNIFORM_BLOCK:
return program.GetActiveUniformBlocksCount();
case GL_UNIFORM:
return static_cast<GLint>(program.GetUniformCount());
case GL_PROGRAM_INPUT:
return program.GetActiveAttributesCount();
case GL_PROGRAM_OUTPUT:
return program.GetActiveFragmentOutputCount();
default:
return 0;
}
}
GLint GetProgramResourceMaxNameLength(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
const ProgramResourceCache& cache) {
switch (programInterface) {
case GL_SHADER_STORAGE_BLOCK: {
SizeT maxLength = 0;
for (const auto& block : cache.storageBlocks) maxLength = std::max(maxLength, block.name.size() + 1);
return static_cast<GLint>(maxLength);
}
case GL_BUFFER_VARIABLE: {
SizeT maxLength = 0;
for (const auto& var : cache.bufferVariables) maxLength = std::max(maxLength, var.name.size() + 1);
return static_cast<GLint>(maxLength);
}
case GL_UNIFORM_BLOCK:
return program.GetActiveUniformBlocksMaxNameLength() + 1;
case GL_UNIFORM:
return program.GetUniformMaxLength() + 1;
case GL_PROGRAM_INPUT:
return program.GetActiveAttributesMaxLength() + 1;
case GL_PROGRAM_OUTPUT: {
SizeT maxLength = 0;
const Int activeCount = program.GetActiveFragmentOutputCount();
for (Int index = 0; index < activeCount; ++index) {
maxLength = std::max(maxLength, program.GetActiveFragmentOutputName(index).size() + 1);
}
return static_cast<GLint>(maxLength);
}
default:
return 0;
}
}
} // namespace
void ClearProgramResourceCaches() {
@@ -395,11 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
auto& cache = GetProgramResourceCache(program);
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
[&](const StorageBlockResource& block) { return block.name == name; });
return it == cache.storageBlocks.end()
? GL_INVALID_INDEX
: static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
auto find = [&cache](const String& key) {
return std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
[&](const StorageBlockResource& block) { return block.name == key; });
};
auto it = find(name);
if (it == cache.storageBlocks.end()) {
// Cache names are normalized (NormalizeDescriptorName drops the array suffix), so
// an arrayed block that GL enumerates per element - "B[0]", "B[1]" - is one entry
// here, spelled "B". Retry against the bare name before giving up.
const auto bracket = name.rfind('[');
if (bracket == String::npos || name.empty() || name.back() != ']') return GL_INVALID_INDEX;
it = find(name.substr(0, bracket));
if (it == cache.storageBlocks.end()) return GL_INVALID_INDEX;
}
return static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
}
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
@@ -874,357 +785,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
if (!params) return;
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) return;
auto& cache = GetProgramResourceCache(*programObject);
switch (pname) {
case GL_ACTIVE_RESOURCES:
*params = GetProgramResourceActiveCount(*programObject, programInterface, cache);
return;
case GL_MAX_NAME_LENGTH:
*params = GetProgramResourceMaxNameLength(*programObject, programInterface, cache);
return;
case GL_MAX_NUM_ACTIVE_VARIABLES:
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
SizeT maxCount = 0;
for (const auto& block : cache.storageBlocks) {
maxCount = std::max(maxCount, block.activeVariables.size());
}
*params = static_cast<GLint>(maxCount);
} else if (programInterface == GL_UNIFORM_BLOCK) {
GLint maxCount = 0;
const Int activeBlocks = programObject->GetActiveUniformBlocksCount();
for (Int index = 0; index < activeBlocks; ++index) {
maxCount = std::max(maxCount, programObject->GetUniformBlockActiveUniformCount(index));
}
*params = maxCount;
} else {
*params = 0;
}
return;
default:
*params = 0;
return;
}
}
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
if (!name) return GL_INVALID_INDEX;
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) return GL_INVALID_INDEX;
auto& cache = GetProgramResourceCache(*programObject);
const String resourceName = name;
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
return GetShaderStorageBlockIndex(*programObject, name);
}
if (programInterface == GL_BUFFER_VARIABLE) {
const auto it = std::find_if(cache.bufferVariables.begin(), cache.bufferVariables.end(),
[&](const BufferVariableResource& var) { return var.name == resourceName; });
return it == cache.bufferVariables.end()
? GL_INVALID_INDEX
: static_cast<GLuint>(std::distance(cache.bufferVariables.begin(), it));
}
if (programInterface == GL_UNIFORM_BLOCK) {
return programObject->GetUniformBlockIndex(name);
}
if (programInterface == GL_UNIFORM) {
const Int activeUniformIndex = programObject->GetActiveUniformIndex(resourceName);
return activeUniformIndex >= 0 ? static_cast<GLuint>(activeUniformIndex) : GL_INVALID_INDEX;
}
if (programInterface == GL_PROGRAM_INPUT) {
return FindProgramInputIndex(*programObject, resourceName);
}
if (programInterface == GL_PROGRAM_OUTPUT) {
return FindProgramOutputIndex(*programObject, resourceName);
}
return GL_INVALID_INDEX;
}
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) return;
auto& cache = GetProgramResourceCache(*programObject);
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
CopyResourceName(cache.storageBlocks[index].name, bufSize, length, name);
return;
}
if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
CopyResourceName(cache.bufferVariables[index].name, bufSize, length, name);
return;
}
if (programInterface == GL_UNIFORM_BLOCK && programObject->IsActiveUniformBlock(index)) {
CopyResourceName(programObject->GetUniformBlockName(index), bufSize, length, name);
return;
}
if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
CopyResourceName(programObject->GetActiveUniformName(index), bufSize, length, name);
return;
}
if (programInterface == GL_PROGRAM_INPUT && index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
CopyResourceName(programObject->GetActiveAttribName(index), bufSize, length, name);
return;
}
if (programInterface == GL_PROGRAM_OUTPUT &&
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
CopyResourceName(programObject->GetActiveFragmentOutputName(index), bufSize, length, name);
return;
}
if (length) *length = 0;
if (name && bufSize > 0) name[0] = '\0';
}
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || !props || !params || bufSize <= 0) return;
auto& cache = GetProgramResourceCache(*programObject);
GLsizei written = 0;
auto writeValue = [&](GLint value) {
if (written < bufSize) {
params[written++] = value;
}
};
for (GLsizei propIndex = 0; propIndex < propCount; ++propIndex) {
const GLenum prop = props[propIndex];
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
const auto& block = cache.storageBlocks[index];
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(block.name.size() + 1));
break;
case GL_BUFFER_BINDING:
writeValue(static_cast<GLint>(block.binding));
break;
case GL_BUFFER_DATA_SIZE:
writeValue(block.dataSize);
break;
case GL_NUM_ACTIVE_VARIABLES:
writeValue(static_cast<GLint>(block.activeVariables.size()));
break;
case GL_ACTIVE_VARIABLES:
for (const auto variable : block.activeVariables) writeValue(static_cast<GLint>(variable));
break;
default:
writeValue(0);
break;
}
} else if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
const auto& var = cache.bufferVariables[index];
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(var.name.size() + 1));
break;
case GL_TYPE:
writeValue(GL_FLOAT);
break;
case GL_ARRAY_SIZE:
writeValue(1);
break;
case GL_OFFSET:
writeValue(var.offset);
break;
case GL_BLOCK_INDEX:
writeValue(static_cast<GLint>(var.blockIndex));
break;
case GL_ARRAY_STRIDE:
case GL_MATRIX_STRIDE:
case GL_TOP_LEVEL_ARRAY_SIZE:
case GL_TOP_LEVEL_ARRAY_STRIDE:
case GL_IS_ROW_MAJOR:
writeValue(0);
break;
default:
writeValue(0);
break;
}
} else if (programInterface == GL_UNIFORM_BLOCK &&
programObject->IsActiveUniformBlock(index)) {
const auto activeVariables = GetUniformBlockActiveVariables(*programObject, index);
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(programObject->GetUniformBlockName(index).size() + 1));
break;
case GL_BUFFER_BINDING:
writeValue(static_cast<GLint>(programObject->GetUniformBlockBinding(index)));
break;
case GL_BUFFER_DATA_SIZE:
writeValue(static_cast<GLint>(programObject->GetUBOSizeAt(index)));
break;
case GL_NUM_ACTIVE_VARIABLES:
writeValue(static_cast<GLint>(activeVariables.size()));
break;
case GL_ACTIVE_VARIABLES:
for (const GLuint variableIndex : activeVariables) {
writeValue(static_cast<GLint>(variableIndex));
}
break;
case GL_REFERENCED_BY_VERTEX_SHADER:
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangVertex) ? GL_TRUE
: GL_FALSE);
break;
case GL_REFERENCED_BY_FRAGMENT_SHADER:
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangFragment) ? GL_TRUE
: GL_FALSE);
break;
case GL_REFERENCED_BY_COMPUTE_SHADER:
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangCompute) ? GL_TRUE
: GL_FALSE);
break;
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
writeValue(GL_FALSE);
break;
default:
writeValue(0);
break;
}
} else if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
const auto& uniformName = programObject->GetActiveUniformName(index);
const GLint location = programObject->GetUniformLocation(uniformName);
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(uniformName.size() + 1));
break;
case GL_TYPE:
writeValue(static_cast<GLint>(programObject->GetActiveUniformType(index)));
break;
case GL_ARRAY_SIZE:
writeValue(programObject->GetActiveUniformArraySize(index));
break;
case GL_BLOCK_INDEX:
writeValue(programObject->GetActiveUniformBlockIndex(index));
break;
case GL_LOCATION:
writeValue(location);
break;
case GL_OFFSET:
writeValue(location >= 0 && programObject->IsValidUniformLocation(location)
? static_cast<GLint>(programObject->GetUniformOffset(location))
: 0);
break;
case GL_ARRAY_STRIDE:
case GL_MATRIX_STRIDE:
case GL_IS_ROW_MAJOR:
case GL_TOP_LEVEL_ARRAY_SIZE:
case GL_TOP_LEVEL_ARRAY_STRIDE:
case GL_REFERENCED_BY_VERTEX_SHADER:
case GL_REFERENCED_BY_FRAGMENT_SHADER:
case GL_REFERENCED_BY_COMPUTE_SHADER:
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
writeValue(0);
break;
default:
writeValue(0);
break;
}
} else if (programInterface == GL_PROGRAM_INPUT &&
index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
const auto& resourceName = programObject->GetActiveAttribName(index);
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(resourceName.size() + 1));
break;
case GL_TYPE:
writeValue(static_cast<GLint>(programObject->GetActiveAttribType(index)));
break;
case GL_ARRAY_SIZE:
writeValue(programObject->GetActiveAttribArraySize(index));
break;
case GL_LOCATION:
writeValue(programObject->GetAttributeLocation(resourceName));
break;
case GL_REFERENCED_BY_VERTEX_SHADER:
writeValue(GL_TRUE);
break;
case GL_REFERENCED_BY_FRAGMENT_SHADER:
case GL_REFERENCED_BY_COMPUTE_SHADER:
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
case GL_IS_PER_PATCH:
case GL_LOCATION_INDEX:
writeValue(0);
break;
default:
writeValue(0);
break;
}
} else if (programInterface == GL_PROGRAM_OUTPUT &&
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
const auto& resourceName = programObject->GetActiveFragmentOutputName(index);
switch (prop) {
case GL_NAME_LENGTH:
writeValue(static_cast<GLint>(resourceName.size() + 1));
break;
case GL_TYPE:
writeValue(static_cast<GLint>(programObject->GetFragmentOutputType(index)));
break;
case GL_ARRAY_SIZE:
writeValue(programObject->GetActiveFragmentOutputArraySize(index));
break;
case GL_LOCATION:
writeValue(programObject->GetFragmentOutputLocation(index));
break;
case GL_LOCATION_INDEX:
writeValue(0);
break;
case GL_REFERENCED_BY_FRAGMENT_SHADER:
writeValue(GL_TRUE);
break;
case GL_REFERENCED_BY_VERTEX_SHADER:
case GL_REFERENCED_BY_COMPUTE_SHADER:
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
case GL_IS_PER_PATCH:
writeValue(0);
break;
default:
writeValue(0);
break;
}
} else {
writeValue(0);
}
}
if (length) *length = written;
}
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || !name) return -1;
if (programInterface == GL_UNIFORM) {
return programObject->GetUniformLocation(name);
}
if (programInterface == GL_PROGRAM_INPUT) {
return programObject->GetAttributeLocation(name);
}
if (programInterface == GL_PROGRAM_OUTPUT) {
return GetProgramOutputLocation(*programObject, name);
}
return -1;
}
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || !name) return -1;
if (programInterface == GL_PROGRAM_OUTPUT) {
return GetProgramOutputLocation(*programObject, name) >= 0 ? 0 : -1;
}
return -1;
}
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) return;
auto& cache = GetProgramResourceCache(*programObject);
if (!programObject || storageBlockName == nullptr) return;
const Int maxBindings = pActiveBackendObject
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
: 0;
@@ -1234,13 +797,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
return;
}
if (storageBlockIndex >= cache.storageBlocks.size()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage block index is not active."));
return;
}
cache.storageBlocks[storageBlockIndex].binding = storageBlockBinding;
// The frontend already validated that the name denotes an active block, and has
// already recorded the new binding on the program - which is what reseeds this cache
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
// (the common case: the very next draw reads it) from having to be thrown away.
auto& cache = GetProgramResourceCache(*programObject);
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
if (blockIndex == GL_INVALID_INDEX) return;
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
}
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
@@ -97,15 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
+135 -195
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "GL_Program.h"
#include "ProgramInterface.h"
#include "Config.h"
#include <cmath>
#include <limits>
@@ -109,32 +110,37 @@ namespace MobileGL::MG_Impl::GLImpl {
return programObject;
}
static bool IsProgramInterfaceEnum(GLenum programInterface) {
switch (programInterface) {
case GL_UNIFORM:
case GL_UNIFORM_BLOCK:
case GL_PROGRAM_INPUT:
case GL_PROGRAM_OUTPUT:
case GL_BUFFER_VARIABLE:
case GL_SHADER_STORAGE_BLOCK:
case GL_ATOMIC_COUNTER_BUFFER:
case GL_TRANSFORM_FEEDBACK_VARYING:
case GL_VERTEX_SUBROUTINE:
case GL_TESS_CONTROL_SUBROUTINE:
case GL_TESS_EVALUATION_SUBROUTINE:
case GL_GEOMETRY_SUBROUTINE:
case GL_FRAGMENT_SUBROUTINE:
case GL_COMPUTE_SUBROUTINE:
case GL_VERTEX_SUBROUTINE_UNIFORM:
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
case GL_COMPUTE_SUBROUTINE_UNIFORM:
return true;
default:
return false;
// The four non-location interface queries validate the NAME only: GL 4.6 imposes the
// successful-link requirement on GetProgramResourceLocation/LocationIndex alone, and
// requires the others to report a program that has never linked as one with zero active
// resources. Being stricter leaves a stray GL_INVALID_OPERATION behind that aborts the
// caller's next subcase.
static const SharedPtr<MG_State::GLState::ProgramObject>& TryToGetProgramForInterfaceQuery(GLuint program,
const char* caller) {
static const SharedPtr<MG_State::GLState::ProgramObject> nullProgramObject = nullptr;
if (!MG_State::pGLContext->ValidateProgramName(program)) {
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError(
error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
std::to_string(program) + " is not a program object."));
return nullProgramObject;
}
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
std::to_string(program) + " is not a program object."));
return nullProgramObject;
}
return programObject;
}
static bool IsProgramInterfaceEnum(GLenum programInterface) {
return ProgramInterface::IsInterfaceEnum(programInterface);
}
static bool IsSubroutineUniformInterface(GLenum programInterface) {
@@ -157,11 +163,16 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_ACTIVE_RESOURCES:
break;
case GL_MAX_NAME_LENGTH:
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER;
// Neither buffer interface has resource names. GL_TRANSFORM_FEEDBACK_BUFFER only
// became reachable here when IsInterfaceEnum grew the GL 4.4 interfaces, so it
// needs the same exclusion GL_ATOMIC_COUNTER_BUFFER already had.
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
break;
case GL_MAX_NUM_ACTIVE_VARIABLES:
valid = programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
programInterface == GL_SHADER_STORAGE_BLOCK;
programInterface == GL_SHADER_STORAGE_BLOCK ||
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
break;
case GL_MAX_NUM_COMPATIBLE_SUBROUTINES:
valid = IsSubroutineUniformInterface(programInterface);
@@ -180,7 +191,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
static bool ValidateNamedProgramResourceInterface(GLenum programInterface, const char* caller) {
if (!IsProgramInterfaceEnum(programInterface) || programInterface == GL_ATOMIC_COUNTER_BUFFER) {
if (!ProgramInterface::IsNamedInterface(programInterface)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
@@ -190,66 +201,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
static Int GetKnownProgramResourceCount(const SharedPtr<MG_State::GLState::ProgramObject>& programObject,
GLenum programInterface) {
switch (programInterface) {
case GL_UNIFORM:
return programObject->GetUniformCount();
case GL_UNIFORM_BLOCK:
return programObject->GetActiveUniformBlocksCount();
case GL_PROGRAM_INPUT:
return programObject->GetActiveAttributesCount();
case GL_PROGRAM_OUTPUT:
return programObject->GetActiveFragmentOutputCount();
default:
return -1;
}
}
// The GL_UNIFORM interface and glGetActiveUniform(s)iv are the same query in two
// spellings, so they answer from the same place - the frontend reflection. The backend
// program is not that place: it does not exist at all for a program whose types its
// shading language cannot express (a double-precision uniform has no ESSL form), and
// the interface queries would then describe a program with no uniforms.
//
// Writes the GL_UNIFORM value of `prop` for active uniform `index`; false for a prop
// the reflection does not model, which the caller forwards to the backend instead.
Bool GetUniformResourceProp(const SharedPtr<MG_State::GLState::ProgramObject>& programObject, Uint index,
GLenum prop, GLint* out) {
switch (prop) {
case GL_TYPE:
*out = static_cast<GLint>(programObject->GetActiveUniformType(index));
return true;
case GL_ARRAY_SIZE:
*out = programObject->GetActiveUniformArraySize(index);
return true;
case GL_NAME_LENGTH:
*out = static_cast<GLint>(programObject->GetActiveUniformName(index).length() + 1);
return true;
case GL_BLOCK_INDEX:
*out = programObject->GetActiveUniformBlockIndex(index);
return true;
case GL_OFFSET:
*out = programObject->GetActiveUniformOffset(index);
return true;
case GL_ARRAY_STRIDE:
*out = programObject->GetActiveUniformArrayStride(index);
return true;
case GL_MATRIX_STRIDE:
*out = programObject->GetActiveUniformMatrixStride(index);
return true;
case GL_IS_ROW_MAJOR:
*out = programObject->GetActiveUniformIsRowMajor(index);
return true;
case GL_LOCATION:
// A block member has no location; GetUniformLocation already reports -1 for one.
*out = programObject->GetUniformLocation(programObject->GetActiveUniformName(index));
return true;
default:
return false;
}
}
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
if (bufSize <= 0) {
if (length) *length = 0;
@@ -2654,171 +2605,160 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return;
if (!ValidateProgramInterfaceivQuery(programInterface, pname)) return;
auto getProgramInterfaceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramInterfaceiv;
if (!getProgramInterfaceiv) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support program interface queries."));
if (!params) return;
switch (pname) {
case GL_ACTIVE_RESOURCES:
*params = ProgramInterface::GetActiveResourceCount(*programObject, programInterface);
return;
case GL_MAX_NAME_LENGTH:
*params = ProgramInterface::GetMaxNameLength(*programObject, programInterface);
return;
case GL_MAX_NUM_ACTIVE_VARIABLES:
*params = ProgramInterface::GetMaxNumActiveVariables(*programObject, programInterface);
return;
default:
// GL_MAX_NUM_COMPATIBLE_SUBROUTINES: the subroutine interfaces are always empty
// here (glslang refuses `subroutine` when generating SPIR-V), so zero it is.
*params = 0;
return;
}
if (programInterface == GL_UNIFORM) {
if (pname == GL_ACTIVE_RESOURCES) {
*params = static_cast<GLint>(programObject->GetUniformCount());
return;
}
if (pname == GL_MAX_NAME_LENGTH) {
// Stored as the bare length; GL_MAX_NAME_LENGTH counts the terminator.
*params = programObject->GetUniformMaxLength() + 1;
return;
}
}
getProgramInterfaceiv(program, programInterface, pname, params);
}
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return GL_INVALID_INDEX;
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
if (!name) return GL_INVALID_INDEX;
if (programInterface == GL_UNIFORM) {
const Int uniformIndex = programObject->GetActiveUniformIndex(name);
return uniformIndex < 0 ? GL_INVALID_INDEX : static_cast<GLuint>(uniformIndex);
}
auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
if (!getProgramResourceIndex) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support program interface queries."));
return GL_INVALID_INDEX;
}
GLuint index = getProgramResourceIndex(program, programInterface, name);
const String resourceName = name;
if (index == GL_INVALID_INDEX && resourceName.length() > 3 &&
resourceName.compare(resourceName.length() - 3, 3, "[0]") == 0) {
index = getProgramResourceIndex(program, programInterface,
resourceName.substr(0, resourceName.length() - 3).c_str());
}
return index;
return ProgramInterface::GetResourceIndex(*programObject, programInterface, name);
}
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name) {
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return;
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return;
const Int resourceCount = GetKnownProgramResourceCount(programObject, programInterface);
if (resourceCount >= 0 && index >= static_cast<GLuint>(resourceCount)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
return;
}
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
return;
}
if (programInterface == GL_UNIFORM) {
// Same index space GetProgramResourceIndex answers in, and the range check above
// already used it.
const String& uniformName = programObject->GetActiveUniformName(index);
CopyStr(bufSize, length, name, uniformName.c_str(), static_cast<GLsizei>(uniformName.length()));
return;
}
auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName;
if (!getProgramResourceName) {
String resourceName;
if (!ProgramInterface::GetResourceName(*programObject, programInterface, index, resourceName)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support program interface queries."));
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
return;
}
getProgramResourceName(program, programInterface, index, bufSize, length, name);
CopyStr(bufSize, length, name, resourceName.c_str(), static_cast<GLsizei>(resourceName.length()));
}
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return;
if (propCount < 0 || bufSize < 0) {
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"propCount and bufSize must be non-negative."));
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Unsupported program interface."));
return;
}
if (programInterface == GL_UNIFORM) {
if (index >= programObject->GetUniformCount()) {
if (propCount <= 0 || bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"propCount must be positive and bufSize "
"non-negative."));
return;
}
if (props == nullptr) return;
// Both prop checks run BEFORE any value is produced: a property this command does
// not know at all is INVALID_ENUM, one it knows but the interface does not carry is
// INVALID_OPERATION (GL 4.6 Table 7.2). The two are deliberately different errors.
for (GLsizei i = 0; i < propCount; ++i) {
if (!ProgramInterface::IsResourceProp(props[i])) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "prop is not a valid property name."));
return;
}
if (!ProgramInterface::InterfaceSupportsProp(programInterface, props[i])) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"prop is not supported for this program interface."));
return;
}
}
Vector<GLint> values;
for (GLsizei i = 0; i < propCount; ++i) {
if (!ProgramInterface::GetResourceProp(*programObject, programInterface, index, props[i], values)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
return;
}
if (props == nullptr || params == nullptr) return;
GLsizei written = 0;
for (GLsizei i = 0; i < propCount && written < bufSize; ++i) {
GLint value = 0;
if (!GetUniformResourceProp(programObject, index, props[i], &value)) {
// GL_ATOMIC_COUNTER_BUFFER_INDEX and the GL_REFERENCED_BY_* stage props are
// not modelled here; ask the backend, which indexes resources by name.
auto backendGetIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
auto backendGetiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
if (backendGetIndex && backendGetiv) {
const GLuint backendIndex = backendGetIndex(program, GL_UNIFORM,
programObject->GetActiveUniformName(index).c_str());
if (backendIndex != GL_INVALID_INDEX) {
GLsizei one = 0;
backendGetiv(program, GL_UNIFORM, backendIndex, 1, &props[i], 1, &one, &value);
}
}
}
params[written++] = value;
}
if (length) *length = written;
return;
}
auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
if (!getProgramResourceiv) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support program interface queries."));
return;
}
getProgramResourceiv(program, programInterface, index, propCount, props, bufSize, length, params);
if (params == nullptr) return;
const GLsizei written = static_cast<GLsizei>(std::min<SizeT>(values.size(), static_cast<SizeT>(bufSize)));
for (GLsizei i = 0; i < written; ++i) params[i] = values[i];
if (length) *length = written;
}
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
// Unlike the four queries above, this one and GetProgramResourceLocationIndex really
// do require a successful link (GL 4.6 §7.3.1.3).
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
if (!programObject) return -1;
auto getProgramResourceLocation = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocation;
if (!getProgramResourceLocation) {
if (!ProgramInterface::InterfaceHasLocations(programInterface)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support program interface queries."));
"Program interface has no locations."));
return -1;
}
return getProgramResourceLocation(program, programInterface, name);
return ProgramInterface::GetResourceLocation(*programObject, programInterface, name);
}
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
if (!programObject) return -1;
auto getProgramResourceLocationIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocationIndex;
if (!getProgramResourceLocationIndex) return -1;
return getProgramResourceLocationIndex(program, programInterface, name);
if (programInterface != GL_PROGRAM_OUTPUT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"GetProgramResourceLocationIndex only accepts GL_PROGRAM_OUTPUT."));
return -1;
}
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
}
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
// Since wave 2 that index is the interface-query layer's, so this is where the one index
// space the application sees gets turned into whatever the backend's is; the backends are
// handed the block NAME and do their own lookup. Getting this wrong is silent: the call
// succeeds and rebinds a DIFFERENT buffer.
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject || !programObject->GetLinkStatus()) return;
if (!ValidateShaderStorageBlockBinding(storageBlockBinding)) return;
String blockName;
if (!ProgramInterface::GetResourceName(*programObject, GL_SHADER_STORAGE_BLOCK, storageBlockIndex,
blockName)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"storageBlockIndex is not an active shader storage block index."));
return;
}
// Recorded before the backend call, and independently of whether a backend is even
// present: this is the state GL_BUFFER_BINDING reports, and it is also what reseeds a
// backend's own reflection cache after any rebuild.
programObject->SetShaderStorageBlockBinding(blockName, storageBlockBinding);
auto shaderStorageBlockBinding = MG_Backend::gBackendFunctionsTable.GL.ShaderStorageBlockBinding;
if (!shaderStorageBlockBinding) {
MG_State::pGLContext->RecordError(
@@ -2827,7 +2767,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support shader storage block binding."));
return;
}
shaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding);
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
}
void ValidateProgram(GLuint program) {
@@ -0,0 +1,841 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.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
#include "ProgramInterface.h"
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <cstring>
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
namespace {
// glslang folds atomic counters into synthesized blocks named
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
enum class BlockKind {
Uniform, // a real GL uniform block
GlobalUbo, // the synthesized MGL_GLOBAL_UBO: GL sees its members as default-block
AtomicCounter, // gl_AtomicCounterBlock_<binding>
Storage, // a shader storage block
};
// One row of any interface. Fields a given interface does not have keep the
// spec-mandated "not applicable" value, so a prop read never has to special-case
// the interface a second time.
struct Resource {
String name;
GLenum type = GL_NONE;
GLint arraySize = 1;
GLint location = -1;
GLint locationIndex = -1;
GLint blockIndex = -1;
GLint offset = -1;
GLint arrayStride = -1;
GLint matrixStride = -1;
GLint isRowMajor = 0;
GLint atomicCounterBufferIndex = -1;
GLint topLevelArraySize = 0;
GLint topLevelArrayStride = 0;
GLint bufferBinding = 0;
GLint bufferDataSize = 0;
GLint isPerPatch = 0;
GLint xfbBufferIndex = 0;
Uint32 stages = 0; // EShLanguageMask
Vector<GLuint> activeVariables;
};
using ResourceList = Vector<Resource>;
struct Model {
ResourceList uniforms;
ResourceList uniformBlocks;
ResourceList atomicCounterBuffers;
ResourceList bufferVariables;
ResourceList storageBlocks;
ResourceList programInputs;
ResourceList programOutputs;
ResourceList xfbVaryings;
Bool valid = false;
};
const ResourceList& EmptyList() {
static const ResourceList empty;
return empty;
}
// ---- name spelling (cluster 6) -------------------------------------------------
Bool EndsWithZeroSubscript(const String& name) {
return name.length() >= 3 && name.compare(name.length() - 3, 3, "[0]") == 0;
}
// The enumerated spelling of an array resource is "name[0]". glslang already applies
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
// stage inputs/outputs, so those get it here.
String WithArraySuffix(const String& name, const glslang::TType* type) {
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
return name + "[0]";
}
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
// (a shader storage block's unsized trailing member), 1 for a non-array.
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
if (type != nullptr && type->isArray()) {
if (!type->isSizedArray()) return 0;
return type->getOuterArraySize();
}
return reflectedSize < 1 ? 1 : reflectedSize;
}
// Two spellings name the same resource when they are equal, or differ only by the
// "[0]" the enumeration appends to an array.
Bool NamesMatch(const String& resourceName, const String& query) {
if (resourceName == query) return true;
if (EndsWithZeroSubscript(resourceName) &&
resourceName.compare(0, resourceName.length() - 3, query) == 0) {
return true;
}
return EndsWithZeroSubscript(query) && query.compare(0, query.length() - 3, resourceName) == 0;
}
// Splits "base[k]" into ("base", k). GL 4.6 §7.3.1.1 requires the subscript to be a
// decimal integer with no white space and no leading zeros, which is exactly what
// separates array-names' "a[1]" (resolves) from "a[01]", "a[0 + 0]" and "a[ 0]" (do
// not). Returns false when there is no trailing subscript at all; sets `malformed`
// when there is one but it is not a strict decimal.
Bool SplitTrailingSubscript(const String& name, String& outBase, Uint& outElement, Bool& outMalformed) {
outMalformed = false;
if (name.empty() || name.back() != ']') return false;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos) return false;
const SizeT first = bracket + 1;
const SizeT last = name.length() - 1; // one past the digits
if (first >= last) {
outMalformed = true;
return false;
}
// No leading zeros: "0" is the only spelling that may start with '0'.
if (name[first] == '0' && last - first > 1) {
outMalformed = true;
return false;
}
Uint element = 0;
for (SizeT i = first; i < last; ++i) {
if (name[i] < '0' || name[i] > '9') {
outMalformed = true;
return false;
}
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) {
outMalformed = true;
return false;
}
}
outBase = name.substr(0, bracket);
outElement = element;
return true;
}
// ---- block classification ------------------------------------------------------
Bool IsAtomicCounterBlockName(const String& name) {
return name.compare(0, std::strlen(kAtomicCounterBlockPrefix), kAtomicCounterBlockPrefix) == 0;
}
// "gl_AtomicCounterBlock_5" -> 5. The suffix is the GL binding the counters were
// declared with, which glslang does NOT keep in the block's own layout qualifier
// (that one is remapped to a plain buffer binding).
GLint AtomicCounterBlockBinding(const String& name) {
const SizeT underscore = name.rfind('_');
if (underscore == String::npos || underscore + 1 >= name.length()) return 0;
GLint binding = 0;
for (SizeT i = underscore + 1; i < name.length(); ++i) {
if (name[i] < '0' || name[i] > '9') return 0;
binding = binding * 10 + (name[i] - '0');
}
return binding;
}
// Element index of an arrayed block instance ("TrickyBuffer[1]" -> 1).
GLint BlockArrayElement(const String& name) {
String base;
Uint element = 0;
Bool malformed = false;
if (!SplitTrailingSubscript(name, base, element, malformed)) return 0;
return static_cast<GLint>(element);
}
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
return BlockKind::GlobalUbo;
}
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
const glslang::TType* type = block.getType();
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
return BlockKind::Uniform;
}
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
// uniform matrices. 0 for a non-matrix.
GLint MatrixStrideOf(const glslang::TType* type) {
if (type == nullptr || !type->isMatrix()) return 0;
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
constexpr int scalarSize = 4;
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
: (strideVectorComponents == 2) ? 2 * scalarSize
: 4 * scalarSize;
return (vectorAlignment + 15) & ~15;
}
GLint IsRowMajorOf(const glslang::TType* type) {
if (type == nullptr || !type->isMatrix()) return 0;
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
}
GLint MappedLocation(Int rawLocation) {
// glslang parks "no location" at layoutLocationEnd; GL spells it -1.
if (rawLocation < 0 || rawLocation >= static_cast<Int>(glslang::TQualifier::layoutLocationEnd)) return -1;
return rawLocation;
}
// ---- model construction --------------------------------------------------------
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
blockKind.assign(blockCount, BlockKind::Uniform);
blockInterfaceIndex.assign(blockCount, -1);
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
const BlockKind kind = ClassifyBlock(block);
blockKind[tIndex] = kind;
if (kind == BlockKind::AtomicCounter) {
Resource resource;
// GL_ATOMIC_COUNTER_BUFFER resources have no name (and GetProgramResource
// Index/Name reject the interface outright, which is why this stays empty).
resource.bufferBinding = AtomicCounterBlockBinding(block.name);
resource.bufferDataSize = block.size;
resource.stages = static_cast<Uint32>(block.stages);
blockInterfaceIndex[tIndex] = static_cast<Int>(model.atomicCounterBuffers.size());
model.atomicCounterBuffers.push_back(Move(resource));
} else if (kind == BlockKind::Storage) {
Resource resource;
resource.name = block.name;
// glslang reports the DECLARED binding for every instance of an arrayed
// block; GL gives element k the binding base + k. That is only the initial
// value: GL_BUFFER_BINDING must report the CURRENT binding, so a later
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
// exactly the same rule GL_UNIFORM_BLOCK follows through
// GetUniformBlockBinding below).
const GLint declared = block.getBinding();
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
resource.bufferDataSize = block.size;
resource.stages = static_cast<Uint32>(block.stages);
blockInterfaceIndex[tIndex] = static_cast<Int>(model.storageBlocks.size());
model.storageBlocks.push_back(Move(resource));
}
}
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
// with them (which is exactly what the CTS does).
const Int glBlockCount = program.GetActiveUniformBlocksCount();
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
Resource resource;
resource.name = program.GetUniformBlockName(glIndex);
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
if (tIndex >= 0 && tIndex < blockCount) {
resource.stages =
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
}
model.uniformBlocks.push_back(Move(resource));
}
}
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
const Vector<BlockKind>& blockKind,
const Vector<Int>& blockInterfaceIndex) {
const Uint uniformCount = program.GetUniformCount();
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
const Int tIndex = program.TProgramUniformIndex(glIndex);
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
const glslang::TType* type = refl.getType();
const Int owner = refl.index;
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
? blockKind[owner]
: BlockKind::GlobalUbo;
Resource resource;
resource.name = refl.name;
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.stages = static_cast<Uint32>(refl.stages);
if (kind == BlockKind::Storage) {
resource.blockIndex = blockInterfaceIndex[owner];
resource.offset = refl.offset;
resource.arrayStride = refl.arrayStride;
resource.matrixStride = MatrixStrideOf(type);
resource.isRowMajor = IsRowMajorOf(type);
// GL requires 1 for a member that is not inside a top-level array (and for
// the top-level array itself); glslang leaves 0/-1 there.
resource.topLevelArraySize = refl.topLevelArraySize > 0 ? refl.topLevelArraySize : 1;
resource.topLevelArrayStride = refl.topLevelArrayStride;
model.bufferVariables.push_back(Move(resource));
continue;
}
if (kind == BlockKind::AtomicCounter) {
// An atomic counter is a default-block uniform with no location and no
// owning uniform block; what it does have is a buffer to point at.
resource.type = GL_UNSIGNED_INT_ATOMIC_COUNTER;
resource.blockIndex = -1;
resource.offset = refl.offset;
resource.arrayStride = refl.arrayStride;
resource.matrixStride = 0;
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
resource.location = -1;
} else {
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
resource.offset = program.GetActiveUniformOffset(glIndex);
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
// A member of a named uniform block has no location, whatever the
// frontend's own location table says (it hands one out to every uniform
// so glUniform* can address block members through the global UBO).
resource.location =
resource.blockIndex >= 0 ? -1 : program.GetUniformLocation(refl.name);
}
model.uniforms.push_back(Move(resource));
}
// GL_ACTIVE_VARIABLES, both directions.
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
const Resource& uniform = model.uniforms[i];
if (uniform.atomicCounterBufferIndex >= 0 &&
uniform.atomicCounterBufferIndex < static_cast<GLint>(model.atomicCounterBuffers.size())) {
model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back(
static_cast<GLuint>(i));
}
}
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
// Members of an arrayed block are reflected once, against instance [0].
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
if (model.uniforms[i].blockIndex == owner) {
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
}
}
}
for (SizeT blockIndex = 0; blockIndex < model.storageBlocks.size(); ++blockIndex) {
for (SizeT i = 0; i < model.bufferVariables.size(); ++i) {
if (model.bufferVariables[i].blockIndex == static_cast<GLint>(blockIndex)) {
model.storageBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
}
}
}
}
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
const Int inputCount = mutableReflection.getNumPipeInputs();
for (Int index = 0; index < inputCount; ++index) {
const auto& refl = mutableReflection.getPipeInput(index);
const glslang::TType* type = refl.getType();
Resource resource;
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
// names; GL enumerates the GL spellings.
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
resource.name = WithArraySuffix(glName, type);
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.location = program.GetAttributeLocation(refl.name);
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages);
model.programInputs.push_back(Move(resource));
}
const Int outputCount = mutableReflection.getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) {
const auto& refl = mutableReflection.getPipeOutput(index);
const glslang::TType* type = refl.getType();
Resource resource;
resource.name = WithArraySuffix(refl.name, type);
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
if (resource.location < 0) {
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
// output both have no location, and therefore no color index either.
resource.locationIndex = -1;
} else {
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
// glBindFragDataLocationIndexed wins; otherwise the shader's
// layout(index = N), which the frag-data maps never saw.
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
}
}
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages);
model.programOutputs.push_back(Move(resource));
}
}
void BuildXfb(ProgramObject& program, Model& model) {
const auto& requested = program.GetTransformFeedbackInterfaceNames();
const auto& captured = program.GetTransformFeedbackVaryings();
for (const String& name : requested) {
Resource resource;
resource.name = name;
// ARB_transform_feedback3's layout controls are enumerated as resources of
// type NONE: gl_NextBuffer with array size 0, gl_SkipComponentsN with N.
if (name == "gl_NextBuffer") {
resource.type = GL_NONE;
resource.arraySize = 0;
} else if (name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' &&
name[17] <= '4') {
resource.type = GL_NONE;
resource.arraySize = name[17] - '0';
} else {
resource.type = GL_NONE;
resource.arraySize = 1;
for (const auto& varying : captured) {
if (varying.name != name) continue;
resource.type = varying.type;
resource.arraySize = varying.size < 1 ? 1 : varying.size;
resource.offset = static_cast<GLint>(varying.offsetBytes);
resource.xfbBufferIndex = static_cast<GLint>(varying.bufferIndex);
break;
}
}
model.xfbVaryings.push_back(Move(resource));
}
}
Model BuildModel(ProgramObject& program) {
Model model;
if (!program.GetLinkStatus()) return model;
const glslang::TProgram* reflection = program.GetReflection();
if (reflection == nullptr) return model;
model.valid = true;
Vector<BlockKind> blockKind;
Vector<Int> blockInterfaceIndex;
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
BuildStageIO(program, *reflection, model);
BuildXfb(program, model);
return model;
}
const ResourceList& Select(const Model& model, GLenum programInterface) {
switch (programInterface) {
case GL_UNIFORM:
return model.uniforms;
case GL_UNIFORM_BLOCK:
return model.uniformBlocks;
case GL_ATOMIC_COUNTER_BUFFER:
return model.atomicCounterBuffers;
case GL_BUFFER_VARIABLE:
return model.bufferVariables;
case GL_SHADER_STORAGE_BLOCK:
return model.storageBlocks;
case GL_PROGRAM_INPUT:
return model.programInputs;
case GL_PROGRAM_OUTPUT:
return model.programOutputs;
case GL_TRANSFORM_FEEDBACK_VARYING:
return model.xfbVaryings;
default:
// The subroutine interfaces are accepted by the API but nothing can populate
// them: glslang refuses `subroutine` when generating SPIR-V, so a program
// using one never links. Zero active resources is the honest answer.
return EmptyList();
}
}
} // namespace
Bool IsInterfaceEnum(GLenum programInterface) {
switch (programInterface) {
case GL_UNIFORM:
case GL_UNIFORM_BLOCK:
case GL_PROGRAM_INPUT:
case GL_PROGRAM_OUTPUT:
case GL_BUFFER_VARIABLE:
case GL_SHADER_STORAGE_BLOCK:
case GL_ATOMIC_COUNTER_BUFFER:
case GL_TRANSFORM_FEEDBACK_VARYING:
case GL_TRANSFORM_FEEDBACK_BUFFER:
case GL_VERTEX_SUBROUTINE:
case GL_TESS_CONTROL_SUBROUTINE:
case GL_TESS_EVALUATION_SUBROUTINE:
case GL_GEOMETRY_SUBROUTINE:
case GL_FRAGMENT_SUBROUTINE:
case GL_COMPUTE_SUBROUTINE:
case GL_VERTEX_SUBROUTINE_UNIFORM:
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
case GL_COMPUTE_SUBROUTINE_UNIFORM:
return true;
default:
return false;
}
}
Bool IsNamedInterface(GLenum programInterface) {
// GL 4.6 §7.3.1.2: the two buffer interfaces have no resource names, and asking for
// one is INVALID_ENUM (deliberately asymmetric with GetProgramInterfaceiv, which
// does count them).
return IsInterfaceEnum(programInterface) && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
}
Bool InterfaceHasLocations(GLenum programInterface) {
switch (programInterface) {
case GL_UNIFORM:
case GL_PROGRAM_INPUT:
case GL_PROGRAM_OUTPUT:
case GL_VERTEX_SUBROUTINE_UNIFORM:
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
case GL_COMPUTE_SUBROUTINE_UNIFORM:
return true;
default:
return false;
}
}
Bool IsResourceProp(GLenum prop) {
switch (prop) {
case GL_NAME_LENGTH:
case GL_TYPE:
case GL_ARRAY_SIZE:
case GL_OFFSET:
case GL_BLOCK_INDEX:
case GL_ARRAY_STRIDE:
case GL_MATRIX_STRIDE:
case GL_IS_ROW_MAJOR:
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
case GL_BUFFER_BINDING:
case GL_BUFFER_DATA_SIZE:
case GL_NUM_ACTIVE_VARIABLES:
case GL_ACTIVE_VARIABLES:
case GL_REFERENCED_BY_VERTEX_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_FRAGMENT_SHADER:
case GL_REFERENCED_BY_COMPUTE_SHADER:
case GL_TOP_LEVEL_ARRAY_SIZE:
case GL_TOP_LEVEL_ARRAY_STRIDE:
case GL_LOCATION:
case GL_LOCATION_INDEX:
case GL_IS_PER_PATCH:
case GL_LOCATION_COMPONENT:
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
case GL_NUM_COMPATIBLE_SUBROUTINES:
case GL_COMPATIBLE_SUBROUTINES:
return true;
default:
return false;
}
}
// GL 4.6 Table 7.2, transcribed row by row: which interfaces each property applies to.
// Too tight a table turns a currently-answered prop into a fresh INVALID_OPERATION, so
// the rows below are deliberately no narrower than the spec's.
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop) {
const Bool isSubroutine =
programInterface == GL_VERTEX_SUBROUTINE || programInterface == GL_TESS_CONTROL_SUBROUTINE ||
programInterface == GL_TESS_EVALUATION_SUBROUTINE || programInterface == GL_GEOMETRY_SUBROUTINE ||
programInterface == GL_FRAGMENT_SUBROUTINE || programInterface == GL_COMPUTE_SUBROUTINE;
const Bool isSubroutineUniform =
programInterface == GL_VERTEX_SUBROUTINE_UNIFORM ||
programInterface == GL_TESS_CONTROL_SUBROUTINE_UNIFORM ||
programInterface == GL_TESS_EVALUATION_SUBROUTINE_UNIFORM ||
programInterface == GL_GEOMETRY_SUBROUTINE_UNIFORM ||
programInterface == GL_FRAGMENT_SUBROUTINE_UNIFORM || programInterface == GL_COMPUTE_SUBROUTINE_UNIFORM;
switch (prop) {
case GL_NAME_LENGTH:
return programInterface != GL_ATOMIC_COUNTER_BUFFER && programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
case GL_TYPE:
case GL_ARRAY_SIZE:
return programInterface == GL_UNIFORM || programInterface == GL_PROGRAM_INPUT ||
programInterface == GL_PROGRAM_OUTPUT || programInterface == GL_BUFFER_VARIABLE ||
programInterface == GL_TRANSFORM_FEEDBACK_VARYING ||
(prop == GL_ARRAY_SIZE && isSubroutineUniform);
case GL_OFFSET:
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE ||
programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
case GL_BLOCK_INDEX:
case GL_ARRAY_STRIDE:
case GL_MATRIX_STRIDE:
case GL_IS_ROW_MAJOR:
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE;
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
return programInterface == GL_UNIFORM;
case GL_BUFFER_BINDING:
case GL_NUM_ACTIVE_VARIABLES:
case GL_ACTIVE_VARIABLES:
// Table 7.2 lists GL_TRANSFORM_FEEDBACK_BUFFER on these three rows too. This
// implementation enumerates no resources on that interface, so the query still
// ends in an error - but INVALID_VALUE for the out-of-range index, not the
// INVALID_OPERATION a narrower table would invent.
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
programInterface == GL_SHADER_STORAGE_BLOCK ||
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
case GL_BUFFER_DATA_SIZE:
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
programInterface == GL_SHADER_STORAGE_BLOCK;
case GL_REFERENCED_BY_VERTEX_SHADER:
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
case GL_REFERENCED_BY_GEOMETRY_SHADER:
case GL_REFERENCED_BY_FRAGMENT_SHADER:
case GL_REFERENCED_BY_COMPUTE_SHADER:
return programInterface == GL_UNIFORM || programInterface == GL_UNIFORM_BLOCK ||
programInterface == GL_ATOMIC_COUNTER_BUFFER || programInterface == GL_BUFFER_VARIABLE ||
programInterface == GL_SHADER_STORAGE_BLOCK || programInterface == GL_PROGRAM_INPUT ||
programInterface == GL_PROGRAM_OUTPUT || isSubroutineUniform;
case GL_TOP_LEVEL_ARRAY_SIZE:
case GL_TOP_LEVEL_ARRAY_STRIDE:
return programInterface == GL_BUFFER_VARIABLE;
case GL_LOCATION:
return InterfaceHasLocations(programInterface);
case GL_LOCATION_INDEX:
return programInterface == GL_PROGRAM_OUTPUT;
case GL_IS_PER_PATCH:
case GL_LOCATION_COMPONENT:
return programInterface == GL_PROGRAM_INPUT || programInterface == GL_PROGRAM_OUTPUT;
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
return programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
return programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
case GL_NUM_COMPATIBLE_SUBROUTINES:
case GL_COMPATIBLE_SUBROUTINES:
return isSubroutineUniform;
default:
(void)isSubroutine;
return false;
}
}
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface) {
const Model model = BuildModel(program);
return static_cast<Int>(Select(model, programInterface).size());
}
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface) {
if (!IsNamedInterface(programInterface)) return 0;
const Model model = BuildModel(program);
SizeT longest = 0;
for (const Resource& resource : Select(model, programInterface)) {
longest = std::max(longest, resource.name.length() + 1);
}
return static_cast<Int>(longest);
}
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface) {
const Model model = BuildModel(program);
SizeT longest = 0;
for (const Resource& resource : Select(model, programInterface)) {
longest = std::max(longest, resource.activeVariables.size());
}
return static_cast<Int>(longest);
}
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name) {
if (name == nullptr || name[0] == '\0') return GL_INVALID_INDEX;
const Model model = BuildModel(program);
const ResourceList& resources = Select(model, programInterface);
const String query = name;
// The layout controls of an interleaved capture are enumerable but not addressable
// by name (GL 4.6 §7.3.1.1).
if (programInterface == GL_TRANSFORM_FEEDBACK_VARYING &&
(query == "gl_NextBuffer" ||
(query.size() == 18 && query.compare(0, 17, "gl_SkipComponents") == 0))) {
return GL_INVALID_INDEX;
}
for (SizeT i = 0; i < resources.size(); ++i) {
if (NamesMatch(resources[i].name, query)) return static_cast<GLuint>(i);
}
return GL_INVALID_INDEX;
}
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName) {
const Model model = BuildModel(program);
const ResourceList& resources = Select(model, programInterface);
if (index >= resources.size()) return false;
outName = resources[index].name;
return true;
}
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
Vector<GLint>& outValues) {
const Model model = BuildModel(program);
const ResourceList& resources = Select(model, programInterface);
if (index >= resources.size()) return false;
const Resource& resource = resources[index];
const auto referencedBy = [&resource](EShLanguage stage) {
return (resource.stages & static_cast<Uint32>(1u << stage)) != 0 ? GL_TRUE : GL_FALSE;
};
switch (prop) {
case GL_NAME_LENGTH:
outValues.push_back(static_cast<GLint>(resource.name.length() + 1));
break;
case GL_TYPE:
outValues.push_back(static_cast<GLint>(resource.type));
break;
case GL_ARRAY_SIZE:
outValues.push_back(resource.arraySize);
break;
case GL_OFFSET:
outValues.push_back(resource.offset);
break;
case GL_BLOCK_INDEX:
outValues.push_back(resource.blockIndex);
break;
case GL_ARRAY_STRIDE:
outValues.push_back(resource.arrayStride);
break;
case GL_MATRIX_STRIDE:
outValues.push_back(resource.matrixStride);
break;
case GL_IS_ROW_MAJOR:
outValues.push_back(resource.isRowMajor);
break;
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
outValues.push_back(resource.atomicCounterBufferIndex);
break;
case GL_BUFFER_BINDING:
outValues.push_back(resource.bufferBinding);
break;
case GL_BUFFER_DATA_SIZE:
outValues.push_back(resource.bufferDataSize);
break;
case GL_NUM_ACTIVE_VARIABLES:
outValues.push_back(static_cast<GLint>(resource.activeVariables.size()));
break;
case GL_ACTIVE_VARIABLES:
for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast<GLint>(variable));
break;
case GL_REFERENCED_BY_VERTEX_SHADER:
outValues.push_back(referencedBy(EShLangVertex));
break;
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
outValues.push_back(referencedBy(EShLangTessControl));
break;
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
outValues.push_back(referencedBy(EShLangTessEvaluation));
break;
case GL_REFERENCED_BY_GEOMETRY_SHADER:
outValues.push_back(referencedBy(EShLangGeometry));
break;
case GL_REFERENCED_BY_FRAGMENT_SHADER:
outValues.push_back(referencedBy(EShLangFragment));
break;
case GL_REFERENCED_BY_COMPUTE_SHADER:
outValues.push_back(referencedBy(EShLangCompute));
break;
case GL_TOP_LEVEL_ARRAY_SIZE:
outValues.push_back(resource.topLevelArraySize);
break;
case GL_TOP_LEVEL_ARRAY_STRIDE:
outValues.push_back(resource.topLevelArrayStride);
break;
case GL_LOCATION:
outValues.push_back(resource.location);
break;
case GL_LOCATION_INDEX:
outValues.push_back(resource.locationIndex);
break;
case GL_IS_PER_PATCH:
outValues.push_back(resource.isPerPatch);
break;
case GL_LOCATION_COMPONENT:
outValues.push_back(0);
break;
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
outValues.push_back(resource.xfbBufferIndex);
break;
default:
outValues.push_back(0);
break;
}
return true;
}
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name) {
if (name == nullptr || name[0] == '\0') return -1;
const String query = name;
String base;
Uint element = 0;
Bool malformed = false;
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
if (malformed) return -1;
const Model model = BuildModel(program);
const ResourceList& resources = Select(model, programInterface);
for (const Resource& resource : resources) {
if (NamesMatch(resource.name, query)) return resource.location;
}
if (!subscripted || element == 0) return -1;
// "d[1]" addresses the second element of an array resource enumerated as "d[0]".
for (const Resource& resource : resources) {
if (!NamesMatch(resource.name, base)) continue;
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
return resource.location + static_cast<GLint>(element);
}
return -1;
}
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name) {
if (programInterface != GL_PROGRAM_OUTPUT || name == nullptr || name[0] == '\0') return -1;
const String query = name;
String base;
Uint element = 0;
Bool malformed = false;
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
if (malformed) return -1;
const Model model = BuildModel(program);
for (const Resource& resource : model.programOutputs) {
if (NamesMatch(resource.name, query)) return resource.locationIndex;
}
if (!subscripted) return -1;
for (const Resource& resource : model.programOutputs) {
if (!NamesMatch(resource.name, base)) continue;
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
return resource.locationIndex;
}
return -1;
}
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
@@ -0,0 +1,66 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h
// 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
#pragma once
#include <Includes.h>
namespace MobileGL::MG_State::GLState {
class ProgramObject;
}
// The GL program interface (ARB_program_interface_query / GL 4.3 §7.3.1) as a frontend
// resource model.
//
// WHY IT IS HERE AND NOT IN A BACKEND. glGetProgramResource* describes the program the
// APPLICATION wrote, in the application's namespace. Neither backend program is in that
// namespace: DirectGLES compiles SPIRV-Cross-generated ESSL where default-block uniforms
// live inside the synthesized MGL_GLOBAL_UBO (so a GL_UNIFORM location query against it is
// structurally -1) and stage in/out names are rewritten; DirectVulkan has no GL-level
// reflection at all and can only re-derive a partial, diverging copy. The one authoritative
// source is the frontend glslang reflection a link already produced, which is the same
// place glGetActiveUniform answers from. This layer generalizes that rule to every
// interface, so the six entry points never consult gBackendFunctionsTable.
//
// NAMING RULES LIVE HERE, NOT IN ProgramObject. The interface query spells resources
// differently from glGetActiveUniform / glGetActiveAttrib (an array is "name[0]", a lookup
// accepts both "name" and "name[0]", a subscript must be a strict decimal). Those two
// getters are what GL30-33 exercises and they must not move, so every normalization is
// applied on the way in and out of THIS file.
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
using ProgramObject = MG_State::GLState::ProgramObject;
// <programInterface> is one of the GL 4.6 Table 7.1 interfaces.
Bool IsInterfaceEnum(GLenum programInterface);
// Interfaces whose resources have names (everything except GL_ATOMIC_COUNTER_BUFFER).
Bool IsNamedInterface(GLenum programInterface);
// <prop> is a property token GetProgramResourceiv knows at all (else GL_INVALID_ENUM).
Bool IsResourceProp(GLenum prop);
// <prop> applies to <programInterface> (else GL_INVALID_OPERATION).
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop);
// Interfaces GetProgramResourceLocation accepts (else GL_INVALID_ENUM).
Bool InterfaceHasLocations(GLenum programInterface);
// GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH / GL_MAX_NUM_ACTIVE_VARIABLES. All three
// report zero for an interface this implementation cannot enumerate and for a program
// that has not linked successfully - which is what the spec requires of a program with
// no active resources.
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface);
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface);
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface);
// GL_INVALID_INDEX when <name> names no active resource of the interface.
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name);
// False when <index> is out of range for the interface (the caller raises INVALID_VALUE).
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName);
// Appends the value(s) of <prop> for the resource; GL_ACTIVE_VARIABLES appends several.
// False when <index> is out of range.
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
Vector<GLint>& outValues);
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name);
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name);
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
@@ -1057,6 +1057,10 @@ namespace MobileGL::MG_State::GLState {
Bool ProgramLinkTask::ResolveTransformFeedbackVaryings() {
artifacts.xfbVaryings.clear();
// The GL_TRANSFORM_FEEDBACK_VARYING interface enumerates the request verbatim -
// pseudo-varyings included - while xfbVaryings below keeps only what is actually
// captured. Snapshot it before the loop consumes gl_NextBuffer/gl_SkipComponentsN.
artifacts.xfbInterfaceNames = in.requestedXfbVaryings;
artifacts.xfbStrides.clear();
artifacts.xfbBufferMode = in.requestedXfbBufferMode;
artifacts.xfbVaryingNameMaxLength = 0;
@@ -1127,15 +1131,45 @@ namespace MobileGL::MG_State::GLState {
bytesPerElement = 4;
resolved = true;
} else if (linkerObjects != nullptr) {
// GL lets a capture name a single element of an output array ("b[0]"), which
// captures one element of the element type - not the whole array. Strip a
// trailing strict-decimal subscript and look the base declaration up.
String declaredName = name;
Bool singleElement = false;
Uint element = 0;
if (name.size() > 3 && name.back() == ']') {
const SizeT bracket = name.rfind('[');
if (bracket != String::npos && bracket + 1 < name.size() - 1) {
Bool digitsOnly = true;
for (SizeT c = bracket + 1; c + 1 < name.size(); ++c) {
if (name[c] < '0' || name[c] > '9') {
digitsOnly = false;
break;
}
element = element * 10 + static_cast<Uint>(name[c] - '0');
}
if (digitsOnly) {
declaredName = name.substr(0, bracket);
singleElement = true;
}
}
}
for (const auto* node : linkerObjects->getSequence()) {
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
continue;
}
if (symbol->getName() != name.c_str()) {
if (symbol->getName() != declaredName.c_str()) {
continue;
}
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
if (resolved && singleElement) {
if (static_cast<Int>(element) >= varying.size) {
resolved = false;
break;
}
varying.size = 1;
}
break;
}
}
@@ -116,6 +116,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.explicitOpaqueUniformBindings.clear();
artifacts.uniformBlockIndexByName.clear();
artifacts.uniformBlockBinding.clear();
artifacts.shaderStorageBlockBinding.clear();
artifacts.uniformOffsets.clear();
artifacts.uniformSizesInBytes.clear();
artifacts.globalUboScratch.clear();
@@ -127,6 +128,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.attribInNameMaxLength = 0;
artifacts.uniformBlockNameMaxLength = 0;
artifacts.xfbVaryings.clear();
artifacts.xfbInterfaceNames.clear();
artifacts.xfbStrides.clear();
artifacts.xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
artifacts.xfbVaryingNameMaxLength = 0;
@@ -82,6 +82,14 @@ namespace MobileGL::MG_State::GLState {
return -1;
}
if (name.length() < 4) return -1;
// An array of arrays is keyed by its full "[0]"-terminated spelling
// ("a[2][1][0]"), so a query that already ends in a subscript may still be the
// NAME of an array rather than an element of one. Try that first; only then
// treat the trailing subscript as an element index.
{
const auto arrayOfArraysIt = Artifacts().uniformLocations.find(name + "[0]");
if (arrayOfArraysIt != Artifacts().uniformLocations.end()) return (Int)arrayOfArraysIt->second;
}
const SizeT bracket = name.rfind('[');
// Require at least one digit between the brackets.
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
@@ -130,6 +138,14 @@ namespace MobileGL::MG_State::GLState {
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
return Artifacts().tProgramUniformIndexToGl[tIndex];
}
// GL uniform-block index -> glslang TProgram block index (the inverse of
// GlBlockIndexFromTProgram). The interface-query layer needs it to reach block
// properties glslang exposes but no typed getter here does.
Int TProgramBlockIndex(Uint glBlockIndex) const {
return glBlockIndex < Artifacts().glBlockIndexToTProgram.size()
? Artifacts().glBlockIndexToTProgram[glBlockIndex]
: -1;
}
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(Artifacts().tProgramBlockIndexToGl.size())) return -1;
return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
@@ -529,9 +545,42 @@ namespace MobileGL::MG_State::GLState {
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
// Set by glShaderStorageBlockBinding, keyed by the block's GL name rather than by any
// index. A shader storage block has THREE index spaces - the frontend interface-query
// enumeration, DirectVulkan's SPIR-V descriptor order and DirectGLES's real-driver
// order - and the name is the only coordinate all three agree on. Absent from the map
// means "never rebound", and the shader's declared binding still stands.
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
}
// -1 when the block has never been rebound. `blockName` is the interface-query
// spelling; an arrayed block's elements ("B[0]", "B[1]") are separate GL resources
// with separate bindings, so they are separate keys.
Int GetShaderStorageBlockBindingOverride(const String& blockName) const {
const auto it = Artifacts().shaderStorageBlockBinding.find(blockName);
if (it != Artifacts().shaderStorageBlockBinding.end()) return it->second;
// A backend that collapses an arrayed block down to one resource knows it only by
// the bare block name; answer that with element zero's binding.
const auto zeroth = Artifacts().shaderStorageBlockBinding.find(blockName + "[0]");
return zeroth != Artifacts().shaderStorageBlockBinding.end() ? zeroth->second : -1;
}
// Every rebinding recorded so far, for a backend that has to REPLAY them onto a
// driver program it just (re)built. Empty for the overwhelming majority of programs -
// check .empty() before doing any per-block work.
const UnorderedMap<String, Int>& GetShaderStorageBlockBindingOverrides() const {
return Artifacts().shaderStorageBlockBinding;
}
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
// The linked glslang reflection itself, for the ONE consumer that needs resource
// lists no typed getter above exposes: the GL program-interface query layer
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
// blocks, buffer variables, atomic counters and per-stage reference masks. Null
// until a link has succeeded. Read through the join gate like everything else.
const glslang::TProgram* GetReflection() const { return Artifacts().program.get(); }
Int GetShaderIndexByStage(ShaderStage stage) const {
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
return shader->GetShaderStage() == stage;
@@ -611,6 +660,9 @@ namespace MobileGL::MG_State::GLState {
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
UnorderedMap<String, Uint> uniformBlockIndexByName;
Vector<Int> uniformBlockBinding;
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
// SetShaderStorageBlockBinding for why this one is by name and not by index.
UnorderedMap<String, Int> shaderStorageBlockBinding;
// Need to be reflected after linking of SPIR-V binary
Vector<Uint> uniformOffsets;
@@ -629,6 +681,12 @@ namespace MobileGL::MG_State::GLState {
// Transform feedback: the linked snapshot (the request lives outside, on the
// GL-thread-owned side).
Vector<XfbVarying> xfbVaryings;
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
// xfbVaryings deliberately drops (they steer the capture layout and must never
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
// full request, pseudo-varyings and all, so the interface query needs its own copy.
Vector<String> xfbInterfaceNames;
Vector<Uint32> xfbStrides;
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
@@ -711,6 +769,9 @@ namespace MobileGL::MG_State::GLState {
return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr;
}
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return Artifacts().xfbVaryings; }
// The GL_TRANSFORM_FEEDBACK_VARYING resource list: every name the last successful
// link was asked to capture, in request order, pseudo-varyings included.
const Vector<String>& GetTransformFeedbackInterfaceNames() const { return Artifacts().xfbInterfaceNames; }
// Stride of one captured vertex in the given capture buffer slot.
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
return bufferIndex < Artifacts().xfbStrides.size() ? Artifacts().xfbStrides[bufferIndex] : 0;
+17
View File
@@ -130,6 +130,22 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_executable(
ProgramInterfaceTest
ProgramInterfaceTest.cpp
)
target_include_directories(ProgramInterfaceTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ProgramInterfaceTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
target_include_directories(ProgramTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
@@ -144,6 +160,7 @@ target_link_libraries(
include(GoogleTest)
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
# compile pool so there is something in flight to race against.
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
File diff suppressed because it is too large Load Diff