diff --git a/CMakeLists.txt b/CMakeLists.txt index c0d0c9f9..3ff0e1a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 9d62a6c5..616cdd06 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index abf2294d..4e3f7e3b 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e3804368..3b3e16d6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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(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(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index ee1115b0..d594a52a 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -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(); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 8ae52826..00f4a73f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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(binding)); + } + } + void BackendProgramObjectImpl::SyncToBackend( const SharedPtr& stateProgramObject) { #ifdef TRACY_ENABLE @@ -3846,9 +3874,16 @@ namespace MobileGL::MG_Backend::DirectGLES { if (attachedCount > 0) { Vector 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(actualCount, 0, static_cast(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 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 log(static_cast(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 log(logLength); + if (logLength < 0) logLength = 0; + Vector log(static_cast(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; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 6e75950f..a940f08f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -981,6 +981,22 @@ namespace MobileGL::MG_Backend::DirectGLES { extern Uint g_lastUsedBackendProgramId; extern StateBackendObjectRegistry 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 { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index b2fe4d70..c079678d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 91f549cd..45c8a899 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -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(rebound); block.dataSize = static_cast(binding->block.size); const GLuint blockIndex = static_cast(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(source.size()); - if (length) { - *length = writtenLength; - } - if (name && bufSize > 0) { - const GLsizei copyLength = std::min(bufSize - 1, writtenLength); - std::memcpy(name, source.data(), static_cast(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(indirect); } - Vector GetUniformBlockActiveVariables(const MG_State::GLState::ProgramObject& program, - GLuint blockIndex) { - Vector activeVariables; - const Uint uniformCount = program.GetUniformCount(); - activeVariables.reserve(uniformCount); - for (Uint uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) { - if (program.GetActiveUniformBlockIndex(uniformIndex) == static_cast(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(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(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(cache.storageBlocks.size()); - case GL_BUFFER_VARIABLE: - return static_cast(cache.bufferVariables.size()); - case GL_UNIFORM_BLOCK: - return program.GetActiveUniformBlocksCount(); - case GL_UNIFORM: - return static_cast(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(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(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(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(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(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(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(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(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(programObject->GetActiveAttributesCount())) { - CopyResourceName(programObject->GetActiveAttribName(index), bufSize, length, name); - return; - } - if (programInterface == GL_PROGRAM_OUTPUT && - index < static_cast(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(block.name.size() + 1)); - break; - case GL_BUFFER_BINDING: - writeValue(static_cast(block.binding)); - break; - case GL_BUFFER_DATA_SIZE: - writeValue(block.dataSize); - break; - case GL_NUM_ACTIVE_VARIABLES: - writeValue(static_cast(block.activeVariables.size())); - break; - case GL_ACTIVE_VARIABLES: - for (const auto variable : block.activeVariables) writeValue(static_cast(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(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(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(programObject->GetUniformBlockName(index).size() + 1)); - break; - case GL_BUFFER_BINDING: - writeValue(static_cast(programObject->GetUniformBlockBinding(index))); - break; - case GL_BUFFER_DATA_SIZE: - writeValue(static_cast(programObject->GetUBOSizeAt(index))); - break; - case GL_NUM_ACTIVE_VARIABLES: - writeValue(static_cast(activeVariables.size())); - break; - case GL_ACTIVE_VARIABLES: - for (const GLuint variableIndex : activeVariables) { - writeValue(static_cast(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(uniformName.size() + 1)); - break; - case GL_TYPE: - writeValue(static_cast(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(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(programObject->GetActiveAttributesCount())) { - const auto& resourceName = programObject->GetActiveAttribName(index); - switch (prop) { - case GL_NAME_LENGTH: - writeValue(static_cast(resourceName.size() + 1)); - break; - case GL_TYPE: - writeValue(static_cast(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(programObject->GetActiveFragmentOutputCount())) { - const auto& resourceName = programObject->GetActiveFragmentOutputName(index); - switch (prop) { - case GL_NAME_LENGTH: - writeValue(static_cast(resourceName.size() + 1)); - break; - case GL_TYPE: - writeValue(static_cast(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("DirectVulkan", __func__, "Shader storage binding is out of range.")); return; } - if (storageBlockIndex >= cache.storageBlocks.size()) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique("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"); diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index 9ccc7eaa..2f992399 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -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& texture, TextureUploadTarget uploadTarget, diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 121da8da..c52e152a 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "GL_Program.h" +#include "ProgramInterface.h" #include "Config.h" #include #include @@ -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& TryToGetProgramForInterfaceQuery(GLuint program, + const char* caller) { + static const SharedPtr 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("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("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("MG_Impl/GLImpl", caller, @@ -190,66 +201,6 @@ namespace MobileGL::MG_Impl::GLImpl { return true; } - static Int GetKnownProgramResourceCount(const SharedPtr& 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& programObject, Uint index, - GLenum prop, GLint* out) { - switch (prop) { - case GL_TYPE: - *out = static_cast(programObject->GetActiveUniformType(index)); - return true; - case GL_ARRAY_SIZE: - *out = programObject->GetActiveUniformArraySize(index); - return true; - case GL_NAME_LENGTH: - *out = static_cast(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("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(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(uniformIndex); - } - auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex; - if (!getProgramResourceIndex) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("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(resourceCount)) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", __func__, "index is out of range.")); - return; - } if (bufSize < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("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(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("MG_Impl/GLImpl", __func__, - "Backend does not support program interface queries.")); + ErrorCode::InvalidValue, + MakeUnique("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(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("MG_Impl/GLImpl", __func__, - "propCount and bufSize must be non-negative.")); + ErrorCode::InvalidEnum, + MakeUnique("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("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("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("MG_Impl/GLImpl", __func__, + "prop is not supported for this program interface.")); + return; + } + } + + Vector values; + for (GLsizei i = 0; i < propCount; ++i) { + if (!ProgramInterface::GetResourceProp(*programObject, programInterface, index, props[i], values)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("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("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(std::min(values.size(), static_cast(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("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("MG_Impl/GLImpl", __func__, + "GetProgramResourceLocationIndex only accepts GL_PROGRAM_OUTPUT.")); + return -1; + } + return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name); } + // GL 4.6 ยง7.6.2: is an active shader storage block index of + // - 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("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) { diff --git a/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp new file mode 100644 index 00000000..274e3962 --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp @@ -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 +#include + +#include + +namespace MobileGL::MG_Impl::GLImpl::ProgramInterface { + namespace { + // glslang folds atomic counters into synthesized blocks named + // "_" (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_ + 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 activeVariables; + }; + + using ResourceList = Vector; + + 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(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(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(glslang::TQualifier::layoutLocationEnd)) return -1; + return rawLocation; + } + + // ---- model construction -------------------------------------------------------- + + void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model, + Vector& blockKind, Vector& blockInterfaceIndex) { + const Int blockCount = const_cast(reflection).getNumUniformBlocks(); + blockKind.assign(blockCount, BlockKind::Uniform); + blockInterfaceIndex.assign(blockCount, -1); + + for (Int tIndex = 0; tIndex < blockCount; ++tIndex) { + const auto& block = const_cast(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(block.stages); + blockInterfaceIndex[tIndex] = static_cast(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(rebound); + resource.bufferDataSize = block.size; + resource.stages = static_cast(block.stages); + blockInterfaceIndex[tIndex] = static_cast(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(program.GetUniformBlockBinding(glIndex)); + resource.bufferDataSize = static_cast(program.GetUBOSizeAt(glIndex)); + const Int tIndex = program.TProgramBlockIndex(static_cast(glIndex)); + if (tIndex >= 0 && tIndex < blockCount) { + resource.stages = + static_cast(const_cast(reflection).getUniformBlock(tIndex).stages); + } + model.uniformBlocks.push_back(Move(resource)); + } + } + + void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model, + const Vector& blockKind, + const Vector& blockInterfaceIndex) { + const Uint uniformCount = program.GetUniformCount(); + for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) { + const Int tIndex = program.TProgramUniformIndex(glIndex); + const auto& refl = const_cast(reflection).getUniform(tIndex); + const glslang::TType* type = refl.getType(); + const Int owner = refl.index; + const BlockKind kind = (owner >= 0 && owner < static_cast(blockKind.size())) + ? blockKind[owner] + : BlockKind::GlobalUbo; + + Resource resource; + resource.name = refl.name; + resource.type = static_cast(refl.glDefineType); + resource.arraySize = ArraySizeOf(type, refl.size); + resource.stages = static_cast(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(model.atomicCounterBuffers.size())) { + model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back( + static_cast(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(program.GetUniformBlockMemberOwnerIndex(static_cast(blockIndex))); + for (SizeT i = 0; i < model.uniforms.size(); ++i) { + if (model.uniforms[i].blockIndex == owner) { + model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast(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(blockIndex)) { + model.storageBlocks[blockIndex].activeVariables.push_back(static_cast(i)); + } + } + } + } + + void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) { + auto& mutableReflection = const_cast(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(refl.glDefineType); + resource.arraySize = ArraySizeOf(type, refl.size); + resource.location = program.GetAttributeLocation(refl.name); + if (resource.location < 0) resource.location = MappedLocation(static_cast(refl.layoutLocation())); + resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; + resource.stages = static_cast(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(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(type->getQualifier().layoutIndex); + } + } + resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; + resource.stages = static_cast(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(varying.offsetBytes); + resource.xfbBufferIndex = static_cast(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; + Vector 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(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(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(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(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& 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(1u << stage)) != 0 ? GL_TRUE : GL_FALSE; + }; + + switch (prop) { + case GL_NAME_LENGTH: + outValues.push_back(static_cast(resource.name.length() + 1)); + break; + case GL_TYPE: + outValues.push_back(static_cast(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(resource.activeVariables.size())); + break; + case GL_ACTIVE_VARIABLES: + for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast(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(element) >= resource.arraySize) return -1; + return resource.location + static_cast(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(element) >= resource.arraySize) return -1; + return resource.locationIndex; + } + return -1; + } +} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface diff --git a/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h new file mode 100644 index 00000000..30005266 --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h @@ -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 + +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; + + // 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); + // is a property token GetProgramResourceiv knows at all (else GL_INVALID_ENUM). + Bool IsResourceProp(GLenum prop); + // applies to (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 names no active resource of the interface. + GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name); + // False when 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 for the resource; GL_ACTIVE_VARIABLES appends several. + // False when is out of range. + Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop, + Vector& 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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 2d9eda53..28aac99e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -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(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(element) >= varying.size) { + resolved = false; + break; + } + varying.size = 1; + } break; } } diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 1c76129a..0e9752ad 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 1c8db92e..9540e55e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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(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(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(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& GetShaderStorageBlockBindingOverrides() const { + return Artifacts().shaderStorageBlockBinding; + } + Vector>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; } const Vector>& 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& 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 uniformBlockIndexByName; Vector uniformBlockBinding; + // glShaderStorageBlockBinding overrides, keyed by GL block name. See + // SetShaderStorageBlockBinding for why this one is by name and not by index. + UnorderedMap shaderStorageBlockBinding; // Need to be reflected after linking of SPIR-V binary Vector 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 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 xfbInterfaceNames; Vector xfbStrides; Vector gsStripTriangles; Bool gsStripCaptureFixup = false; @@ -711,6 +769,9 @@ namespace MobileGL::MG_State::GLState { return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr; } const Vector& 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& 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; diff --git a/MobileGL/MG_Test/Program/CMakeLists.txt b/MobileGL/MG_Test/Program/CMakeLists.txt index c2748fb6..ddbc4daa 100644 --- a/MobileGL/MG_Test/Program/CMakeLists.txt +++ b/MobileGL/MG_Test/Program/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp new file mode 100644 index 00000000..b9fa7d3b --- /dev/null +++ b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp @@ -0,0 +1,1103 @@ +// MobileGL - MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) answered +// entirely from the frontend glslang reflection - MG_Impl/GLImpl/Program/ProgramInterface. +// +// GPU-free on purpose: every expectation below is a property of the LINKED PROGRAM, not of +// any driver, which is the whole point of the layer. The shaders and the expected values +// are lifted from KHR-GL4x.program_interface_query so a failure here is the same failure +// the conformance suite would report, minutes earlier and without a GPU. + +#include + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include "MG_Impl/GLImpl/Getter/GL_Getter.h" +#include "MG_Impl/GLImpl/Program/GL_Program.h" +#include "MG_State/GLState/Core.h" + +using namespace MobileGL; +using namespace MobileGL::MG_Impl::GLImpl; + +namespace { + class ProgramInterfaceTest : public ::testing::Test { + protected: + void SetUp() override { MobileGL::Initialize(); } + }; + + GLuint MakeProgram(const char* vs, const char* fs, const char* cs = nullptr) { + const GLuint p = CreateProgram(); + const auto attach = [p](GLenum stage, const char* source) { + if (!source) return; + const GLuint sh = CreateShader(stage); + ShaderSource(sh, 1, &source, nullptr); + CompileShader(sh); + AttachShader(p, sh); + }; + attach(GL_VERTEX_SHADER, vs); + attach(GL_FRAGMENT_SHADER, fs); + attach(GL_COMPUTE_SHADER, cs); + return p; + } + + void ExpectLinked(GLuint program) { + GLint status = 0; + GetProgramiv(program, GL_LINK_STATUS, &status); + if (status == GL_TRUE) return; + char log[4096] = ""; + GetProgramInfoLog(program, sizeof(log), nullptr, log); + FAIL() << "link failed: " << log; + } + + GLint Interfaceiv(GLuint program, GLenum iface, GLenum pname) { + GLint value = -12345; + GetProgramInterfaceiv(program, iface, pname, &value); + return value; + } + + std::string ResourceName(GLuint program, GLenum iface, GLuint index) { + GLchar buffer[1024] = {'\0'}; + GLsizei length = 0; + GetProgramResourceName(program, iface, index, sizeof(buffer), &length, buffer); + EXPECT_GE(length, 0); + EXPECT_EQ(buffer[length], '\0') << "length must not count the terminator"; + return std::string(buffer); + } + + // Resolves by name and immediately checks that the index round-trips back to the + // expected spelling, which is how the CTS uses these two together. + void ExpectResource(GLuint program, GLenum iface, const char* queryName, const char* enumeratedName) { + const GLuint index = GetProgramResourceIndex(program, iface, queryName); + ASSERT_NE(index, GL_INVALID_INDEX) << "no resource named '" << queryName << "'"; + EXPECT_EQ(ResourceName(program, iface, index), enumeratedName) << "for query '" << queryName << "'"; + } + + std::vector Props(GLuint program, GLenum iface, GLuint index, const std::vector& props) { + std::vector params(256, -12345); + GLsizei length = 0; + GetProgramResourceiv(program, iface, index, static_cast(props.size()), props.data(), + static_cast(params.size()), &length, params.data()); + params.resize(length < 0 ? 0 : static_cast(length)); + return params; + } + + std::vector PropsOf(GLuint program, GLenum iface, const char* name, const std::vector& props) { + const GLuint index = GetProgramResourceIndex(program, iface, name); + EXPECT_NE(index, GL_INVALID_INDEX) << "no resource named '" << name << "'"; + if (index == GL_INVALID_INDEX) return {}; + return Props(program, iface, index, props); + } + + GLenum TakeError() { return GetError(); } + void ClearErrors() { + for (int i = 0; i < 32 && TakeError() != GL_NO_ERROR; ++i) { + } + } + + const char* kSimpleVs = R"(#version 430 +in vec4 position; +void main(void) { gl_Position = position; } +)"; + const char* kSimpleFs = R"(#version 430 +out vec4 color; +void main() { color = vec4(0, 1, 0, 1); } +)"; + + // ---------------------------------------------------------------- simple-shaders ---- + TEST_F(ProgramInterfaceTest, SimpleShaders) { + const GLuint p = MakeProgram(kSimpleVs, kSimpleFs); + BindAttribLocation(p, 0, "position"); + BindFragDataLocation(p, 0, "color"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + // KNOWN GAP, not an expectation: a separable FRAGMENT program's own inputs are not + // in the reflection at all - glslang builds the "pipe input" list from the vertex + // stage unless EShReflectionIntermediateIO is set, and setting that makes a + // vertex-only separable program report its VS outputs as fragment outputs, which + // fails ValidateFragmentOutputLocations and breaks glCreateShaderProgramv. So + // GL_PROGRAM_INPUT is empty here until that validation is stage-aware. + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 6); + + EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "color"), 0u); + EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_INPUT, "position"), 0u); + EXPECT_EQ(ResourceName(p, GL_PROGRAM_OUTPUT, 0), "color"); + EXPECT_EQ(ResourceName(p, GL_PROGRAM_INPUT, 0), "position"); + + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "position"), 0); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "color"), 0); + EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "color"), 0); + + const std::vector inProps = {GL_NAME_LENGTH, + GL_TYPE, + GL_ARRAY_SIZE, + GL_REFERENCED_BY_COMPUTE_SHADER, + GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_GEOMETRY_SHADER, + GL_REFERENCED_BY_TESS_CONTROL_SHADER, + GL_REFERENCED_BY_TESS_EVALUATION_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER, + GL_LOCATION, + GL_IS_PER_PATCH}; + EXPECT_EQ(Props(p, GL_PROGRAM_INPUT, 0, inProps), + (std::vector{9, GL_FLOAT_VEC4, 1, 0, 0, 0, 0, 0, 1, 0, 0})); + + std::vector outProps = inProps; + outProps.push_back(GL_LOCATION_INDEX); + EXPECT_EQ(Props(p, GL_PROGRAM_OUTPUT, 0, outProps), + (std::vector{6, GL_FLOAT_VEC4, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------------- input-types ---- + TEST_F(ProgramInterfaceTest, InputTypesNamesLocationsAndProps) { + const char* vs = R"(#version 430 +in mat4 a; +in ivec4 b; +in float c[2]; +in mat2x3 d[2]; +in uvec2 e; +in uint f; +in vec3 g[2]; +in int h; +void main(void) +{ + vec4 pos; + pos.w = h + g[0].x + g[1].y + d[1][1].y; + pos.y = b.x * c[0] + c[1] + d[0][0].x; + pos.x = a[0].x + a[1].y + a[2].z + a[3].w; + pos.z = d[0][1].z + e.x * f + d[1][0].z; + gl_Position = pos; +} +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + BindAttribLocation(p, 0, "a"); + BindAttribLocation(p, 4, "b"); + BindAttribLocation(p, 5, "c"); + BindAttribLocation(p, 7, "d"); + BindAttribLocation(p, 11, "e"); + BindAttribLocation(p, 12, "f"); + BindAttribLocation(p, 13, "g"); + BindAttribLocation(p, 15, "h"); + BindFragDataLocation(p, 0, "color"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 8); + // "c[0]" and friends: an array input is enumerated with the [0] subscript, which is + // what makes MAX_NAME_LENGTH 5 rather than 2. + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 5); + + ExpectResource(p, GL_PROGRAM_INPUT, "a", "a"); + ExpectResource(p, GL_PROGRAM_INPUT, "c[0]", "c[0]"); + ExpectResource(p, GL_PROGRAM_INPUT, "c", "c[0]"); + ExpectResource(p, GL_PROGRAM_INPUT, "d", "d[0]"); + ExpectResource(p, GL_PROGRAM_INPUT, "g", "g[0]"); + ExpectResource(p, GL_PROGRAM_INPUT, "h", "h"); + + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "a"), 0); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "b"), 4); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[0]"), 5); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c"), 5); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[1]"), 6); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "d[0]"), 7); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "g[1]"), 14); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "h"), 15); + // Out of range, and a subscript that is not a strict decimal. + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[2]"), -1); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[01]"), -1); + + const std::vector props = {GL_NAME_LENGTH, + GL_TYPE, + GL_ARRAY_SIZE, + GL_REFERENCED_BY_COMPUTE_SHADER, + GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER, + GL_LOCATION, + GL_IS_PER_PATCH}; + EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "a", props), + (std::vector{2, GL_FLOAT_MAT4, 1, 0, 0, 1, 0, 0})); + EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "c[0]", props), + (std::vector{5, GL_FLOAT, 2, 0, 0, 1, 5, 0})); + EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "d", props), + (std::vector{5, GL_FLOAT_MAT2x3, 2, 0, 0, 1, 7, 0})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------------ output-types ---- + TEST_F(ProgramInterfaceTest, OutputTypesAndLocationIndex) { + const char* fs = R"(#version 430 +out vec3 a[2]; +out uint b; +out float c[2]; +out int d[2]; +out vec2 e; +void main() { + c[1] = -0.6; d[0] = 0; b = 12u; c[0] = 1.1; e = vec2(0, 1); d[1] = -19; + a[1] = vec3(0, 1, 0); a[0] = vec3(0, 1, 0); +} +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + BindAttribLocation(p, 0, "position"); + BindFragDataLocation(p, 0, "a"); + BindFragDataLocation(p, 2, "b"); + BindFragDataLocation(p, 3, "c"); + BindFragDataLocation(p, 5, "d"); + BindFragDataLocation(p, 7, "e"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 5); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 5); + ExpectResource(p, GL_PROGRAM_OUTPUT, "a", "a[0]"); + ExpectResource(p, GL_PROGRAM_OUTPUT, "c[0]", "c[0]"); + ExpectResource(p, GL_PROGRAM_OUTPUT, "e", "e"); + + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a[0]"), 0); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a"), 0); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a[1]"), 1); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "b"), 2); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "c[1]"), 4); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "d[1]"), 6); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "e"), 7); + for (const char* name : {"a[0]", "a", "b", "c[0]", "c", "d[0]", "d", "e"}) { + EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, name), 0) << name; + } + + const std::vector props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_LOCATION, GL_IS_PER_PATCH, GL_LOCATION_INDEX}; + EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "a", props), + (std::vector{5, GL_FLOAT_VEC3, 2, 1, 0, 0, 0})); + EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "d", props), (std::vector{5, GL_INT, 2, 1, 5, 0, 0})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // --------------------------------------------------------------- output-built-in ---- + TEST_F(ProgramInterfaceTest, OutputBuiltInsHaveNoLocation) { + const char* fs = R"(#version 430 +void main(void) { gl_FragDepth = 0.1; gl_SampleMask[0] = 1; } +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 2); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 17); + ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_FragDepth", "gl_FragDepth"); + ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]", "gl_SampleMask[0]"); + + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "gl_FragDepth"), -1); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "gl_SampleMask"), -1); + EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "gl_FragDepth"), -1); + EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]"), -1); + + const std::vector props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_LOCATION, GL_LOCATION_INDEX}; + EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_FragDepth", props), + (std::vector{13, GL_FLOAT, 1, 1, -1, -1})); + EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]", props), + (std::vector{17, GL_INT, 1, 1, -1, -1})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ---------------------------------------------------------------- input-built-in ---- + TEST_F(ProgramInterfaceTest, InputBuiltInsUseGlSpellings) { + const char* vs = R"(#version 430 +void main(void) { gl_Position = (gl_VertexID + gl_InstanceID) * vec4(0.1); } +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + BindFragDataLocation(p, 0, "color"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 2); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 14); + // The Vulkan-semantics parse calls these gl_VertexIndex / gl_InstanceIndex. + ExpectResource(p, GL_PROGRAM_INPUT, "gl_VertexID", "gl_VertexID"); + ExpectResource(p, GL_PROGRAM_INPUT, "gl_InstanceID", "gl_InstanceID"); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "gl_VertexID"), -1); + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "gl_InstanceID"), -1); + + const std::vector props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_VERTEX_SHADER, + GL_LOCATION}; + EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "gl_VertexID", props), (std::vector{12, GL_INT, 1, 1, -1})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------------ uniform-simple -- + TEST_F(ProgramInterfaceTest, UniformSimple) { + const char* vs = R"(#version 430 +in vec4 position; +uniform vec4 repos; +void main(void) { gl_Position = position + repos; } +)"; + const char* fs = R"(#version 430 +uniform vec4 recolor; +out vec4 color; +void main() { color = vec4(0, 1, 0, 1) + recolor; } +)"; + const GLuint p = MakeProgram(vs, fs); + BindAttribLocation(p, 0, "position"); + BindFragDataLocation(p, 0, "color"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + GLint activeUniforms = 0; + GetProgramiv(p, GL_ACTIVE_UNIFORMS, &activeUniforms); + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_ACTIVE_RESOURCES), activeUniforms); + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 8); + ExpectResource(p, GL_UNIFORM, "repos", "repos"); + ExpectResource(p, GL_UNIFORM, "recolor", "recolor"); + + // The sharpest single symptom of the old backend-forwarding design: these two had to + // agree and did not, because the ESSL program keeps "repos" inside MGL_GLOBAL_UBO. + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "repos"), GetUniformLocation(p, "repos")); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "recolor"), GetUniformLocation(p, "recolor")); + + const std::vector props = {GL_NAME_LENGTH, + GL_TYPE, + GL_ARRAY_SIZE, + GL_OFFSET, + GL_BLOCK_INDEX, + GL_ARRAY_STRIDE, + GL_MATRIX_STRIDE, + GL_IS_ROW_MAJOR, + GL_ATOMIC_COUNTER_BUFFER_INDEX, + GL_REFERENCED_BY_COMPUTE_SHADER, + GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER, + GL_LOCATION}; + EXPECT_EQ(PropsOf(p, GL_UNIFORM, "repos", props), + (std::vector{6, GL_FLOAT_VEC4, 1, -1, -1, -1, -1, 0, -1, 0, 0, 1, + GetUniformLocation(p, "repos")})); + EXPECT_EQ(PropsOf(p, GL_UNIFORM, "recolor", props), + (std::vector{8, GL_FLOAT_VEC4, 1, -1, -1, -1, -1, 0, -1, 0, 1, 0, + GetUniformLocation(p, "recolor")})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------------- array-names ---- + TEST_F(ProgramInterfaceTest, ArrayNameSubscriptsAreStrictDecimals) { + const char* vs = R"(#version 430 +in vec4 position; +uniform vec4 a[2]; +void main(void) { gl_Position = position + a[0] + a[1]; } +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + BindAttribLocation(p, 0, "position"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a"), GetUniformLocation(p, "a")); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a[0]"), GetUniformLocation(p, "a")); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a[1]"), GetUniformLocation(p, "a[1]")); + for (const char* bad : {"a[2]", "a[0 + 0]", "a[0+0]", "a[ 0]", "a[0 ]", "a[\n0]", "a[\t0]", "a[01]", "a[00]"}) { + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, bad), -1) << "for '" << bad << "'"; + } + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // -------------------------------------------------------------- arrays-of-arrays ---- + TEST_F(ProgramInterfaceTest, ArraysOfArrays) { + const char* vs = R"(#version 430 +in vec4 position; +uniform vec4 a[3][4][5]; +void main(void) { + gl_Position = position; + for (int i = 0; i < 5; ++i) gl_Position += a[2][1][i]; +} +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 11); + ExpectResource(p, GL_UNIFORM, "a[2][1]", "a[2][1][0]"); + EXPECT_EQ(GetProgramResourceIndex(p, GL_UNIFORM, "a[2][1][0]"), + GetProgramResourceIndex(p, GL_UNIFORM, "a[2][1]")); + + const std::vector props = {GL_NAME_LENGTH, + GL_TYPE, + GL_ARRAY_SIZE, + GL_OFFSET, + GL_BLOCK_INDEX, + GL_ARRAY_STRIDE, + GL_MATRIX_STRIDE, + GL_IS_ROW_MAJOR, + GL_ATOMIC_COUNTER_BUFFER_INDEX, + GL_REFERENCED_BY_VERTEX_SHADER, + GL_LOCATION}; + EXPECT_EQ(PropsOf(p, GL_UNIFORM, "a[2][1]", props), + (std::vector{11, GL_FLOAT_VEC4, 5, -1, -1, -1, -1, 0, -1, 1, + GetUniformLocation(p, "a[2][1]")})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------- uniform-block-types -- + TEST_F(ProgramInterfaceTest, UniformBlocks) { + const char* vs = R"(#version 430 +in vec4 position; +uniform SimpleBlock { mat3x2 a; mat4 b; vec4 c; }; +uniform NotSoSimpleBlockk { ivec2 a[4]; mat3 b[2]; mat2 c; } d; +void main(void) { + float tmp = a[0][1] * b[1][2] * c.x; + tmp = tmp + d.a[2].y + d.b[0][1][1] + d.c[1][1]; + gl_Position = position * tmp; +} +)"; + const char* fs = R"(#version 430 +struct U { bool a[3]; vec4 b; mat3 c; float d[2]; }; +struct UU { U a; U b[2]; uvec2 c; }; +uniform TrickyBlock { UU a[3]; mat4 b; uint c; } e[2]; +out vec4 color; +void main() { color = vec4(0, 1, 0, 1) * e[0].a[2].b[0].d[1]; } +)"; + const GLuint p = MakeProgram(vs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 4); + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NAME_LENGTH), 18); + ExpectResource(p, GL_UNIFORM_BLOCK, "SimpleBlock", "SimpleBlock"); + ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock", "TrickyBlock[0]"); + ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", "TrickyBlock[1]"); + ExpectResource(p, GL_UNIFORM, "NotSoSimpleBlockk.a[0]", "NotSoSimpleBlockk.a[0]"); + ExpectResource(p, GL_UNIFORM, "TrickyBlock.a[2].b[0].d", "TrickyBlock.a[2].b[0].d[0]"); + + const GLuint simple = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "SimpleBlock"); + const GLuint tricky = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "TrickyBlock"); + // The index the interface hands out must be usable with glUniformBlockBinding. + UniformBlockBinding(p, simple, 0); + UniformBlockBinding(p, tricky, 3); + GLint dataSize = 0; + GetActiveUniformBlockiv(p, simple, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize); + EXPECT_EQ(Props(p, GL_UNIFORM_BLOCK, simple, + {GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}), + (std::vector{12, 0, 0, 1, dataSize, 3})); + EXPECT_EQ(Props(p, GL_UNIFORM_BLOCK, tricky, + {GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER}), + (std::vector{15, 3, 1, 0})); + + // A block member reports its block, no location, and no atomic-counter buffer. + EXPECT_EQ(PropsOf(p, GL_UNIFORM, "a", + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR, + GL_ATOMIC_COUNTER_BUFFER_INDEX, GL_REFERENCED_BY_VERTEX_SHADER, GL_LOCATION}), + (std::vector{2, GL_FLOAT_MAT3x2, 1, static_cast(simple), 0, 0, -1, 1, -1})); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a"), -1); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "b"), -1); + + // GL_ACTIVE_VARIABLES lists exactly the three members, in GL_UNIFORM index space. + const std::vector activeVariables = Props(p, GL_UNIFORM_BLOCK, simple, {GL_ACTIVE_VARIABLES}); + ASSERT_EQ(activeVariables.size(), 3u); + for (const GLint variable : activeVariables) { + EXPECT_EQ(Props(p, GL_UNIFORM, static_cast(variable), {GL_BLOCK_INDEX}), + (std::vector{static_cast(simple)})); + } + EXPECT_GE(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 3); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ----------------------------------------------------------------- uniform-block-array + TEST_F(ProgramInterfaceTest, UniformBlockArrayMemberReportsItsBlock) { + const char* fs = R"(#version 430 +uniform TestBlock { mediump vec4 color; } blockInstance[4]; +out mediump vec4 color; +void main() { color = blockInstance[2].color + blockInstance[3].color; } +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + const GLuint block = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "TestBlock"); + ASSERT_NE(block, GL_INVALID_INDEX); + EXPECT_EQ(PropsOf(p, GL_UNIFORM, "TestBlock.color", {GL_BLOCK_INDEX}), + (std::vector{static_cast(block)})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ---------------------------------------------------------------------- ssb-types ---- + TEST_F(ProgramInterfaceTest, ShaderStorageBlocksAndBufferVariables) { + const char* fs = R"(#version 430 +struct U { bool a[3]; mediump vec4 b; mediump mat3 c; mediump float d[2]; }; +struct UU { U a; U b[2]; uvec2 c; }; +layout(binding=4) buffer TrickyBuffer { UU a[3]; mediump mat4 b; uint c; } e[2]; +layout(binding = 0) buffer SimpleBuffer { mediump mat3x2 a; mediump mat4 b; mediump vec4 c; }; +layout(binding = 1) buffer NotSoSimpleBuffer { ivec2 a[4]; mediump mat3 b[2]; mediump mat2 c; } d; +out mediump vec4 color; +void main() { + mediump float tmp = e[0].a[0].b[0].d[0] * float(e[1].c); + mediump float tmp2 = a[0][0] * b[0][0] * c.x; + tmp2 = tmp2 + float(d.a[0].y) + d.b[0][0][0] + d.c[0][0]; + color = vec4(0, 1, 0, 1) * tmp * tmp2; +} +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 4); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 18); + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 28); + EXPECT_GE(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 7); + EXPECT_GE(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 3); + + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer", "SimpleBuffer"); + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer", "TrickyBuffer[0]"); + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer[1]", "TrickyBuffer[1]"); + // A member of a block with no instance name keeps its bare name. + ExpectResource(p, GL_BUFFER_VARIABLE, "a", "a"); + ExpectResource(p, GL_BUFFER_VARIABLE, "NotSoSimpleBuffer.a[0]", "NotSoSimpleBuffer.a[0]"); + ExpectResource(p, GL_BUFFER_VARIABLE, "TrickyBuffer.a[0].b[0].d", "TrickyBuffer.a[0].b[0].d[0]"); + + const GLuint simple = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer"); + const GLuint tricky = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer"); + const GLuint tricky1 = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer[1]"); + EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, simple, + {GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_FRAGMENT_SHADER, + GL_REFERENCED_BY_VERTEX_SHADER}), + (std::vector{13, 0, 3, 1, 0})); + // An arrayed block gives element k the binding base + k. + EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, tricky, {GL_NAME_LENGTH, GL_BUFFER_BINDING}), + (std::vector{16, 4})); + EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, tricky1, {GL_NAME_LENGTH, GL_BUFFER_BINDING}), + (std::vector{16, 5})); + + EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "a", + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE, GL_TOP_LEVEL_ARRAY_STRIDE}), + (std::vector{2, GL_FLOAT_MAT3x2, 1, static_cast(simple), 0, 0, 1, 1, 0})); + EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "TrickyBuffer.a[0].b[0].d", + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_MATRIX_STRIDE, GL_IS_ROW_MAJOR, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}), + (std::vector{28, GL_FLOAT, 2, static_cast(tricky), 0, 0, 1, 3})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // --------------------------------------------------------------- top-level-array ---- + TEST_F(ProgramInterfaceTest, TopLevelArray) { + const char* fs = R"(#version 430 +buffer Block { vec4 a[5][4][3]; }; +out vec4 color; +void main() { color = vec4(0, 1, 0, 1) + a[0][0][0]; } +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 11); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 6); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1); + ExpectResource(p, GL_BUFFER_VARIABLE, "a[0][0]", "a[0][0][0]"); + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "Block", "Block"); + + const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "Block"); + EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "a[0][0]", + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_IS_ROW_MAJOR, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}), + (std::vector{11, GL_FLOAT_VEC4, 3, static_cast(block), 0, 1, 5})); + const std::vector stride = PropsOf(p, GL_BUFFER_VARIABLE, "a[0][0]", {GL_TOP_LEVEL_ARRAY_STRIDE}); + ASSERT_EQ(stride.size(), 1u); + EXPECT_GT(stride[0], 0); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ---------------------------------------------------------------- compute-shader ---- + TEST_F(ProgramInterfaceTest, ComputeRuntimeSizedBufferVariable) { + const char* cs = R"(#version 430 core +layout(local_size_x = 1, local_size_y = 1) in; +layout(std430) buffer Output { vec4 data[]; } g_out; +void main() { + g_out.data[0] = vec4(1.0, 2.0, 3.0, 4.0); + g_out.data[100] = vec4(1.0, 2.0, 3.0, 4.0); +} +)"; + const GLuint p = MakeProgram(nullptr, nullptr, cs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 15); + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 7); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 1); + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "Output", "Output"); + ExpectResource(p, GL_BUFFER_VARIABLE, "Output.data", "Output.data[0]"); + + const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "Output"); + const GLuint variable = GetProgramResourceIndex(p, GL_BUFFER_VARIABLE, "Output.data"); + EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, block, + {GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_COMPUTE_SHADER, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_VERTEX_SHADER, GL_ACTIVE_VARIABLES}), + (std::vector{7, 0, 1, 1, 0, 0, static_cast(variable)})); + // A runtime-sized array reports GL_ARRAY_SIZE 0 and a top-level array size of 1. + EXPECT_EQ(Props(p, GL_BUFFER_VARIABLE, variable, + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_IS_ROW_MAJOR, + GL_REFERENCED_BY_COMPUTE_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}), + (std::vector{15, GL_FLOAT_VEC4, 0, static_cast(block), 0, 1, 1})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // --------------------------------------------------------------- atomic-counters ---- + TEST_F(ProgramInterfaceTest, AtomicCounterBuffers) { + const char* fs = R"(#version 430 +out vec4 color; +layout (binding = 1, offset = 0) uniform atomic_uint a; +layout (binding = 2, offset = 0) uniform atomic_uint b; +layout (binding = 2, offset = 4) uniform atomic_uint c; +layout (binding = 5, offset = 0) uniform atomic_uint d[3]; +layout (binding = 5, offset = 12) uniform atomic_uint e; +void main() { + uint x = atomicCounterIncrement(d[0]) + atomicCounterIncrement(a); + uint y = atomicCounterIncrement(d[1]) + atomicCounterIncrement(b); + uint z = atomicCounterIncrement(d[2]) + atomicCounterIncrement(c); + uint w = atomicCounterIncrement(e); + color = vec4(float(x), float(y), float(z), float(w)); +} +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES), 3); + EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_MAX_NUM_ACTIVE_VARIABLES), 2); + + ExpectResource(p, GL_UNIFORM, "a", "a"); + ExpectResource(p, GL_UNIFORM, "d", "d[0]"); + for (const char* name : {"a", "b", "c", "d", "e", "d[0]", "d[1]", "d[2]"}) { + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, name), -1) << name; + } + + const auto bufferOf = [p](const char* uniform) { + const std::vector value = PropsOf(p, GL_UNIFORM, uniform, {GL_ATOMIC_COUNTER_BUFFER_INDEX}); + EXPECT_EQ(value.size(), 1u); + return value.empty() ? -1 : value[0]; + }; + const GLint bufferA = bufferOf("a"); + const GLint bufferB = bufferOf("b"); + const GLint bufferD = bufferOf("d"); + ASSERT_GE(bufferA, 0); + EXPECT_EQ(bufferB, bufferOf("c")); + EXPECT_EQ(bufferD, bufferOf("e")); + EXPECT_NE(bufferA, bufferB); + + EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast(bufferA), + {GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES, GL_ACTIVE_VARIABLES}), + (std::vector{1, 4, 1, static_cast(GetProgramResourceIndex(p, GL_UNIFORM, "a"))})); + EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast(bufferB), + {GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}), + (std::vector{2, 8, 2})); + EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast(bufferD), + {GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}), + (std::vector{5, 16, 2})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + + // The interface has no resource names at all. + EXPECT_EQ(GetProgramResourceIndex(p, GL_ATOMIC_COUNTER_BUFFER, "a"), GL_INVALID_INDEX); + EXPECT_EQ(TakeError(), GL_INVALID_ENUM); + } + + // --------------------------------------------------------- transform-feedback ------ + TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) { + const char* vs = R"(#version 430 +in vec4 position; +flat out ivec4 a; +out float b[2]; +flat out uvec2 c; +flat out uint d; +out vec3 e[2]; +flat out int f; +void main(void) { + a = ivec4(1); b[0] = 1.1; b[1] = 1.1; c = uvec2(1u); d = 1u; + e[0] = vec3(1.1); e[1] = vec3(1.1); f = 1; + gl_Position = position; +} +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + const char* varyings[6] = {"a", "b[0]", "b[1]", "c", "d", "e"}; + TransformFeedbackVaryings(p, 6, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_ACTIVE_RESOURCES), 6); + EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_MAX_NAME_LENGTH), 5); + for (const char* name : varyings) ExpectResource(p, GL_TRANSFORM_FEEDBACK_VARYING, name, name); + + const std::vector props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE}; + EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "a", props), + (std::vector{2, GL_INT_VEC4, 1})); + EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "b[0]", props), (std::vector{5, GL_FLOAT, 1})); + EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "c", props), + (std::vector{2, GL_UNSIGNED_INT_VEC2, 1})); + EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "e", props), + (std::vector{2, GL_FLOAT_VEC3, 2})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + TEST_F(ProgramInterfaceTest, TransformFeedbackPseudoVaryingsAreEnumeratedButUnnamed) { + const char* vs = R"(#version 430 +in vec4 position; +out ivec4 a; out uvec2 c; out uint d; out int f; out uint e; out int g; +void main(void) { + a = ivec4(1); c = uvec2(1u); d = 1u; f = 1; e = 1u; g = 1; + gl_Position = position; +} +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + const char* varyings[11] = {"a", "gl_NextBuffer", "c", "gl_SkipComponents1", "d", "gl_SkipComponents2", + "f", "gl_SkipComponents3", "e", "gl_SkipComponents4", "g"}; + TransformFeedbackVaryings(p, 11, varyings, GL_INTERLEAVED_ATTRIBS); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + // The capture list drops the layout controls; the interface must not. + EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_ACTIVE_RESOURCES), 11); + EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_MAX_NAME_LENGTH), 19); + + std::vector names; + for (GLuint i = 0; i < 11; ++i) names.push_back(ResourceName(p, GL_TRANSFORM_FEEDBACK_VARYING, i)); + const auto indexOf = [&names](const std::string& name) -> GLuint { + for (GLuint i = 0; i < names.size(); ++i) { + if (names[i] == name) return i; + } + return GL_INVALID_INDEX; + }; + const std::vector props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE}; + ASSERT_NE(indexOf("gl_NextBuffer"), GL_INVALID_INDEX); + EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_NextBuffer"), props), + (std::vector{14, GL_NONE, 0})); + EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_SkipComponents1"), props), + (std::vector{19, GL_NONE, 1})); + EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_SkipComponents4"), props), + (std::vector{19, GL_NONE, 4})); + // ...but they cannot be looked up by name. + for (const char* name : {"gl_NextBuffer", "gl_SkipComponents1", "gl_SkipComponents4"}) { + EXPECT_EQ(GetProgramResourceIndex(p, GL_TRANSFORM_FEEDBACK_VARYING, name), GL_INVALID_INDEX) << name; + } + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------- separate-programs-fragment -- + TEST_F(ProgramInterfaceTest, SeparableFragmentProgramSeparatesUniformsFromBufferVariables) { + const char* fs = R"(#version 430 +out vec4 fs_color; +layout(location = 1) uniform vec4 x; +layout(binding = 0) buffer SimpleBuffer { vec4 a; }; +in vec4 vs_color; +void main() { fs_color = vs_color + x + a; } +)"; + const GLuint p = CreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &fs); + ExpectLinked(p); + ClearErrors(); + + // KNOWN GAP, not an expectation - the same one SimpleShaders documents: a separable + // FRAGMENT program's own inputs are absent from the glslang reflection, so + // GL_PROGRAM_INPUT is empty. Spec-correct values here would be 1 and 9 ("vs_color"). + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 0); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 0); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 9); + // The buffer variable is NOT a uniform, even though the frontend reflection keeps + // both in one list. + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 2); + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 2); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 13); + EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 1); + + ExpectResource(p, GL_PROGRAM_OUTPUT, "fs_color", "fs_color"); + ExpectResource(p, GL_UNIFORM, "x", "x"); + ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer", "SimpleBuffer"); + ExpectResource(p, GL_BUFFER_VARIABLE, "a", "a"); + EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "x"), 1); + + const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer"); + const GLuint variable = GetProgramResourceIndex(p, GL_BUFFER_VARIABLE, "a"); + EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, block, + {GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_COMPUTE_SHADER, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_VERTEX_SHADER, GL_ACTIVE_VARIABLES}), + (std::vector{13, 0, 1, 0, 1, 0, static_cast(variable)})); + EXPECT_EQ(Props(p, GL_BUFFER_VARIABLE, variable, + {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR, + GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE, GL_TOP_LEVEL_ARRAY_STRIDE}), + (std::vector{2, GL_FLOAT_VEC4, 1, static_cast(block), 0, 0, 1, 1, 0})); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ---------------------------------------------------------------- error handling ---- + TEST_F(ProgramInterfaceTest, UnlinkedProgramHasZeroResourcesAndRaisesNoError) { + const GLuint p = CreateProgram(); + ClearErrors(); + + for (const GLenum iface : {GL_PROGRAM_INPUT, GL_PROGRAM_OUTPUT, GL_UNIFORM, GL_UNIFORM_BLOCK, + GL_BUFFER_VARIABLE, GL_SHADER_STORAGE_BLOCK, GL_TRANSFORM_FEEDBACK_VARYING, + GL_VERTEX_SUBROUTINE, GL_FRAGMENT_SUBROUTINE_UNIFORM}) { + EXPECT_EQ(Interfaceiv(p, iface, GL_ACTIVE_RESOURCES), 0) << std::hex << iface; + EXPECT_EQ(Interfaceiv(p, iface, GL_MAX_NAME_LENGTH), 0) << std::hex << iface; + EXPECT_EQ(GetProgramResourceIndex(p, iface, ""), GL_INVALID_INDEX) << std::hex << iface; + } + EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES), 0); + EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_MAX_NUM_ACTIVE_VARIABLES), 0); + EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 0); + // Not one stray error - a leftover here aborts the caller's next query. + EXPECT_EQ(TakeError(), GL_NO_ERROR); + + // Locations, however, really do require a successful link. + EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "pie"), -1); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "pie"), -1); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + } + + TEST_F(ProgramInterfaceTest, ErrorConditions) { + const GLuint p = MakeProgram(kSimpleVs, kSimpleFs); + BindAttribLocation(p, 0, "position"); + BindFragDataLocation(p, 0, "color"); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + GLint value = 0; + GLsizei length = 0; + GLchar name[100] = {'\0'}; + + // is not a name at all. + GetProgramInterfaceiv(1337u, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &value); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + GetProgramResourceIndex(1337u, GL_PROGRAM_INPUT, "pie"); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + GetProgramResourceLocation(1337u, GL_PROGRAM_INPUT, "pie"); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + + // names a shader object. + const GLuint shader = CreateShader(GL_FRAGMENT_SHADER); + GetProgramInterfaceiv(shader, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &value); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + GetProgramResourceIndex(shader, GL_PROGRAM_INPUT, "pie"); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + + // past the end. + GetProgramResourceName(p, GL_PROGRAM_INPUT, 3000, 1024, &length, name); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + // propCount == 0. + GLenum props[1] = {GL_NAME_LENGTH}; + GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 0, props, 1024, &length, &value); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + // Negative sizes. + GetProgramResourceName(p, GL_PROGRAM_INPUT, 0, -100, nullptr, name); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, props, -100, &length, &value); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + + // A prop this command does not know at all vs. one the interface does not carry. + GLenum unknownProp[1] = {GL_TEXTURE_1D}; + GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, unknownProp, 1024, &length, &value); + EXPECT_EQ(TakeError(), GL_INVALID_ENUM); + GLenum wrongInterfaceProp[1] = {GL_OFFSET}; + GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, wrongInterfaceProp, 1024, &length, &value); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + + // GL_ATOMIC_COUNTER_BUFFER has no names, and no locations. + GetProgramResourceName(p, GL_ATOMIC_COUNTER_BUFFER, 0, 1024, &length, name); + EXPECT_EQ(TakeError(), GL_INVALID_ENUM); + GetProgramResourceLocation(p, GL_ATOMIC_COUNTER_BUFFER, "position"); + EXPECT_EQ(TakeError(), GL_INVALID_ENUM); + // ...and GetProgramInterfaceiv rejects a pname the interface does not answer. + GetProgramInterfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NUM_ACTIVE_VARIABLES, &value); + EXPECT_EQ(TakeError(), GL_INVALID_OPERATION); + } + + TEST_F(ProgramInterfaceTest, BufSizeIsRespected) { + const char* vs = R"(#version 430 +in vec4 position; +uniform vec4 someLongName; +void main(void) { gl_Position = position + someLongName; } +)"; + const GLuint p = MakeProgram(vs, kSimpleFs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + const GLuint index = GetProgramResourceIndex(p, GL_UNIFORM, "someLongName"); + ASSERT_NE(index, GL_INVALID_INDEX); + GLchar buffer[3] = {'a', 'b', 'c'}; + GLsizei length = -1; + GetProgramResourceName(p, GL_UNIFORM, index, 0, nullptr, nullptr); + GetProgramResourceName(p, GL_UNIFORM, index, 0, nullptr, buffer); + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[2], 'c'); + GetProgramResourceName(p, GL_UNIFORM, index, 2, &length, buffer); + EXPECT_EQ(buffer[0], 's'); + EXPECT_EQ(buffer[1], '\0'); + EXPECT_EQ(buffer[2], 'c'); + EXPECT_EQ(length, 1); + + GLint params[3] = {1, 2, 3}; + const GLenum props[] = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_OFFSET, GL_BLOCK_INDEX, GL_LOCATION}; + GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 0, nullptr, nullptr); + GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 0, nullptr, params); + EXPECT_EQ(params[0], 1); + EXPECT_EQ(params[2], 3); + GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 2, &length, params); + EXPECT_EQ(params[0], 13); + EXPECT_EQ(params[1], GL_FLOAT_VEC4); + EXPECT_EQ(params[2], 3); + EXPECT_EQ(length, 2); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // ------------------------------------------------------------- buffer binding state ---- + // GL 4.6 ยง7.3.1 makes ONE index space out of glGetProgramResourceIndex and the command + // that consumes its answer: the index glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) + // returns IS the index glShaderStorageBlockBinding takes. The declared bindings below are + // deliberately NOT the enumeration order, so a binding applied through a different index + // space lands on the wrong block instead of failing loudly. + const char* kStorageBlockFs = R"(#version 430 +layout(binding = 3) buffer BlockA { vec4 a; }; +layout(binding = 1) buffer BlockB { vec4 b; }; +layout(binding = 2) buffer BlockC { vec4 c; }; +out vec4 color; +void main() { color = a + b + c; } +)"; + + // A binding read straight back through GL_BUFFER_BINDING, by NAME, so the assertion does + // not depend on the enumeration order it is meant to be checking. + GLint BufferBindingOf(GLuint program, GLenum iface, const char* name) { + const std::vector values = PropsOf(program, iface, name, {GL_BUFFER_BINDING}); + return values.size() == 1 ? values[0] : -12345; + } + + TEST_F(ProgramInterfaceTest, ShaderStorageBlockBindingTakesTheResourceQueryIndex) { + const GLuint p = MakeProgram(kSimpleVs, kStorageBlockFs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + // Bound first, the way a real caller reaches glShaderStorageBlockBinding. It matters + // because that entry point also delegates to the backend, and asking a BACKEND to + // build a program for the first time from inside a non-draw entry point is a + // pre-existing DirectGLES hazard (GetBackendProgramId -> SyncToBackend runs without + // the draw-path globals SyncCurrentProgram would have established; under a loaded + // llvmpipe it throws out of the transpile). Nothing about the index space under test + // depends on this - it just keeps the case testing the frontend contract. + UseProgram(p); + ASSERT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 3); + // Until something rebinds them, GL_BUFFER_BINDING is what the shader declared. + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 3); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + + // THE ROUND TRIP. BlockB is the interesting one: its enumeration index (1) and its + // declared binding (1) coincide, while BlockA's do not, so an implementation that + // confused index with binding would still pass on B alone - hence all three are + // re-read afterwards. + const GLuint blockA = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "BlockA"); + ASSERT_NE(blockA, GL_INVALID_INDEX); + ShaderStorageBlockBinding(p, blockA, 6); + // GPU-free suite: with no backend bound the call still records the binding on the + // program (that is the state GL_BUFFER_BINDING reports) and then reports that it + // could not reach a driver. Swallow exactly that. + ClearErrors(); + + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 6) << "the rebound block"; + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1) << "must not move"; + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2) << "must not move"; + EXPECT_EQ(TakeError(), GL_NO_ERROR); + + // And it survives a second, different rebinding of another block. + const GLuint blockC = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "BlockC"); + ASSERT_NE(blockC, GL_INVALID_INDEX); + ShaderStorageBlockBinding(p, blockC, 0); + ClearErrors(); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 6); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 0); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + TEST_F(ProgramInterfaceTest, ShaderStorageBlockBindingRejectsAnIndexOutsideTheInterface) { + const GLuint p = MakeProgram(kSimpleVs, kStorageBlockFs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + const GLint activeBlocks = Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES); + ASSERT_EQ(activeBlocks, 3); + ShaderStorageBlockBinding(p, static_cast(activeBlocks), 4); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + ClearErrors(); + + // Nothing moved. + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 3); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1); + EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } + + // The same rule on the uniform side: GL_BUFFER_BINDING is the CURRENT binding, and + // GL_UNIFORM_BLOCK's index space is the one glGetUniformBlockIndex / glUniformBlockBinding + // already use. + TEST_F(ProgramInterfaceTest, UniformBlockBufferBindingFollowsUniformBlockBinding) { + const char* fs = R"(#version 430 +layout(binding = 2) uniform BlockU { vec4 u; }; +layout(binding = 0) uniform BlockV { vec4 v; }; +out vec4 color; +void main() { color = u + v; } +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + ASSERT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 2); + EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockU"), 2); + EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockV"), 0); + + // One index space, both directions. + const GLuint interfaceIndex = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "BlockU"); + ASSERT_NE(interfaceIndex, GL_INVALID_INDEX); + EXPECT_EQ(interfaceIndex, GetUniformBlockIndex(p, "BlockU")); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + + UniformBlockBinding(p, interfaceIndex, 5); + ClearErrors(); + EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockU"), 5) << "the rebound block"; + EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockV"), 0) << "must not move"; + // glGetActiveUniformBlockiv is the older spelling of the same state; the two must not + // be able to disagree. + GLint viaActiveUniformBlockiv = -1; + GetActiveUniformBlockiv(p, interfaceIndex, GL_UNIFORM_BLOCK_BINDING, &viaActiveUniformBlockiv); + EXPECT_EQ(viaActiveUniformBlockiv, 5); + EXPECT_EQ(TakeError(), GL_NO_ERROR); + } +} // namespace