mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e0460e472 | ||
|
|
33ff177bb2 | ||
|
|
2e6fc1ffc0 |
@@ -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
|
||||
|
||||
@@ -181,15 +181,18 @@ namespace MobileGL {
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
||||
GLsizei* length, GLchar* name);
|
||||
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
|
||||
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
|
||||
// the application passes is the frontend interface-query enumeration's, and no
|
||||
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
|
||||
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
|
||||
// The name is the one coordinate all three agree on, so the frontend resolves the
|
||||
// index against its own enumeration and each backend maps the name to its own.
|
||||
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
|
||||
GLuint storageBlockBinding);
|
||||
// GL fence sync objects. All entries are optional (may be null); the
|
||||
// frontend then falls back to always-signaled sync semantics.
|
||||
// FenceSync may itself return null when the backend cannot create a
|
||||
|
||||
@@ -968,12 +968,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.Clear = Clear;
|
||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||
|
||||
@@ -1422,15 +1422,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool g_hasSyncedRenderState = false;
|
||||
static RenderStateParameters g_syncedRenderStateParameters;
|
||||
static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
|
||||
// The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState);
|
||||
// an impossible value so the first sync always pushes.
|
||||
static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
|
||||
// GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the
|
||||
// frontend's (desktop-GL default) disabled state down.
|
||||
static Bool g_syncedSrgbFramebufferWrites = true;
|
||||
// Set when the shadow below stops describing the real ES context. The ES context
|
||||
// OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the
|
||||
// previous context's enable state while the frontend's parameter block AND its
|
||||
// version counter both restart from defaults. Every "differs from what I last pushed"
|
||||
// test in SyncRenderState would then agree that nothing needs pushing, and the
|
||||
// leftover state silently applies to the new context - the class of bug the CTS
|
||||
// caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into
|
||||
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole
|
||||
// block rather than the one cap that happened to be noticed.
|
||||
static Bool g_forceFullRenderStateResync = true;
|
||||
void InvalidateSyncedRenderState() {
|
||||
g_forceFullRenderStateResync = true;
|
||||
g_hasSyncedRenderState = false;
|
||||
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
|
||||
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
|
||||
}
|
||||
void SyncRenderState() {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
if (g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) return;
|
||||
const Bool forceFullPush = g_forceFullRenderStateResync;
|
||||
g_forceFullRenderStateResync = false;
|
||||
if (!forceFullPush && g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
|
||||
@@ -1478,7 +1501,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
|
||||
if (tailSpanDirty) {
|
||||
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
||||
if (parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||
if (parameters.cap_mg##Enabled) { \
|
||||
g_GLESFuncs.glEnable(cap_gl); \
|
||||
} else { \
|
||||
@@ -1507,7 +1530,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// into an sRGB colour buffer comes back encoded once too often (the shader's own
|
||||
// decode on the next fetch then leaves the value one conversion short).
|
||||
const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
||||
if (g_GLESCapabilities.SupportsSrgbWriteControl && srgbWrites != g_syncedSrgbFramebufferWrites) {
|
||||
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
|
||||
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
|
||||
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB)
|
||||
: g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB);
|
||||
g_syncedSrgbFramebufferWrites = srgbWrites;
|
||||
@@ -1520,7 +1544,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled;
|
||||
const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled ||
|
||||
g_syncedRenderStateParameters.PrimitiveRestartEnabled;
|
||||
if (restart != syncedRestart) {
|
||||
if (forceFullPush || restart != syncedRestart) {
|
||||
restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX)
|
||||
: g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
}
|
||||
@@ -1556,7 +1580,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Bool allEnabled = true;
|
||||
Bool allDisabled = true;
|
||||
Bool anyCapDirty = false;
|
||||
Bool anyCapDirty = forceFullPush;
|
||||
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
Bool enabled = targetStates[i].Enabled;
|
||||
@@ -1581,7 +1605,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
s.Enabled = false;
|
||||
} else {
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (targetStates[i].Enabled != syncedStates[i].Enabled) {
|
||||
if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) {
|
||||
syncedStates[i].Enabled = targetStates[i].Enabled;
|
||||
syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i)
|
||||
: g_GLESFuncs.glDisablei(GL_BLEND, i);
|
||||
@@ -1591,7 +1615,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Bool allFuncsSame = true;
|
||||
Bool anyFuncDirty = false;
|
||||
Bool anyFuncDirty = forceFullPush;
|
||||
const auto& first = targetStates[0];
|
||||
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
@@ -1630,8 +1654,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& cur = targetStates[i];
|
||||
auto& syn = syncedStates[i];
|
||||
|
||||
if (cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB ||
|
||||
cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha) {
|
||||
if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB ||
|
||||
cur.DstFactorRGB != syn.DstFactorRGB ||
|
||||
cur.SrcFactorAlpha != syn.SrcFactorAlpha ||
|
||||
cur.DstFactorAlpha != syn.DstFactorAlpha) {
|
||||
syn.SrcFactorRGB = cur.SrcFactorRGB;
|
||||
syn.DstFactorRGB = cur.DstFactorRGB;
|
||||
syn.SrcFactorAlpha = cur.SrcFactorAlpha;
|
||||
@@ -1648,7 +1674,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Bool allEquationsSame = true;
|
||||
Bool anyEquationDirty = false;
|
||||
Bool anyEquationDirty = forceFullPush;
|
||||
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
const auto& cur = targetStates[i];
|
||||
@@ -1679,7 +1705,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& cur = targetStates[i];
|
||||
auto& syn = syncedStates[i];
|
||||
|
||||
if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) {
|
||||
if (forceFullPush || cur.ColorEquation != syn.ColorEquation ||
|
||||
cur.AlphaEquation != syn.AlphaEquation) {
|
||||
syn.ColorEquation = cur.ColorEquation;
|
||||
syn.AlphaEquation = cur.AlphaEquation;
|
||||
|
||||
@@ -1693,13 +1720,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Depth state
|
||||
if (parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
|
||||
if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
|
||||
g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc));
|
||||
}
|
||||
if (parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
if (parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
||||
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
|
||||
}
|
||||
}
|
||||
@@ -1710,16 +1737,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex];
|
||||
const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK;
|
||||
|
||||
if (current.Func != synced.Func || current.Ref != synced.Ref ||
|
||||
if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref ||
|
||||
current.ValueMask != synced.ValueMask) {
|
||||
g_GLESFuncs.glStencilFuncSeparate(
|
||||
glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func), current.Ref,
|
||||
current.ValueMask);
|
||||
}
|
||||
if (current.WriteMask != synced.WriteMask) {
|
||||
if (forceFullPush || current.WriteMask != synced.WriteMask) {
|
||||
g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask);
|
||||
}
|
||||
if (current.FailOp != synced.FailOp || current.PassDepthFailOp != synced.PassDepthFailOp ||
|
||||
if (forceFullPush || current.FailOp != synced.FailOp ||
|
||||
current.PassDepthFailOp != synced.PassDepthFailOp ||
|
||||
current.PassDepthPassOp != synced.PassDepthPassOp) {
|
||||
g_GLESFuncs.glStencilOpSeparate(
|
||||
glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp),
|
||||
@@ -1736,7 +1764,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& targetMasks = parameters.ColorMasks;
|
||||
const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks;
|
||||
|
||||
Bool anyDirty = false;
|
||||
Bool anyDirty = forceFullPush;
|
||||
Bool allSame = true;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (targetMasks[i] != syncedMasks[i]) anyDirty = true;
|
||||
@@ -1753,7 +1781,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
|
||||
: g_GLESFuncs.glColorMaskiOES;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (targetMasks[i] != syncedMasks[i]) {
|
||||
if (forceFullPush || targetMasks[i] != syncedMasks[i]) {
|
||||
const BoolVec4& m = targetMasks[i];
|
||||
colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
|
||||
ToGLBoolean(m.w()));
|
||||
@@ -1765,7 +1793,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present.
|
||||
// Without the extension the mode stays FILL and non-FILL requests are dropped.
|
||||
if (parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront &&
|
||||
if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) &&
|
||||
g_GLESCapabilities.SupportsPolygonMode) {
|
||||
const auto polygonModeFn =
|
||||
g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE;
|
||||
@@ -1774,67 +1802,97 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Clear values
|
||||
if (parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
|
||||
if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
|
||||
const FloatVec4& clearCol = parameters.ClearColor;
|
||||
g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w());
|
||||
}
|
||||
if (parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
|
||||
if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
|
||||
g_GLESFuncs.glClearDepthf(parameters.ClearDepth);
|
||||
}
|
||||
if (parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
|
||||
if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
|
||||
g_GLESFuncs.glClearStencil(static_cast<GLint>(parameters.ClearStencil));
|
||||
}
|
||||
if (parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
|
||||
if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
|
||||
const FloatVec4& blendColor = parameters.BlendColor;
|
||||
g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w());
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Cull face mode
|
||||
if (parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
|
||||
if (forceFullPush || parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
|
||||
const CullFaceMode& cfm = parameters.CullFaceModeSetting;
|
||||
g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm));
|
||||
}
|
||||
if (parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
|
||||
if (forceFullPush || parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
|
||||
const FrontFaceMode& ffm = parameters.FrontFaceModeSetting;
|
||||
g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm));
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Scissor box
|
||||
if (parameters.ScissorBox != g_syncedRenderStateParameters.ScissorBox) {
|
||||
const IntVec4& scissorBox = parameters.ScissorBox;
|
||||
g_GLESFuncs.glScissor(scissorBox.x(), scissorBox.y(), scissorBox.z(), scissorBox.w());
|
||||
if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and
|
||||
// for the same reason: what has to reach the driver is NOT simply the parameter
|
||||
// field. (0,0,0,0) is where RenderStateParameters::ScissorBox starts and the only
|
||||
// thing that ever writes it is glScissor, so that value means "the application has
|
||||
// never called glScissor" - it is not a GL scissor box. GL's initial box is the
|
||||
// whole window, which the frontend has no way to spell before a surface exists.
|
||||
// The pre-resync code got away with pushing the field verbatim only by accident:
|
||||
// the shadow held the same default, the field never compared unequal, and the ES
|
||||
// context kept its own correct default. Under the forced full push that accident
|
||||
// is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle, and
|
||||
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
||||
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
||||
// loses the terrain and the whole GUI.
|
||||
IntVec4 backendScissorBox = parameters.ScissorBox;
|
||||
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
||||
backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
||||
}
|
||||
}
|
||||
// Compared against what was actually PUSHED, not against the parameter field, so
|
||||
// the resolved value and the diff can never disagree.
|
||||
if (backendScissorBox != g_syncedBackendScissorBox) {
|
||||
g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(),
|
||||
backendScissorBox.w());
|
||||
g_syncedBackendScissorBox = backendScissorBox;
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Logic op (first field of the tail span)
|
||||
if (parameters.LogicOp != g_syncedRenderStateParameters.LogicOp) {
|
||||
// glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a
|
||||
// plain ES 3.x driver. Before the forced resync it was reached only when an app
|
||||
// actually set a logic op; now every MakeCurrent would call it, so the null check
|
||||
// is mandatory rather than defensive.
|
||||
if (g_GLESFuncs.glLogicOp &&
|
||||
(forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) {
|
||||
g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp));
|
||||
}
|
||||
}
|
||||
|
||||
if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below)
|
||||
if (parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
|
||||
if (forceFullPush || parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
|
||||
parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits) {
|
||||
g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits);
|
||||
}
|
||||
}
|
||||
|
||||
if (headSpanDirty) { // Line width
|
||||
if (parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
|
||||
if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
|
||||
g_GLESFuncs.glLineWidth(parameters.LineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
if (headSpanDirty) { // Point size
|
||||
if (parameters.PointSize != g_syncedRenderStateParameters.PointSize) {
|
||||
if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize,
|
||||
// so the entry point is absent on most drivers; see the glLogicOp note above)
|
||||
if (g_GLESFuncs.glPointSize &&
|
||||
(forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) {
|
||||
g_GLESFuncs.glPointSize(parameters.PointSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Sample coverage
|
||||
if (parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
|
||||
if (forceFullPush || parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
|
||||
parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) {
|
||||
g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue,
|
||||
ToGLBoolean(parameters.SampleCoverageInvert));
|
||||
@@ -1842,7 +1900,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Sample mask
|
||||
if (g_GLESFuncs.glSampleMaski && parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue) {
|
||||
if (g_GLESFuncs.glSampleMaski &&
|
||||
(forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) {
|
||||
g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue);
|
||||
}
|
||||
}
|
||||
@@ -5189,50 +5248,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGetProgramiv(backendProgramId, pname, params);
|
||||
}
|
||||
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return;
|
||||
g_GLESFuncs.glGetProgramInterfaceiv(backendProgramId, programInterface, pname, params);
|
||||
}
|
||||
// NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry
|
||||
// point must never be the thing that BUILDS a backend program.
|
||||
//
|
||||
// The frontend has already recorded the rebinding on the program object
|
||||
// (SetShaderStorageBlockBinding) - that record is what GL_BUFFER_BINDING reports and what
|
||||
// BackendProgramObjectImpl::SyncToBackend replays onto every driver program it builds. So
|
||||
// the only work left here is an optimisation: push the change straight onto a driver
|
||||
// program that is ALREADY built and already current with this link, so the next draw does
|
||||
// not have to be preceded by a rebuild.
|
||||
//
|
||||
// Calling GetBackendProgramId() instead would sync-on-demand from a non-draw entry point,
|
||||
// i.e. transpile and compile the whole program while the draw-path globals that the ESSL
|
||||
// is generated against (PrgramImpl::g_fragColorBroadcastCount, the snorm/unorm clamp
|
||||
// masks - established by SyncCurrentProgram) still hold another program's values. That
|
||||
// bakes a program against the wrong state, and under CPU load it was also observed to
|
||||
// fail the driver compile outright. Deferring is spec-fine: a binding only has to take
|
||||
// effect by the block's next use.
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
if (!storageBlockName) return;
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) return;
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return GL_INVALID_INDEX;
|
||||
return g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, programInterface, name);
|
||||
}
|
||||
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
||||
GLchar* name) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return;
|
||||
g_GLESFuncs.glGetProgramResourceName(backendProgramId, programInterface, index, bufSize, length, name);
|
||||
}
|
||||
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return;
|
||||
g_GLESFuncs.glGetProgramResourceiv(backendProgramId, programInterface, index, propCount, props, bufSize, length,
|
||||
params);
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return -1;
|
||||
return g_GLESFuncs.glGetProgramResourceLocation(backendProgramId, programInterface, name);
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
(void)program;
|
||||
(void)programInterface;
|
||||
(void)name;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
||||
GLuint backendProgramId = GetBackendProgramId(program);
|
||||
if (!backendProgramId) return;
|
||||
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, storageBlockIndex, storageBlockBinding);
|
||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
|
||||
if (!backendProgramSlot || !*backendProgramSlot) return;
|
||||
auto& backendObj = *backendProgramSlot;
|
||||
// Not merely "a program id exists": a backend object whose synced link version has
|
||||
// fallen behind is about to be rebuilt anyway, and its current driver interface is
|
||||
// the PREVIOUS link's - applying to it could land the binding on an unrelated block.
|
||||
if (!backendObj->GetBackendProgramId() ||
|
||||
backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) {
|
||||
return; // SyncToBackend's reseed will carry it
|
||||
}
|
||||
PrgramImpl::ApplyShaderStorageBlockBinding(backendObj->GetBackendProgramId(), storageBlockName,
|
||||
storageBlockBinding);
|
||||
}
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
@@ -6778,6 +6828,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
PixelStoreImpl::InvalidatePackStateCache();
|
||||
// The render-state shadow belongs in this list for the same reason as the ones above:
|
||||
// it describes the real ES context, which outlives the MobileGL context that is
|
||||
// becoming current. See InvalidateSyncedRenderState.
|
||||
RenderStateImpl::InvalidateSyncedRenderState();
|
||||
// eglSwapInterval requires a current context; a request made while none was
|
||||
// current (and dropped by the driver) is retried here.
|
||||
ApplyRequestedSwapInterval();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3812,6 +3812,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) {
|
||||
if (backendProgramId == 0 || blockName.empty()) return false;
|
||||
if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false;
|
||||
GLuint driverIndex =
|
||||
g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str());
|
||||
if (driverIndex == GL_INVALID_INDEX) {
|
||||
// An arrayed block is enumerated per element by GL but declared once; the
|
||||
// generated ESSL carries the bare block name.
|
||||
const auto bracket = blockName.rfind('[');
|
||||
if (bracket == String::npos || blockName.back() != ']') return false;
|
||||
driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK,
|
||||
blockName.substr(0, bracket).c_str());
|
||||
if (driverIndex == GL_INVALID_INDEX) return false;
|
||||
}
|
||||
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
const auto& overrides = stateProgramObject.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.empty()) return; // the overwhelming majority of programs
|
||||
for (const auto& [blockName, binding] : overrides) {
|
||||
if (binding < 0) continue;
|
||||
ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast<Uint>(binding));
|
||||
}
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -3846,9 +3874,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (attachedCount > 0) {
|
||||
Vector<GLuint> attachedShaders(attachedCount);
|
||||
GLsizei actualCount;
|
||||
// Every GL out-param in this function is pre-initialized and every count is
|
||||
// re-clamped after the query. A driver that returns without writing the
|
||||
// out-param (no current context, a lost context, a stubbed entry point) would
|
||||
// otherwise leak an uninitialized stack value straight into a container size
|
||||
// or a loop bound - which is exactly how this path used to throw
|
||||
// length_error out of a Vector fill-ctor.
|
||||
GLsizei actualCount = 0;
|
||||
g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount,
|
||||
attachedShaders.data());
|
||||
actualCount = std::clamp<GLsizei>(actualCount, 0, static_cast<GLsizei>(attachedShaders.size()));
|
||||
MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId);
|
||||
|
||||
for (GLsizei i = 0; i < actualCount; ++i) {
|
||||
@@ -3986,13 +4021,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
|
||||
g_GLESFuncs.glCompileShader(backendShaderId);
|
||||
|
||||
GLint compileStatus;
|
||||
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed"
|
||||
// and take the diagnostic path, never as a silent success that attaches an
|
||||
// uncompiled shader.
|
||||
GLint compileStatus = GL_FALSE;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
|
||||
if (compileStatus == GL_FALSE) {
|
||||
GLint logLength;
|
||||
GLint logLength = 0;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
Vector<GLchar> log(logLength);
|
||||
if (logLength < 0) logLength = 0;
|
||||
// +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator,
|
||||
// but a driver that reports 0 (or fails the query) must still leave
|
||||
// log.data() a readable empty C string for the %s below.
|
||||
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
log.back() = '\0';
|
||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
@@ -4028,14 +4071,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Linking program %u", m_backendProgramId);
|
||||
g_GLESFuncs.glLinkProgram(m_backendProgramId);
|
||||
|
||||
GLint linkStatus;
|
||||
GLint linkStatus = GL_FALSE;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
|
||||
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
|
||||
if (linkStatus != GL_TRUE) {
|
||||
GLint logLength;
|
||||
GLint logLength = 0;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
Vector<GLchar> log(logLength);
|
||||
if (logLength < 0) logLength = 0;
|
||||
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
|
||||
g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data());
|
||||
log.back() = '\0';
|
||||
MGLOG_E("Program %u linking failed for %u: %s", stateProgramObject->GetExternalIndex(),
|
||||
m_backendProgramId, log.data());
|
||||
} else {
|
||||
@@ -4068,6 +4113,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
CacheResourceLocations(stateProgramObject);
|
||||
// AFTER the link, because glShaderStorageBlockBinding needs the driver's linked
|
||||
// interface. This is the only place Espryt applies a rebinding: the frontend
|
||||
// record is authoritative and the glShaderStorageBlockBinding entry point itself
|
||||
// deliberately never forces a program build (see DirectGLES.cpp), so a rebinding
|
||||
// requested while no backend program existed yet arrives here instead.
|
||||
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
|
||||
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
|
||||
|
||||
m_isInitialized = true;
|
||||
|
||||
@@ -981,6 +981,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Uint g_lastUsedBackendProgramId;
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||
g_backendProgramObjects;
|
||||
|
||||
// Points one shader storage block of an ALREADY-LINKED backend program at
|
||||
// `binding`. `blockName` is the frontend interface-query spelling; the real
|
||||
// driver's own index for it is looked up here, because the transpiled ESSL's
|
||||
// block order is not the frontend's. Returns false when the block does not exist
|
||||
// on the backend program (eliminated as unused, or the driver lacks the entry
|
||||
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||
// record either way.
|
||||
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||
// program that was just built. The frontend record is authoritative (only the
|
||||
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
||||
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
||||
// reseed-on-rebuild in BuildProgramResourceCache.
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
|
||||
@@ -617,12 +617,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
|
||||
@@ -232,6 +232,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
StorageBlockResource block{};
|
||||
block.name = blockName;
|
||||
block.binding = binding->binding;
|
||||
// glShaderStorageBlockBinding survives every rebuild of this cache: the
|
||||
// authoritative record of a rebound block lives on the program (it is what
|
||||
// GL_BUFFER_BINDING reports), and only the shader's declared binding is
|
||||
// recoverable from the SPIR-V. Without this, any unrelated state-version
|
||||
// bump would silently revert the block to its declared binding.
|
||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(blockName);
|
||||
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||
block.dataSize = static_cast<GLint>(binding->block.size);
|
||||
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
|
||||
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
|
||||
@@ -256,18 +263,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return programObject.get();
|
||||
}
|
||||
|
||||
void CopyResourceName(const String& source, GLsizei bufSize, GLsizei* length, GLchar* name) {
|
||||
const GLsizei writtenLength = static_cast<GLsizei>(source.size());
|
||||
if (length) {
|
||||
*length = writtenLength;
|
||||
}
|
||||
if (name && bufSize > 0) {
|
||||
const GLsizei copyLength = std::min<GLsizei>(bufSize - 1, writtenLength);
|
||||
std::memcpy(name, source.data(), static_cast<SizeT>(copyLength));
|
||||
name[copyLength] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
@@ -288,100 +283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return reinterpret_cast<const Uint8*>(indirect);
|
||||
}
|
||||
|
||||
Vector<GLuint> GetUniformBlockActiveVariables(const MG_State::GLState::ProgramObject& program,
|
||||
GLuint blockIndex) {
|
||||
Vector<GLuint> activeVariables;
|
||||
const Uint uniformCount = program.GetUniformCount();
|
||||
activeVariables.reserve(uniformCount);
|
||||
for (Uint uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) {
|
||||
if (program.GetActiveUniformBlockIndex(uniformIndex) == static_cast<Int>(blockIndex)) {
|
||||
activeVariables.push_back(uniformIndex);
|
||||
}
|
||||
}
|
||||
return activeVariables;
|
||||
}
|
||||
|
||||
GLuint FindProgramInputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
const Int activeCount = program.GetActiveAttributesCount();
|
||||
for (Int index = 0; index < activeCount; ++index) {
|
||||
if (program.GetActiveAttribName(index) == name) {
|
||||
return static_cast<GLuint>(index);
|
||||
}
|
||||
}
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
|
||||
GLuint FindProgramOutputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
||||
for (Int index = 0; index < activeCount; ++index) {
|
||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
||||
return static_cast<GLuint>(index);
|
||||
}
|
||||
}
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
|
||||
GLint GetProgramOutputLocation(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
||||
for (Int index = 0; index < activeCount; ++index) {
|
||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
||||
return program.GetFragmentOutputLocation(index);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
GLint GetProgramResourceActiveCount(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
||||
const ProgramResourceCache& cache) {
|
||||
switch (programInterface) {
|
||||
case GL_SHADER_STORAGE_BLOCK:
|
||||
return static_cast<GLint>(cache.storageBlocks.size());
|
||||
case GL_BUFFER_VARIABLE:
|
||||
return static_cast<GLint>(cache.bufferVariables.size());
|
||||
case GL_UNIFORM_BLOCK:
|
||||
return program.GetActiveUniformBlocksCount();
|
||||
case GL_UNIFORM:
|
||||
return static_cast<GLint>(program.GetUniformCount());
|
||||
case GL_PROGRAM_INPUT:
|
||||
return program.GetActiveAttributesCount();
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
return program.GetActiveFragmentOutputCount();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
GLint GetProgramResourceMaxNameLength(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
||||
const ProgramResourceCache& cache) {
|
||||
switch (programInterface) {
|
||||
case GL_SHADER_STORAGE_BLOCK: {
|
||||
SizeT maxLength = 0;
|
||||
for (const auto& block : cache.storageBlocks) maxLength = std::max(maxLength, block.name.size() + 1);
|
||||
return static_cast<GLint>(maxLength);
|
||||
}
|
||||
case GL_BUFFER_VARIABLE: {
|
||||
SizeT maxLength = 0;
|
||||
for (const auto& var : cache.bufferVariables) maxLength = std::max(maxLength, var.name.size() + 1);
|
||||
return static_cast<GLint>(maxLength);
|
||||
}
|
||||
case GL_UNIFORM_BLOCK:
|
||||
return program.GetActiveUniformBlocksMaxNameLength() + 1;
|
||||
case GL_UNIFORM:
|
||||
return program.GetUniformMaxLength() + 1;
|
||||
case GL_PROGRAM_INPUT:
|
||||
return program.GetActiveAttributesMaxLength() + 1;
|
||||
case GL_PROGRAM_OUTPUT: {
|
||||
SizeT maxLength = 0;
|
||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
||||
for (Int index = 0; index < activeCount; ++index) {
|
||||
maxLength = std::max(maxLength, program.GetActiveFragmentOutputName(index).size() + 1);
|
||||
}
|
||||
return static_cast<GLint>(maxLength);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ClearProgramResourceCaches() {
|
||||
@@ -395,11 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
auto& cache = GetProgramResourceCache(program);
|
||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||
[&](const StorageBlockResource& block) { return block.name == name; });
|
||||
return it == cache.storageBlocks.end()
|
||||
? GL_INVALID_INDEX
|
||||
: static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
||||
auto find = [&cache](const String& key) {
|
||||
return std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||
[&](const StorageBlockResource& block) { return block.name == key; });
|
||||
};
|
||||
auto it = find(name);
|
||||
if (it == cache.storageBlocks.end()) {
|
||||
// Cache names are normalized (NormalizeDescriptorName drops the array suffix), so
|
||||
// an arrayed block that GL enumerates per element - "B[0]", "B[1]" - is one entry
|
||||
// here, spelled "B". Retry against the bare name before giving up.
|
||||
const auto bracket = name.rfind('[');
|
||||
if (bracket == String::npos || name.empty() || name.back() != ']') return GL_INVALID_INDEX;
|
||||
it = find(name.substr(0, bracket));
|
||||
if (it == cache.storageBlocks.end()) return GL_INVALID_INDEX;
|
||||
}
|
||||
return static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
||||
}
|
||||
|
||||
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
|
||||
@@ -874,357 +785,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
||||
if (!params) return;
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) return;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
switch (pname) {
|
||||
case GL_ACTIVE_RESOURCES:
|
||||
*params = GetProgramResourceActiveCount(*programObject, programInterface, cache);
|
||||
return;
|
||||
case GL_MAX_NAME_LENGTH:
|
||||
*params = GetProgramResourceMaxNameLength(*programObject, programInterface, cache);
|
||||
return;
|
||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
||||
SizeT maxCount = 0;
|
||||
for (const auto& block : cache.storageBlocks) {
|
||||
maxCount = std::max(maxCount, block.activeVariables.size());
|
||||
}
|
||||
*params = static_cast<GLint>(maxCount);
|
||||
} else if (programInterface == GL_UNIFORM_BLOCK) {
|
||||
GLint maxCount = 0;
|
||||
const Int activeBlocks = programObject->GetActiveUniformBlocksCount();
|
||||
for (Int index = 0; index < activeBlocks; ++index) {
|
||||
maxCount = std::max(maxCount, programObject->GetUniformBlockActiveUniformCount(index));
|
||||
}
|
||||
*params = maxCount;
|
||||
} else {
|
||||
*params = 0;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
if (!name) return GL_INVALID_INDEX;
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) return GL_INVALID_INDEX;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
const String resourceName = name;
|
||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
||||
return GetShaderStorageBlockIndex(*programObject, name);
|
||||
}
|
||||
if (programInterface == GL_BUFFER_VARIABLE) {
|
||||
const auto it = std::find_if(cache.bufferVariables.begin(), cache.bufferVariables.end(),
|
||||
[&](const BufferVariableResource& var) { return var.name == resourceName; });
|
||||
return it == cache.bufferVariables.end()
|
||||
? GL_INVALID_INDEX
|
||||
: static_cast<GLuint>(std::distance(cache.bufferVariables.begin(), it));
|
||||
}
|
||||
if (programInterface == GL_UNIFORM_BLOCK) {
|
||||
return programObject->GetUniformBlockIndex(name);
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
const Int activeUniformIndex = programObject->GetActiveUniformIndex(resourceName);
|
||||
return activeUniformIndex >= 0 ? static_cast<GLuint>(activeUniformIndex) : GL_INVALID_INDEX;
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_INPUT) {
|
||||
return FindProgramInputIndex(*programObject, resourceName);
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
||||
return FindProgramOutputIndex(*programObject, resourceName);
|
||||
}
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
||||
GLsizei* length, GLchar* name) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) return;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
||||
CopyResourceName(cache.storageBlocks[index].name, bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
||||
CopyResourceName(cache.bufferVariables[index].name, bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM_BLOCK && programObject->IsActiveUniformBlock(index)) {
|
||||
CopyResourceName(programObject->GetUniformBlockName(index), bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
||||
CopyResourceName(programObject->GetActiveUniformName(index), bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_INPUT && index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
||||
CopyResourceName(programObject->GetActiveAttribName(index), bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_OUTPUT &&
|
||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
||||
CopyResourceName(programObject->GetActiveFragmentOutputName(index), bufSize, length, name);
|
||||
return;
|
||||
}
|
||||
if (length) *length = 0;
|
||||
if (name && bufSize > 0) name[0] = '\0';
|
||||
}
|
||||
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || !props || !params || bufSize <= 0) return;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
GLsizei written = 0;
|
||||
auto writeValue = [&](GLint value) {
|
||||
if (written < bufSize) {
|
||||
params[written++] = value;
|
||||
}
|
||||
};
|
||||
|
||||
for (GLsizei propIndex = 0; propIndex < propCount; ++propIndex) {
|
||||
const GLenum prop = props[propIndex];
|
||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
||||
const auto& block = cache.storageBlocks[index];
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(block.name.size() + 1));
|
||||
break;
|
||||
case GL_BUFFER_BINDING:
|
||||
writeValue(static_cast<GLint>(block.binding));
|
||||
break;
|
||||
case GL_BUFFER_DATA_SIZE:
|
||||
writeValue(block.dataSize);
|
||||
break;
|
||||
case GL_NUM_ACTIVE_VARIABLES:
|
||||
writeValue(static_cast<GLint>(block.activeVariables.size()));
|
||||
break;
|
||||
case GL_ACTIVE_VARIABLES:
|
||||
for (const auto variable : block.activeVariables) writeValue(static_cast<GLint>(variable));
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
||||
const auto& var = cache.bufferVariables[index];
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(var.name.size() + 1));
|
||||
break;
|
||||
case GL_TYPE:
|
||||
writeValue(GL_FLOAT);
|
||||
break;
|
||||
case GL_ARRAY_SIZE:
|
||||
writeValue(1);
|
||||
break;
|
||||
case GL_OFFSET:
|
||||
writeValue(var.offset);
|
||||
break;
|
||||
case GL_BLOCK_INDEX:
|
||||
writeValue(static_cast<GLint>(var.blockIndex));
|
||||
break;
|
||||
case GL_ARRAY_STRIDE:
|
||||
case GL_MATRIX_STRIDE:
|
||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||
case GL_IS_ROW_MAJOR:
|
||||
writeValue(0);
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else if (programInterface == GL_UNIFORM_BLOCK &&
|
||||
programObject->IsActiveUniformBlock(index)) {
|
||||
const auto activeVariables = GetUniformBlockActiveVariables(*programObject, index);
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockName(index).size() + 1));
|
||||
break;
|
||||
case GL_BUFFER_BINDING:
|
||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockBinding(index)));
|
||||
break;
|
||||
case GL_BUFFER_DATA_SIZE:
|
||||
writeValue(static_cast<GLint>(programObject->GetUBOSizeAt(index)));
|
||||
break;
|
||||
case GL_NUM_ACTIVE_VARIABLES:
|
||||
writeValue(static_cast<GLint>(activeVariables.size()));
|
||||
break;
|
||||
case GL_ACTIVE_VARIABLES:
|
||||
for (const GLuint variableIndex : activeVariables) {
|
||||
writeValue(static_cast<GLint>(variableIndex));
|
||||
}
|
||||
break;
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangVertex) ? GL_TRUE
|
||||
: GL_FALSE);
|
||||
break;
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangFragment) ? GL_TRUE
|
||||
: GL_FALSE);
|
||||
break;
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangCompute) ? GL_TRUE
|
||||
: GL_FALSE);
|
||||
break;
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
writeValue(GL_FALSE);
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
||||
const auto& uniformName = programObject->GetActiveUniformName(index);
|
||||
const GLint location = programObject->GetUniformLocation(uniformName);
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(uniformName.size() + 1));
|
||||
break;
|
||||
case GL_TYPE:
|
||||
writeValue(static_cast<GLint>(programObject->GetActiveUniformType(index)));
|
||||
break;
|
||||
case GL_ARRAY_SIZE:
|
||||
writeValue(programObject->GetActiveUniformArraySize(index));
|
||||
break;
|
||||
case GL_BLOCK_INDEX:
|
||||
writeValue(programObject->GetActiveUniformBlockIndex(index));
|
||||
break;
|
||||
case GL_LOCATION:
|
||||
writeValue(location);
|
||||
break;
|
||||
case GL_OFFSET:
|
||||
writeValue(location >= 0 && programObject->IsValidUniformLocation(location)
|
||||
? static_cast<GLint>(programObject->GetUniformOffset(location))
|
||||
: 0);
|
||||
break;
|
||||
case GL_ARRAY_STRIDE:
|
||||
case GL_MATRIX_STRIDE:
|
||||
case GL_IS_ROW_MAJOR:
|
||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
writeValue(0);
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else if (programInterface == GL_PROGRAM_INPUT &&
|
||||
index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
||||
const auto& resourceName = programObject->GetActiveAttribName(index);
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
||||
break;
|
||||
case GL_TYPE:
|
||||
writeValue(static_cast<GLint>(programObject->GetActiveAttribType(index)));
|
||||
break;
|
||||
case GL_ARRAY_SIZE:
|
||||
writeValue(programObject->GetActiveAttribArraySize(index));
|
||||
break;
|
||||
case GL_LOCATION:
|
||||
writeValue(programObject->GetAttributeLocation(resourceName));
|
||||
break;
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
writeValue(GL_TRUE);
|
||||
break;
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
case GL_IS_PER_PATCH:
|
||||
case GL_LOCATION_INDEX:
|
||||
writeValue(0);
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else if (programInterface == GL_PROGRAM_OUTPUT &&
|
||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
||||
const auto& resourceName = programObject->GetActiveFragmentOutputName(index);
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
||||
break;
|
||||
case GL_TYPE:
|
||||
writeValue(static_cast<GLint>(programObject->GetFragmentOutputType(index)));
|
||||
break;
|
||||
case GL_ARRAY_SIZE:
|
||||
writeValue(programObject->GetActiveFragmentOutputArraySize(index));
|
||||
break;
|
||||
case GL_LOCATION:
|
||||
writeValue(programObject->GetFragmentOutputLocation(index));
|
||||
break;
|
||||
case GL_LOCATION_INDEX:
|
||||
writeValue(0);
|
||||
break;
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
writeValue(GL_TRUE);
|
||||
break;
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
case GL_IS_PER_PATCH:
|
||||
writeValue(0);
|
||||
break;
|
||||
default:
|
||||
writeValue(0);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
writeValue(0);
|
||||
}
|
||||
}
|
||||
if (length) *length = written;
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || !name) return -1;
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
return programObject->GetUniformLocation(name);
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_INPUT) {
|
||||
return programObject->GetAttributeLocation(name);
|
||||
}
|
||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
||||
return GetProgramOutputLocation(*programObject, name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || !name) return -1;
|
||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
||||
return GetProgramOutputLocation(*programObject, name) >= 0 ? 0 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) return;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
if (!programObject || storageBlockName == nullptr) return;
|
||||
const Int maxBindings = pActiveBackendObject
|
||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||
: 0;
|
||||
@@ -1234,13 +797,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
||||
return;
|
||||
}
|
||||
if (storageBlockIndex >= cache.storageBlocks.size()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage block index is not active."));
|
||||
return;
|
||||
}
|
||||
cache.storageBlocks[storageBlockIndex].binding = storageBlockBinding;
|
||||
// The frontend already validated that the name denotes an active block, and has
|
||||
// already recorded the new binding on the program - which is what reseeds this cache
|
||||
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
|
||||
// (the common case: the very next draw reads it) from having to be thrown away.
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
|
||||
if (blockIndex == GL_INVALID_INDEX) return;
|
||||
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
||||
}
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
||||
|
||||
@@ -97,15 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
||||
GLsizei* length, GLchar* name);
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GL_Buffer.h"
|
||||
#include "Validators.h"
|
||||
#include "../Texture/GL_Texture.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
#include <Config.h>
|
||||
@@ -861,6 +862,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
||||
auto mappingAccess = bufferObject->GetMappingAccess();
|
||||
// GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open
|
||||
// intersection test. There used to be a second test below this one asking only
|
||||
// `offset + size >= mappedRange.start`, which rejects every write that starts
|
||||
// before a mapped tail as well - it made a legal disjoint glBufferSubData fail.
|
||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
|
||||
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -871,18 +876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
||||
if (offset + size >= mappedRange.start) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
|
||||
"Cannot modify a mapped buffer object unless it was "
|
||||
"mapped with GL_MAP_PERSISTENT_BIT."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
|
||||
}
|
||||
|
||||
@@ -1013,6 +1006,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||
// Error precedence: "no buffer is bound to target" outranks a bad size or bad
|
||||
// flags, so the binding has to be resolved before either is validated.
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
||||
if (!bufferObject) return;
|
||||
|
||||
if (size <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -1021,8 +1019,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
|
||||
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
||||
if (!bufferObject) return;
|
||||
if (bufferObject->IsImmutableStorage()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1057,6 +1053,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||
// Same precedence as BufferStorage_State: the buffer-name error comes first.
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
||||
if (!bufferObject) return;
|
||||
|
||||
if (size <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -1065,8 +1065,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
|
||||
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
||||
if (!bufferObject) return;
|
||||
if (bufferObject->IsImmutableStorage()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1486,12 +1484,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||
}
|
||||
|
||||
// GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair.
|
||||
// Every one of them is INVALID_VALUE, and all of them are checked before a single piece
|
||||
// of state is written - a rejected bind must leave the binding point exactly as it was.
|
||||
// They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores
|
||||
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||
// (a NULL buffers array, or a zero entry inside one).
|
||||
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||
const char* funcName) {
|
||||
if (size <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
std::format("size ({}) must be greater than zero.", size)));
|
||||
return false;
|
||||
}
|
||||
if (offset < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
std::format("offset ({}) must not be negative.", offset)));
|
||||
return false;
|
||||
}
|
||||
// GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own
|
||||
// implementation-defined alignment, which glGetIntegerv already answers.
|
||||
GLenum alignmentQuery = GL_NONE;
|
||||
if (target == GL_SHADER_STORAGE_BUFFER) {
|
||||
alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT;
|
||||
} else if (target == GL_UNIFORM_BUFFER) {
|
||||
alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT;
|
||||
}
|
||||
if (alignmentQuery != GL_NONE) {
|
||||
GLint alignment = 0;
|
||||
GetIntegerv(alignmentQuery, &alignment);
|
||||
if (alignment > 0 && (offset % static_cast<GLintptr>(alignment)) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("offset ({}) must be a multiple of {} ({}).", offset,
|
||||
MG_Util::ConvertGLEnumToString(alignmentQuery), alignment)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
||||
// offset and the size must be multiples of 4.
|
||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
||||
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
||||
offset, size)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||
MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
|
||||
MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1665,15 +1722,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
|
||||
// points (with buffer 0 resetting the binding point).
|
||||
// points (with buffer 0 resetting the binding point) - but only AFTER an up-front check
|
||||
// of the whole [first, first + count) range. Looping straight into the single-bind entry
|
||||
// points reports the single-bind INVALID_VALUE for an out-of-range index instead of the
|
||||
// multi-bind INVALID_OPERATION, and binds the in-range prefix before failing.
|
||||
static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false;
|
||||
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
||||
}
|
||||
|
||||
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The (offset, size) constraints are the one part of glBindBuffersRange that stays
|
||||
// per-element: ARB_multi_bind checks them separately for each binding point, leaves that
|
||||
// point unchanged on failure, and still applies the remaining elements - which is exactly
|
||||
// what looping into BindBufferRange_State does. Only the [first, first + count) range is
|
||||
// an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array)
|
||||
// reset the binding point through BindBufferBase_State and carry no offset/size to check.
|
||||
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||
const GLsizeiptr* sizes) {
|
||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!buffers || buffers[i] == 0) {
|
||||
BindBufferBase_State(target, first + i, 0);
|
||||
|
||||
@@ -53,18 +53,46 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The GL-visible number of indexed binding points for `target`.
|
||||
SizeT GetBufferBindingPointLimit(BufferTarget target) {
|
||||
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
||||
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
||||
const Int backendCount =
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||
}
|
||||
if (target == BufferTarget::TransformFeedback) {
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||
pointCount = std::min<SizeT>(pointCount, 4);
|
||||
}
|
||||
return pointCount;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", funcName,
|
||||
"count must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(pointCount)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl/BufferImpl", funcName,
|
||||
std::format("first + count ({} + {}) exceeds the {} indexed binding points of target {}.", first,
|
||||
count, pointCount, MG_Util::ConvertBufferTargetToString(target))));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
||||
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
||||
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
||||
const Int backendCount =
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||
}
|
||||
if (target == BufferTarget::TransformFeedback) {
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||
pointCount = std::min<SizeT>(pointCount, 4);
|
||||
}
|
||||
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||
|
||||
if (index < pointCount) {
|
||||
return true;
|
||||
|
||||
@@ -17,4 +17,8 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
|
||||
Bool ValidateBufferBindingPointTarget(BufferTarget target);
|
||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
|
||||
// ARB_multi_bind: glBindBuffersBase/Range validate the whole [first, first + count) range
|
||||
// up front and report INVALID_OPERATION, where a single out-of-range index would be
|
||||
// INVALID_VALUE. Naively looping the single-bind entry points reports the wrong class.
|
||||
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
|
||||
|
||||
@@ -474,6 +474,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
// GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT
|
||||
// for its dimension. GetIntegeri_v already floors that at the spec minimum.
|
||||
const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ};
|
||||
for (GLuint dimension = 0; dimension < 3; ++dimension) {
|
||||
GLint maxGroups = 0;
|
||||
GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups);
|
||||
if (numGroups[dimension] > static_cast<GLuint>(std::max(maxGroups, 0))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " +
|
||||
std::to_string(dimension) + "."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
@@ -487,6 +502,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
||||
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||
if (indirect < 0 || (indirect % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"indirect must be non-negative and a multiple of 4."));
|
||||
return;
|
||||
}
|
||||
const auto& indirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
|
||||
if (!indirectBuffer) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||
return;
|
||||
}
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
|
||||
@@ -977,8 +977,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GL
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||
|
||||
@@ -51,6 +51,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
||||
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
||||
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
||||
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
||||
// limits they advertise still have to be legal.
|
||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||
@@ -264,6 +272,60 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL_TEXTURE_BINDING_* is per-texture-unit state: glGetIntegerv answers for the
|
||||
// active unit, glGetIntegeri_v answers for unit `index`. Both need the same
|
||||
// pname -> target decode, so it lives here instead of being spelled out twice.
|
||||
bool TryDecodeTextureUnitBindingPname(GLenum pname, TextureTarget& outTarget) {
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_BINDING_1D: outTarget = TextureTarget::Texture1D; return true;
|
||||
case GL_TEXTURE_BINDING_1D_ARRAY: outTarget = TextureTarget::Texture1DArray; return true;
|
||||
case GL_TEXTURE_BINDING_2D: outTarget = TextureTarget::Texture2D; return true;
|
||||
case GL_TEXTURE_BINDING_2D_ARRAY: outTarget = TextureTarget::Texture2DArray; return true;
|
||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: outTarget = TextureTarget::Texture2DMultisample; return true;
|
||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY:
|
||||
outTarget = TextureTarget::Texture2DMultisampleArray;
|
||||
return true;
|
||||
case GL_TEXTURE_BINDING_3D: outTarget = TextureTarget::Texture3D; return true;
|
||||
case GL_TEXTURE_BINDING_BUFFER: outTarget = TextureTarget::TextureBuffer; return true;
|
||||
case GL_TEXTURE_BINDING_CUBE_MAP: outTarget = TextureTarget::TextureCubeMap; return true;
|
||||
case GL_TEXTURE_BINDING_CUBE_MAP_ARRAY: outTarget = TextureTarget::TextureCubeMapArray; return true;
|
||||
case GL_TEXTURE_BINDING_RECTANGLE: outTarget = TextureTarget::TextureRectangle; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
GLint QueryTextureBindingOnUnit(Int unit, TextureTarget target) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& obj = textureUnit.GetBindingSlot(target).GetBoundObject();
|
||||
return obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
GLint QuerySamplerBindingOnUnit(Int unit) {
|
||||
const auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& sampler = textureUnit.GetSamplerObject();
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -671,7 +733,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-texture-unit bindings: GL 4.6 core table 23.19 makes every GL_TEXTURE_BINDING_*
|
||||
// and GL_SAMPLER_BINDING indexed by texture unit. Without this they fell through to
|
||||
// the raw backend passthrough at the bottom, which knows nothing about the
|
||||
// frontend's binding state.
|
||||
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||
TryDecodeTextureUnitBindingPname(target, textureBindingTarget) || target == GL_SAMPLER_BINDING) {
|
||||
GLint maxUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
|
||||
maxUnits = std::min<GLint>(maxUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
if (index >= static_cast<GLuint>(std::max(maxUnits, 0))) {
|
||||
*data = 0;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture unit index is out of range."));
|
||||
return;
|
||||
}
|
||||
*data = target == GL_SAMPLER_BINDING
|
||||
? QuerySamplerBindingOnUnit(static_cast<Int>(index))
|
||||
: QueryTextureBindingOnUnit(static_cast<Int>(index), textureBindingTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
return;
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
@@ -783,6 +875,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
getIntegeri(target, index, data);
|
||||
}
|
||||
|
||||
// GL_ARB_viewport_array's typed indexed getters. They were no-op stubs, which left the
|
||||
// caller's output buffer holding whatever was on the stack. The multi-component indexed
|
||||
// rectangles are answered from the frontend's own viewport/scissor/depth-range state, via
|
||||
// the non-indexed getter of the matching type - GL_DEPTH_RANGE is float state, so putting
|
||||
// it through the integer query would round it to 0/1. Everything else MobileGL answers
|
||||
// indexed is scalar integer-domain state, where converting the integer query is exact.
|
||||
void GetFloati_v(GLenum target, GLuint index, GLfloat* data) {
|
||||
if (!data) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||
return;
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
GetIntegeri_v(target, index, ints);
|
||||
data[0] = static_cast<GLfloat>(ints[0]);
|
||||
}
|
||||
|
||||
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data) {
|
||||
if (!data) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||
return;
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
GetIntegeri_v(target, index, ints);
|
||||
data[0] = static_cast<GLdouble>(ints[0]);
|
||||
}
|
||||
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
||||
if (!data) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -959,6 +1091,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-texture-unit bindings: the non-indexed query reports the active unit.
|
||||
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||
TryDecodeTextureUnitBindingPname(pname, textureBindingTarget)) {
|
||||
*params = QueryTextureBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit(), textureBindingTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_ACTIVE_TEXTURE:
|
||||
*params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
|
||||
@@ -1085,11 +1224,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: 0;
|
||||
return;
|
||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||
*params = 0; // debug-group entrypoints are stubbed
|
||||
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||
*params = kFrontendMaxDebugGroupStackDepth;
|
||||
return;
|
||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||
return;
|
||||
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||
// Size of the message log ring; KHR_debug requires at least 1.
|
||||
*params = kFrontendMaxDebugLoggedMessages;
|
||||
return;
|
||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||
*params = 0; // debug-group entrypoints are stubbed
|
||||
return;
|
||||
@@ -1531,13 +1676,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_MASK_VALUE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||
return;
|
||||
case GL_SAMPLER_BINDING: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
const auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& sampler = tu.GetSamplerObject();
|
||||
*params = sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
case GL_SAMPLER_BINDING:
|
||||
*params = QuerySamplerBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
return;
|
||||
}
|
||||
case GL_SAMPLES:
|
||||
*params = ResolveDrawFramebufferSampleCount();
|
||||
return;
|
||||
@@ -1633,87 +1774,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_STEREO:
|
||||
*params = 0; // stereo surfaces are not exposed
|
||||
return;
|
||||
case GL_TEXTURE_BINDING_1D: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1D);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_1D_ARRAY: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1DArray);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_2D: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2D);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
MGLOG_D("Get GL_TEXTURE_BINDING_2D: %d", *params);
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_2D_ARRAY: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DArray);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisample);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisampleArray);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_3D: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture3D);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_BUFFER: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureBuffer);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_CUBE_MAP: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureCubeMap);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_BINDING_RECTANGLE: {
|
||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureRectangle);
|
||||
const auto& obj = slot.GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_TEXTURE_COMPRESSION_HINT:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
||||
return;
|
||||
@@ -1986,6 +2046,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
||||
break;
|
||||
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||
// state-query conversion rules.
|
||||
*params = static_cast<GLint>(std::min<Uint64>(dynamicParameters.MaxShaderStorageBlockSize,
|
||||
static_cast<Uint64>(INT32_MAX)));
|
||||
break;
|
||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||
break;
|
||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||
break;
|
||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxTextureBufferSize;
|
||||
break;
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetIntegerv(GLenum pname, GLint* params);
|
||||
void GetInteger64v(GLenum pname, GLint64* params);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetFloati_v(GLenum target, GLuint index, GLfloat* data);
|
||||
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Program.h"
|
||||
#include "ProgramInterface.h"
|
||||
#include "Config.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
@@ -109,32 +110,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return programObject;
|
||||
}
|
||||
|
||||
static bool IsProgramInterfaceEnum(GLenum programInterface) {
|
||||
switch (programInterface) {
|
||||
case GL_UNIFORM:
|
||||
case GL_UNIFORM_BLOCK:
|
||||
case GL_PROGRAM_INPUT:
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
case GL_BUFFER_VARIABLE:
|
||||
case GL_SHADER_STORAGE_BLOCK:
|
||||
case GL_ATOMIC_COUNTER_BUFFER:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||
case GL_VERTEX_SUBROUTINE:
|
||||
case GL_TESS_CONTROL_SUBROUTINE:
|
||||
case GL_TESS_EVALUATION_SUBROUTINE:
|
||||
case GL_GEOMETRY_SUBROUTINE:
|
||||
case GL_FRAGMENT_SUBROUTINE:
|
||||
case GL_COMPUTE_SUBROUTINE:
|
||||
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
// The four non-location interface queries validate the NAME only: GL 4.6 imposes the
|
||||
// successful-link requirement on GetProgramResourceLocation/LocationIndex alone, and
|
||||
// requires the others to report a program that has never linked as one with zero active
|
||||
// resources. Being stricter leaves a stray GL_INVALID_OPERATION behind that aborts the
|
||||
// caller's next subcase.
|
||||
static const SharedPtr<MG_State::GLState::ProgramObject>& TryToGetProgramForInterfaceQuery(GLuint program,
|
||||
const char* caller) {
|
||||
static const SharedPtr<MG_State::GLState::ProgramObject> nullProgramObject = nullptr;
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
std::to_string(program) + " is not a program object."));
|
||||
return nullProgramObject;
|
||||
}
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
if (!programObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
std::to_string(program) + " is not a program object."));
|
||||
return nullProgramObject;
|
||||
}
|
||||
return programObject;
|
||||
}
|
||||
|
||||
static bool IsProgramInterfaceEnum(GLenum programInterface) {
|
||||
return ProgramInterface::IsInterfaceEnum(programInterface);
|
||||
}
|
||||
|
||||
static bool IsSubroutineUniformInterface(GLenum programInterface) {
|
||||
@@ -157,11 +163,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_ACTIVE_RESOURCES:
|
||||
break;
|
||||
case GL_MAX_NAME_LENGTH:
|
||||
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER;
|
||||
// Neither buffer interface has resource names. GL_TRANSFORM_FEEDBACK_BUFFER only
|
||||
// became reachable here when IsInterfaceEnum grew the GL 4.4 interfaces, so it
|
||||
// needs the same exclusion GL_ATOMIC_COUNTER_BUFFER already had.
|
||||
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
|
||||
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
break;
|
||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
||||
valid = programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||
programInterface == GL_SHADER_STORAGE_BLOCK;
|
||||
programInterface == GL_SHADER_STORAGE_BLOCK ||
|
||||
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
break;
|
||||
case GL_MAX_NUM_COMPATIBLE_SUBROUTINES:
|
||||
valid = IsSubroutineUniformInterface(programInterface);
|
||||
@@ -180,7 +191,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
static bool ValidateNamedProgramResourceInterface(GLenum programInterface, const char* caller) {
|
||||
if (!IsProgramInterfaceEnum(programInterface) || programInterface == GL_ATOMIC_COUNTER_BUFFER) {
|
||||
if (!ProgramInterface::IsNamedInterface(programInterface)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
@@ -190,66 +201,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Int GetKnownProgramResourceCount(const SharedPtr<MG_State::GLState::ProgramObject>& programObject,
|
||||
GLenum programInterface) {
|
||||
switch (programInterface) {
|
||||
case GL_UNIFORM:
|
||||
return programObject->GetUniformCount();
|
||||
case GL_UNIFORM_BLOCK:
|
||||
return programObject->GetActiveUniformBlocksCount();
|
||||
case GL_PROGRAM_INPUT:
|
||||
return programObject->GetActiveAttributesCount();
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
return programObject->GetActiveFragmentOutputCount();
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// The GL_UNIFORM interface and glGetActiveUniform(s)iv are the same query in two
|
||||
// spellings, so they answer from the same place - the frontend reflection. The backend
|
||||
// program is not that place: it does not exist at all for a program whose types its
|
||||
// shading language cannot express (a double-precision uniform has no ESSL form), and
|
||||
// the interface queries would then describe a program with no uniforms.
|
||||
//
|
||||
// Writes the GL_UNIFORM value of `prop` for active uniform `index`; false for a prop
|
||||
// the reflection does not model, which the caller forwards to the backend instead.
|
||||
Bool GetUniformResourceProp(const SharedPtr<MG_State::GLState::ProgramObject>& programObject, Uint index,
|
||||
GLenum prop, GLint* out) {
|
||||
switch (prop) {
|
||||
case GL_TYPE:
|
||||
*out = static_cast<GLint>(programObject->GetActiveUniformType(index));
|
||||
return true;
|
||||
case GL_ARRAY_SIZE:
|
||||
*out = programObject->GetActiveUniformArraySize(index);
|
||||
return true;
|
||||
case GL_NAME_LENGTH:
|
||||
*out = static_cast<GLint>(programObject->GetActiveUniformName(index).length() + 1);
|
||||
return true;
|
||||
case GL_BLOCK_INDEX:
|
||||
*out = programObject->GetActiveUniformBlockIndex(index);
|
||||
return true;
|
||||
case GL_OFFSET:
|
||||
*out = programObject->GetActiveUniformOffset(index);
|
||||
return true;
|
||||
case GL_ARRAY_STRIDE:
|
||||
*out = programObject->GetActiveUniformArrayStride(index);
|
||||
return true;
|
||||
case GL_MATRIX_STRIDE:
|
||||
*out = programObject->GetActiveUniformMatrixStride(index);
|
||||
return true;
|
||||
case GL_IS_ROW_MAJOR:
|
||||
*out = programObject->GetActiveUniformIsRowMajor(index);
|
||||
return true;
|
||||
case GL_LOCATION:
|
||||
// A block member has no location; GetUniformLocation already reports -1 for one.
|
||||
*out = programObject->GetUniformLocation(programObject->GetActiveUniformName(index));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
|
||||
if (bufSize <= 0) {
|
||||
if (length) *length = 0;
|
||||
@@ -409,14 +360,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLuint CreateShader_State(GLenum type) {
|
||||
auto shaderId = MG_State::pGLContext->CreateShader(MG_Util::ConvertGLEnumToShaderStage(type));
|
||||
if (shaderId == 0) {
|
||||
// GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM (it
|
||||
// used to be documented as INVALID_VALUE). The check has to happen HERE: the state
|
||||
// layer hands out a name for ShaderStage::Unknown just as happily as for a real
|
||||
// stage, so the old "shaderId == 0 means bad type" test could never fire and an
|
||||
// unknown shaderType silently produced a usable shader name and no error at all.
|
||||
const ShaderStage stage = MG_Util::ConvertGLEnumToShaderStage(type);
|
||||
if (stage == ShaderStage::Unknown) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "`shaderType` is not an accepted value."));
|
||||
return 0;
|
||||
}
|
||||
return shaderId;
|
||||
return MG_State::pGLContext->CreateShader(stage);
|
||||
}
|
||||
|
||||
void DeleteProgram_State(GLuint program) {
|
||||
@@ -855,6 +811,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return loc;
|
||||
}
|
||||
|
||||
// A float matrix lives in the global UBO under std140 rules - one 16-byte-aligned column
|
||||
// vector per column - while the value glGetUniform* must return is tightly packed
|
||||
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
|
||||
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
|
||||
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
|
||||
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||
const Int columns = ttype->getMatrixCols();
|
||||
const Int rows = ttype->getMatrixRows();
|
||||
for (Int column = 0; column < columns; ++column) {
|
||||
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
||||
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||
// everything except a float matrix, whose padded columns make it wider.
|
||||
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||
if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) {
|
||||
return static_cast<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
|
||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
@@ -885,23 +866,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = (char*)programObject->MapUBO();
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
|
||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
else {
|
||||
// TODO: we only deal with mat3 yet, deal with other types later
|
||||
// assuming float here, which may not be the case
|
||||
auto* pBase = pUBO + offset;
|
||||
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
||||
Memcpy((char*)params + ttype->getMatrixCols() * sizeof(float) * i, pBase + 4 * sizeof(float) * i,
|
||||
ttype->getMatrixCols() * sizeof(float));
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: handle 1i variant as texture unit
|
||||
@@ -940,23 +914,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<T, GLfloat>) {
|
||||
if (ttype->getBasicType() != glslang::EbtDouble && ttype->isMatrix() &&
|
||||
ttype->getMatrixCols() == 3) {
|
||||
auto* pBase = pUBO + offset;
|
||||
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
||||
Memcpy(reinterpret_cast<char*>(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i,
|
||||
pBase + 4 * sizeof(GLfloat) * i, ttype->getMatrixCols() * sizeof(GLfloat));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||
}
|
||||
|
||||
// A double-precision uniform is the one case where the stored component type can
|
||||
@@ -1257,6 +1224,52 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
|
||||
// A float matrix sits in the global UBO under std140 rules: each of its `columns`
|
||||
// column vectors starts on its own 16-byte boundary no matter how many rows it has, so
|
||||
// the only shape that may be written as one contiguous block is mat4. Writing a matNxM
|
||||
// as N*M packed floats puts every column after the first at the wrong byte offset.
|
||||
template <typename Program>
|
||||
void UniformMatrixfv_Object(Program& programObject, const char* caller, GLint location, GLsizei count,
|
||||
GLboolean transpose, const GLfloat* value, Int columns, Int rows,
|
||||
const String& ownerDescription) {
|
||||
// std140: a column vector of a float matrix is padded out to a vec4.
|
||||
constexpr SizeT kColumnStride = 4 * sizeof(GLfloat);
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
GLfloat column[4] = {};
|
||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) {
|
||||
// GL 3.3 2.11.4: values for elements beyond the end of the uniform array
|
||||
// are ignored. Never step onto a neighboring uniform's location.
|
||||
break;
|
||||
}
|
||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||
RecordInvalidUniformLocationError(caller, location + matrix, ownerDescription);
|
||||
return;
|
||||
}
|
||||
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||
return;
|
||||
}
|
||||
if (value == nullptr) return;
|
||||
const GLfloat* source = value + static_cast<SizeT>(matrix) * componentCount;
|
||||
for (Int c = 0; c < columns; ++c) {
|
||||
for (Int r = 0; r < rows; ++r) {
|
||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||
}
|
||||
const SizeT byteOffset = static_cast<SizeT>(c) * kColumnStride;
|
||||
switch (rows) {
|
||||
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
|
||||
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
|
||||
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to transpose a 2x2 matrix
|
||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||
// Input matrix is in column-major order (OpenGL default)
|
||||
@@ -1362,8 +1375,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void UniformMatrix2fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
// For 2x2 matrices, we have 4 elements per matrix
|
||||
// If transpose is GL_TRUE, we need to transpose the matrix data
|
||||
// A mat2 is NOT four contiguous floats in the global UBO: std140 pads each column
|
||||
// vector out to 16 bytes, so column 1 starts at byte 16, not byte 8.
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
@@ -1374,26 +1387,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
}
|
||||
if (transpose == GL_TRUE) {
|
||||
// Transpose the matrix before uploading
|
||||
GLfloat transposedMatrix[4];
|
||||
TransposeMatrix2x2(value + i * 4, transposedMatrix);
|
||||
Uniform_State<4>(*programObject, location + i, transposedMatrix);
|
||||
} else {
|
||||
// No transpose needed, directly copy the matrix data
|
||||
Uniform_State<4>(*programObject, location + i, value + i * 4);
|
||||
}
|
||||
}
|
||||
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||
"the current program object");
|
||||
}
|
||||
|
||||
void UniformMatrix3fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
@@ -1471,7 +1466,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count) {
|
||||
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value, Int columns, Int rows) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
@@ -1482,21 +1478,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(caller, location + i, "the current program object");
|
||||
return;
|
||||
}
|
||||
if (programObject->IsUniformOpaqueAtLocation(location + i)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement non-square matrix uniform uploads for non-opaque uniforms.
|
||||
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||
"the current program object");
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
@@ -1514,23 +1497,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
}
|
||||
if (transpose == GL_TRUE) {
|
||||
GLfloat transposedMatrix[4];
|
||||
TransposeMatrix2x2(value + i * 4, transposedMatrix);
|
||||
Uniform_State<4>(*programObject, location + i, transposedMatrix);
|
||||
} else {
|
||||
Uniform_State<4>(*programObject, location + i, value + i * 4);
|
||||
}
|
||||
}
|
||||
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
@@ -1605,7 +1573,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count) {
|
||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
|
||||
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
@@ -1619,21 +1588,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(caller, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
}
|
||||
if (programObject->IsUniformOpaqueAtLocation(location + i)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement non-square matrix uniform uploads for non-opaque uniforms.
|
||||
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
|
||||
GLuint GetUniformBlockIndex_State(GLuint program, const GLchar* uniformBlockName) {
|
||||
@@ -2442,27 +2398,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void UniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 3);
|
||||
}
|
||||
|
||||
void UniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 2);
|
||||
}
|
||||
|
||||
void UniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 4);
|
||||
}
|
||||
|
||||
void UniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 2);
|
||||
}
|
||||
|
||||
void UniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 4);
|
||||
}
|
||||
|
||||
void UniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
||||
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 3);
|
||||
}
|
||||
|
||||
void ProgramUniform1f(GLuint program, GLint location, GLfloat v0) {
|
||||
@@ -2587,32 +2543,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 3);
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 2);
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 4);
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 2);
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 4);
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 3);
|
||||
}
|
||||
|
||||
GLuint GetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName) {
|
||||
@@ -2649,171 +2605,160 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return;
|
||||
if (!ValidateProgramInterfaceivQuery(programInterface, pname)) return;
|
||||
auto getProgramInterfaceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramInterfaceiv;
|
||||
if (!getProgramInterfaceiv) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support program interface queries."));
|
||||
if (!params) return;
|
||||
switch (pname) {
|
||||
case GL_ACTIVE_RESOURCES:
|
||||
*params = ProgramInterface::GetActiveResourceCount(*programObject, programInterface);
|
||||
return;
|
||||
case GL_MAX_NAME_LENGTH:
|
||||
*params = ProgramInterface::GetMaxNameLength(*programObject, programInterface);
|
||||
return;
|
||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
||||
*params = ProgramInterface::GetMaxNumActiveVariables(*programObject, programInterface);
|
||||
return;
|
||||
default:
|
||||
// GL_MAX_NUM_COMPATIBLE_SUBROUTINES: the subroutine interfaces are always empty
|
||||
// here (glslang refuses `subroutine` when generating SPIR-V), so zero it is.
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
if (pname == GL_ACTIVE_RESOURCES) {
|
||||
*params = static_cast<GLint>(programObject->GetUniformCount());
|
||||
return;
|
||||
}
|
||||
if (pname == GL_MAX_NAME_LENGTH) {
|
||||
// Stored as the bare length; GL_MAX_NAME_LENGTH counts the terminator.
|
||||
*params = programObject->GetUniformMaxLength() + 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
getProgramInterfaceiv(program, programInterface, pname, params);
|
||||
}
|
||||
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return GL_INVALID_INDEX;
|
||||
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
|
||||
if (!name) return GL_INVALID_INDEX;
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
const Int uniformIndex = programObject->GetActiveUniformIndex(name);
|
||||
return uniformIndex < 0 ? GL_INVALID_INDEX : static_cast<GLuint>(uniformIndex);
|
||||
}
|
||||
auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
||||
if (!getProgramResourceIndex) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support program interface queries."));
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
GLuint index = getProgramResourceIndex(program, programInterface, name);
|
||||
const String resourceName = name;
|
||||
if (index == GL_INVALID_INDEX && resourceName.length() > 3 &&
|
||||
resourceName.compare(resourceName.length() - 3, 3, "[0]") == 0) {
|
||||
index = getProgramResourceIndex(program, programInterface,
|
||||
resourceName.substr(0, resourceName.length() - 3).c_str());
|
||||
}
|
||||
return index;
|
||||
return ProgramInterface::GetResourceIndex(*programObject, programInterface, name);
|
||||
}
|
||||
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
||||
GLchar* name) {
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return;
|
||||
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return;
|
||||
const Int resourceCount = GetKnownProgramResourceCount(programObject, programInterface);
|
||||
if (resourceCount >= 0 && index >= static_cast<GLuint>(resourceCount)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||
return;
|
||||
}
|
||||
if (bufSize < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
// Same index space GetProgramResourceIndex answers in, and the range check above
|
||||
// already used it.
|
||||
const String& uniformName = programObject->GetActiveUniformName(index);
|
||||
CopyStr(bufSize, length, name, uniformName.c_str(), static_cast<GLsizei>(uniformName.length()));
|
||||
return;
|
||||
}
|
||||
auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName;
|
||||
if (!getProgramResourceName) {
|
||||
String resourceName;
|
||||
if (!ProgramInterface::GetResourceName(*programObject, programInterface, index, resourceName)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support program interface queries."));
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||
return;
|
||||
}
|
||||
getProgramResourceName(program, programInterface, index, bufSize, length, name);
|
||||
CopyStr(bufSize, length, name, resourceName.c_str(), static_cast<GLsizei>(resourceName.length()));
|
||||
}
|
||||
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return;
|
||||
if (propCount < 0 || bufSize < 0) {
|
||||
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"propCount and bufSize must be non-negative."));
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Unsupported program interface."));
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
if (index >= programObject->GetUniformCount()) {
|
||||
if (propCount <= 0 || bufSize < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"propCount must be positive and bufSize "
|
||||
"non-negative."));
|
||||
return;
|
||||
}
|
||||
if (props == nullptr) return;
|
||||
// Both prop checks run BEFORE any value is produced: a property this command does
|
||||
// not know at all is INVALID_ENUM, one it knows but the interface does not carry is
|
||||
// INVALID_OPERATION (GL 4.6 Table 7.2). The two are deliberately different errors.
|
||||
for (GLsizei i = 0; i < propCount; ++i) {
|
||||
if (!ProgramInterface::IsResourceProp(props[i])) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "prop is not a valid property name."));
|
||||
return;
|
||||
}
|
||||
if (!ProgramInterface::InterfaceSupportsProp(programInterface, props[i])) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"prop is not supported for this program interface."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<GLint> values;
|
||||
for (GLsizei i = 0; i < propCount; ++i) {
|
||||
if (!ProgramInterface::GetResourceProp(*programObject, programInterface, index, props[i], values)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||
return;
|
||||
}
|
||||
if (props == nullptr || params == nullptr) return;
|
||||
GLsizei written = 0;
|
||||
for (GLsizei i = 0; i < propCount && written < bufSize; ++i) {
|
||||
GLint value = 0;
|
||||
if (!GetUniformResourceProp(programObject, index, props[i], &value)) {
|
||||
// GL_ATOMIC_COUNTER_BUFFER_INDEX and the GL_REFERENCED_BY_* stage props are
|
||||
// not modelled here; ask the backend, which indexes resources by name.
|
||||
auto backendGetIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
||||
auto backendGetiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
||||
if (backendGetIndex && backendGetiv) {
|
||||
const GLuint backendIndex = backendGetIndex(program, GL_UNIFORM,
|
||||
programObject->GetActiveUniformName(index).c_str());
|
||||
if (backendIndex != GL_INVALID_INDEX) {
|
||||
GLsizei one = 0;
|
||||
backendGetiv(program, GL_UNIFORM, backendIndex, 1, &props[i], 1, &one, &value);
|
||||
}
|
||||
}
|
||||
}
|
||||
params[written++] = value;
|
||||
}
|
||||
if (length) *length = written;
|
||||
return;
|
||||
}
|
||||
auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
||||
if (!getProgramResourceiv) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support program interface queries."));
|
||||
return;
|
||||
}
|
||||
getProgramResourceiv(program, programInterface, index, propCount, props, bufSize, length, params);
|
||||
if (params == nullptr) return;
|
||||
const GLsizei written = static_cast<GLsizei>(std::min<SizeT>(values.size(), static_cast<SizeT>(bufSize)));
|
||||
for (GLsizei i = 0; i < written; ++i) params[i] = values[i];
|
||||
if (length) *length = written;
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
// Unlike the four queries above, this one and GetProgramResourceLocationIndex really
|
||||
// do require a successful link (GL 4.6 §7.3.1.3).
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return -1;
|
||||
auto getProgramResourceLocation = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocation;
|
||||
if (!getProgramResourceLocation) {
|
||||
if (!ProgramInterface::InterfaceHasLocations(programInterface)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support program interface queries."));
|
||||
"Program interface has no locations."));
|
||||
return -1;
|
||||
}
|
||||
return getProgramResourceLocation(program, programInterface, name);
|
||||
return ProgramInterface::GetResourceLocation(*programObject, programInterface, name);
|
||||
}
|
||||
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return -1;
|
||||
auto getProgramResourceLocationIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocationIndex;
|
||||
if (!getProgramResourceLocationIndex) return -1;
|
||||
return getProgramResourceLocationIndex(program, programInterface, name);
|
||||
if (programInterface != GL_PROGRAM_OUTPUT) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"GetProgramResourceLocationIndex only accepts GL_PROGRAM_OUTPUT."));
|
||||
return -1;
|
||||
}
|
||||
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
||||
}
|
||||
|
||||
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
||||
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
||||
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
||||
// space the application sees gets turned into whatever the backend's is; the backends are
|
||||
// handed the block NAME and do their own lookup. Getting this wrong is silent: the call
|
||||
// succeeds and rebinds a DIFFERENT buffer.
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject || !programObject->GetLinkStatus()) return;
|
||||
if (!ValidateShaderStorageBlockBinding(storageBlockBinding)) return;
|
||||
String blockName;
|
||||
if (!ProgramInterface::GetResourceName(*programObject, GL_SHADER_STORAGE_BLOCK, storageBlockIndex,
|
||||
blockName)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"storageBlockIndex is not an active shader storage block index."));
|
||||
return;
|
||||
}
|
||||
// Recorded before the backend call, and independently of whether a backend is even
|
||||
// present: this is the state GL_BUFFER_BINDING reports, and it is also what reseeds a
|
||||
// backend's own reflection cache after any rebuild.
|
||||
programObject->SetShaderStorageBlockBinding(blockName, storageBlockBinding);
|
||||
auto shaderStorageBlockBinding = MG_Backend::gBackendFunctionsTable.GL.ShaderStorageBlockBinding;
|
||||
if (!shaderStorageBlockBinding) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -2822,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) {
|
||||
@@ -2859,6 +2804,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// is written as that sequence rather than as a private shortcut - every error it can
|
||||
// raise is one of theirs, raised at the point they would raise it.
|
||||
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) {
|
||||
// GL 4.6 core 7.3: a negative count is INVALID_VALUE and is checked before anything
|
||||
// is created, so a bad count never leaks a shader name. An unrecognised type is
|
||||
// INVALID_ENUM, which CreateShader_State raises below.
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint shader = CreateShader_State(type);
|
||||
if (shader == 0) return 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,841 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "ProgramInterface.h"
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
namespace {
|
||||
// glslang folds atomic counters into synthesized blocks named
|
||||
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
||||
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
||||
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
||||
|
||||
enum class BlockKind {
|
||||
Uniform, // a real GL uniform block
|
||||
GlobalUbo, // the synthesized MGL_GLOBAL_UBO: GL sees its members as default-block
|
||||
AtomicCounter, // gl_AtomicCounterBlock_<binding>
|
||||
Storage, // a shader storage block
|
||||
};
|
||||
|
||||
// One row of any interface. Fields a given interface does not have keep the
|
||||
// spec-mandated "not applicable" value, so a prop read never has to special-case
|
||||
// the interface a second time.
|
||||
struct Resource {
|
||||
String name;
|
||||
GLenum type = GL_NONE;
|
||||
GLint arraySize = 1;
|
||||
GLint location = -1;
|
||||
GLint locationIndex = -1;
|
||||
GLint blockIndex = -1;
|
||||
GLint offset = -1;
|
||||
GLint arrayStride = -1;
|
||||
GLint matrixStride = -1;
|
||||
GLint isRowMajor = 0;
|
||||
GLint atomicCounterBufferIndex = -1;
|
||||
GLint topLevelArraySize = 0;
|
||||
GLint topLevelArrayStride = 0;
|
||||
GLint bufferBinding = 0;
|
||||
GLint bufferDataSize = 0;
|
||||
GLint isPerPatch = 0;
|
||||
GLint xfbBufferIndex = 0;
|
||||
Uint32 stages = 0; // EShLanguageMask
|
||||
Vector<GLuint> activeVariables;
|
||||
};
|
||||
|
||||
using ResourceList = Vector<Resource>;
|
||||
|
||||
struct Model {
|
||||
ResourceList uniforms;
|
||||
ResourceList uniformBlocks;
|
||||
ResourceList atomicCounterBuffers;
|
||||
ResourceList bufferVariables;
|
||||
ResourceList storageBlocks;
|
||||
ResourceList programInputs;
|
||||
ResourceList programOutputs;
|
||||
ResourceList xfbVaryings;
|
||||
Bool valid = false;
|
||||
};
|
||||
|
||||
const ResourceList& EmptyList() {
|
||||
static const ResourceList empty;
|
||||
return empty;
|
||||
}
|
||||
|
||||
// ---- name spelling (cluster 6) -------------------------------------------------
|
||||
|
||||
Bool EndsWithZeroSubscript(const String& name) {
|
||||
return name.length() >= 3 && name.compare(name.length() - 3, 3, "[0]") == 0;
|
||||
}
|
||||
|
||||
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||
// stage inputs/outputs, so those get it here.
|
||||
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
||||
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
||||
return name + "[0]";
|
||||
}
|
||||
|
||||
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
||||
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
||||
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
||||
if (type != nullptr && type->isArray()) {
|
||||
if (!type->isSizedArray()) return 0;
|
||||
return type->getOuterArraySize();
|
||||
}
|
||||
return reflectedSize < 1 ? 1 : reflectedSize;
|
||||
}
|
||||
|
||||
// Two spellings name the same resource when they are equal, or differ only by the
|
||||
// "[0]" the enumeration appends to an array.
|
||||
Bool NamesMatch(const String& resourceName, const String& query) {
|
||||
if (resourceName == query) return true;
|
||||
if (EndsWithZeroSubscript(resourceName) &&
|
||||
resourceName.compare(0, resourceName.length() - 3, query) == 0) {
|
||||
return true;
|
||||
}
|
||||
return EndsWithZeroSubscript(query) && query.compare(0, query.length() - 3, resourceName) == 0;
|
||||
}
|
||||
|
||||
// Splits "base[k]" into ("base", k). GL 4.6 §7.3.1.1 requires the subscript to be a
|
||||
// decimal integer with no white space and no leading zeros, which is exactly what
|
||||
// separates array-names' "a[1]" (resolves) from "a[01]", "a[0 + 0]" and "a[ 0]" (do
|
||||
// not). Returns false when there is no trailing subscript at all; sets `malformed`
|
||||
// when there is one but it is not a strict decimal.
|
||||
Bool SplitTrailingSubscript(const String& name, String& outBase, Uint& outElement, Bool& outMalformed) {
|
||||
outMalformed = false;
|
||||
if (name.empty() || name.back() != ']') return false;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos) return false;
|
||||
const SizeT first = bracket + 1;
|
||||
const SizeT last = name.length() - 1; // one past the digits
|
||||
if (first >= last) {
|
||||
outMalformed = true;
|
||||
return false;
|
||||
}
|
||||
// No leading zeros: "0" is the only spelling that may start with '0'.
|
||||
if (name[first] == '0' && last - first > 1) {
|
||||
outMalformed = true;
|
||||
return false;
|
||||
}
|
||||
Uint element = 0;
|
||||
for (SizeT i = first; i < last; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') {
|
||||
outMalformed = true;
|
||||
return false;
|
||||
}
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0FFFFFFFu) {
|
||||
outMalformed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
outBase = name.substr(0, bracket);
|
||||
outElement = element;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- block classification ------------------------------------------------------
|
||||
|
||||
Bool IsAtomicCounterBlockName(const String& name) {
|
||||
return name.compare(0, std::strlen(kAtomicCounterBlockPrefix), kAtomicCounterBlockPrefix) == 0;
|
||||
}
|
||||
|
||||
// "gl_AtomicCounterBlock_5" -> 5. The suffix is the GL binding the counters were
|
||||
// declared with, which glslang does NOT keep in the block's own layout qualifier
|
||||
// (that one is remapped to a plain buffer binding).
|
||||
GLint AtomicCounterBlockBinding(const String& name) {
|
||||
const SizeT underscore = name.rfind('_');
|
||||
if (underscore == String::npos || underscore + 1 >= name.length()) return 0;
|
||||
GLint binding = 0;
|
||||
for (SizeT i = underscore + 1; i < name.length(); ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return 0;
|
||||
binding = binding * 10 + (name[i] - '0');
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
// Element index of an arrayed block instance ("TrickyBuffer[1]" -> 1).
|
||||
GLint BlockArrayElement(const String& name) {
|
||||
String base;
|
||||
Uint element = 0;
|
||||
Bool malformed = false;
|
||||
if (!SplitTrailingSubscript(name, base, element, malformed)) return 0;
|
||||
return static_cast<GLint>(element);
|
||||
}
|
||||
|
||||
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
||||
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||
return BlockKind::GlobalUbo;
|
||||
}
|
||||
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||
const glslang::TType* type = block.getType();
|
||||
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
||||
return BlockKind::Uniform;
|
||||
}
|
||||
|
||||
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||
// uniform matrices. 0 for a non-matrix.
|
||||
GLint MatrixStrideOf(const glslang::TType* type) {
|
||||
if (type == nullptr || !type->isMatrix()) return 0;
|
||||
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
||||
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
||||
constexpr int scalarSize = 4;
|
||||
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||
: 4 * scalarSize;
|
||||
return (vectorAlignment + 15) & ~15;
|
||||
}
|
||||
|
||||
GLint IsRowMajorOf(const glslang::TType* type) {
|
||||
if (type == nullptr || !type->isMatrix()) return 0;
|
||||
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
||||
}
|
||||
|
||||
GLint MappedLocation(Int rawLocation) {
|
||||
// glslang parks "no location" at layoutLocationEnd; GL spells it -1.
|
||||
if (rawLocation < 0 || rawLocation >= static_cast<Int>(glslang::TQualifier::layoutLocationEnd)) return -1;
|
||||
return rawLocation;
|
||||
}
|
||||
|
||||
// ---- model construction --------------------------------------------------------
|
||||
|
||||
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||
blockInterfaceIndex.assign(blockCount, -1);
|
||||
|
||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||
const BlockKind kind = ClassifyBlock(block);
|
||||
blockKind[tIndex] = kind;
|
||||
if (kind == BlockKind::AtomicCounter) {
|
||||
Resource resource;
|
||||
// GL_ATOMIC_COUNTER_BUFFER resources have no name (and GetProgramResource
|
||||
// Index/Name reject the interface outright, which is why this stays empty).
|
||||
resource.bufferBinding = AtomicCounterBlockBinding(block.name);
|
||||
resource.bufferDataSize = block.size;
|
||||
resource.stages = static_cast<Uint32>(block.stages);
|
||||
blockInterfaceIndex[tIndex] = static_cast<Int>(model.atomicCounterBuffers.size());
|
||||
model.atomicCounterBuffers.push_back(Move(resource));
|
||||
} else if (kind == BlockKind::Storage) {
|
||||
Resource resource;
|
||||
resource.name = block.name;
|
||||
// glslang reports the DECLARED binding for every instance of an arrayed
|
||||
// block; GL gives element k the binding base + k. That is only the initial
|
||||
// value: GL_BUFFER_BINDING must report the CURRENT binding, so a later
|
||||
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||
// GetUniformBlockBinding below).
|
||||
const GLint declared = block.getBinding();
|
||||
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||
resource.bufferDataSize = block.size;
|
||||
resource.stages = static_cast<Uint32>(block.stages);
|
||||
blockInterfaceIndex[tIndex] = static_cast<Int>(model.storageBlocks.size());
|
||||
model.storageBlocks.push_back(Move(resource));
|
||||
}
|
||||
}
|
||||
|
||||
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||
// with them (which is exactly what the CTS does).
|
||||
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
||||
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||
Resource resource;
|
||||
resource.name = program.GetUniformBlockName(glIndex);
|
||||
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||
if (tIndex >= 0 && tIndex < blockCount) {
|
||||
resource.stages =
|
||||
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
|
||||
}
|
||||
model.uniformBlocks.push_back(Move(resource));
|
||||
}
|
||||
}
|
||||
|
||||
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||
const Vector<BlockKind>& blockKind,
|
||||
const Vector<Int>& blockInterfaceIndex) {
|
||||
const Uint uniformCount = program.GetUniformCount();
|
||||
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
||||
const glslang::TType* type = refl.getType();
|
||||
const Int owner = refl.index;
|
||||
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||
? blockKind[owner]
|
||||
: BlockKind::GlobalUbo;
|
||||
|
||||
Resource resource;
|
||||
resource.name = refl.name;
|
||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||
resource.stages = static_cast<Uint32>(refl.stages);
|
||||
|
||||
if (kind == BlockKind::Storage) {
|
||||
resource.blockIndex = blockInterfaceIndex[owner];
|
||||
resource.offset = refl.offset;
|
||||
resource.arrayStride = refl.arrayStride;
|
||||
resource.matrixStride = MatrixStrideOf(type);
|
||||
resource.isRowMajor = IsRowMajorOf(type);
|
||||
// GL requires 1 for a member that is not inside a top-level array (and for
|
||||
// the top-level array itself); glslang leaves 0/-1 there.
|
||||
resource.topLevelArraySize = refl.topLevelArraySize > 0 ? refl.topLevelArraySize : 1;
|
||||
resource.topLevelArrayStride = refl.topLevelArrayStride;
|
||||
model.bufferVariables.push_back(Move(resource));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind == BlockKind::AtomicCounter) {
|
||||
// An atomic counter is a default-block uniform with no location and no
|
||||
// owning uniform block; what it does have is a buffer to point at.
|
||||
resource.type = GL_UNSIGNED_INT_ATOMIC_COUNTER;
|
||||
resource.blockIndex = -1;
|
||||
resource.offset = refl.offset;
|
||||
resource.arrayStride = refl.arrayStride;
|
||||
resource.matrixStride = 0;
|
||||
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||
resource.location = -1;
|
||||
} else {
|
||||
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
||||
resource.offset = program.GetActiveUniformOffset(glIndex);
|
||||
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
||||
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
||||
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
||||
// A member of a named uniform block has no location, whatever the
|
||||
// frontend's own location table says (it hands one out to every uniform
|
||||
// so glUniform* can address block members through the global UBO).
|
||||
resource.location =
|
||||
resource.blockIndex >= 0 ? -1 : program.GetUniformLocation(refl.name);
|
||||
}
|
||||
model.uniforms.push_back(Move(resource));
|
||||
}
|
||||
|
||||
// GL_ACTIVE_VARIABLES, both directions.
|
||||
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||
const Resource& uniform = model.uniforms[i];
|
||||
if (uniform.atomicCounterBufferIndex >= 0 &&
|
||||
uniform.atomicCounterBufferIndex < static_cast<GLint>(model.atomicCounterBuffers.size())) {
|
||||
model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back(
|
||||
static_cast<GLuint>(i));
|
||||
}
|
||||
}
|
||||
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
||||
// Members of an arrayed block are reflected once, against instance [0].
|
||||
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
||||
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||
if (model.uniforms[i].blockIndex == owner) {
|
||||
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (SizeT blockIndex = 0; blockIndex < model.storageBlocks.size(); ++blockIndex) {
|
||||
for (SizeT i = 0; i < model.bufferVariables.size(); ++i) {
|
||||
if (model.bufferVariables[i].blockIndex == static_cast<GLint>(blockIndex)) {
|
||||
model.storageBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||
|
||||
const Int inputCount = mutableReflection.getNumPipeInputs();
|
||||
for (Int index = 0; index < inputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeInput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
Resource resource;
|
||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||
// names; GL enumerates the GL spellings.
|
||||
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||
resource.name = WithArraySuffix(glName, type);
|
||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||
resource.location = program.GetAttributeLocation(refl.name);
|
||||
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||
resource.stages = static_cast<Uint32>(refl.stages);
|
||||
model.programInputs.push_back(Move(resource));
|
||||
}
|
||||
|
||||
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||
for (Int index = 0; index < outputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
Resource resource;
|
||||
resource.name = WithArraySuffix(refl.name, type);
|
||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||
if (resource.location < 0) {
|
||||
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
|
||||
// output both have no location, and therefore no color index either.
|
||||
resource.locationIndex = -1;
|
||||
} else {
|
||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||
// layout(index = N), which the frag-data maps never saw.
|
||||
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
||||
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
||||
}
|
||||
}
|
||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||
resource.stages = static_cast<Uint32>(refl.stages);
|
||||
model.programOutputs.push_back(Move(resource));
|
||||
}
|
||||
}
|
||||
|
||||
void BuildXfb(ProgramObject& program, Model& model) {
|
||||
const auto& requested = program.GetTransformFeedbackInterfaceNames();
|
||||
const auto& captured = program.GetTransformFeedbackVaryings();
|
||||
for (const String& name : requested) {
|
||||
Resource resource;
|
||||
resource.name = name;
|
||||
// ARB_transform_feedback3's layout controls are enumerated as resources of
|
||||
// type NONE: gl_NextBuffer with array size 0, gl_SkipComponentsN with N.
|
||||
if (name == "gl_NextBuffer") {
|
||||
resource.type = GL_NONE;
|
||||
resource.arraySize = 0;
|
||||
} else if (name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' &&
|
||||
name[17] <= '4') {
|
||||
resource.type = GL_NONE;
|
||||
resource.arraySize = name[17] - '0';
|
||||
} else {
|
||||
resource.type = GL_NONE;
|
||||
resource.arraySize = 1;
|
||||
for (const auto& varying : captured) {
|
||||
if (varying.name != name) continue;
|
||||
resource.type = varying.type;
|
||||
resource.arraySize = varying.size < 1 ? 1 : varying.size;
|
||||
resource.offset = static_cast<GLint>(varying.offsetBytes);
|
||||
resource.xfbBufferIndex = static_cast<GLint>(varying.bufferIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
model.xfbVaryings.push_back(Move(resource));
|
||||
}
|
||||
}
|
||||
|
||||
Model BuildModel(ProgramObject& program) {
|
||||
Model model;
|
||||
if (!program.GetLinkStatus()) return model;
|
||||
const glslang::TProgram* reflection = program.GetReflection();
|
||||
if (reflection == nullptr) return model;
|
||||
model.valid = true;
|
||||
|
||||
Vector<BlockKind> blockKind;
|
||||
Vector<Int> blockInterfaceIndex;
|
||||
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||
BuildStageIO(program, *reflection, model);
|
||||
BuildXfb(program, model);
|
||||
return model;
|
||||
}
|
||||
|
||||
const ResourceList& Select(const Model& model, GLenum programInterface) {
|
||||
switch (programInterface) {
|
||||
case GL_UNIFORM:
|
||||
return model.uniforms;
|
||||
case GL_UNIFORM_BLOCK:
|
||||
return model.uniformBlocks;
|
||||
case GL_ATOMIC_COUNTER_BUFFER:
|
||||
return model.atomicCounterBuffers;
|
||||
case GL_BUFFER_VARIABLE:
|
||||
return model.bufferVariables;
|
||||
case GL_SHADER_STORAGE_BLOCK:
|
||||
return model.storageBlocks;
|
||||
case GL_PROGRAM_INPUT:
|
||||
return model.programInputs;
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
return model.programOutputs;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||
return model.xfbVaryings;
|
||||
default:
|
||||
// The subroutine interfaces are accepted by the API but nothing can populate
|
||||
// them: glslang refuses `subroutine` when generating SPIR-V, so a program
|
||||
// using one never links. Zero active resources is the honest answer.
|
||||
return EmptyList();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool IsInterfaceEnum(GLenum programInterface) {
|
||||
switch (programInterface) {
|
||||
case GL_UNIFORM:
|
||||
case GL_UNIFORM_BLOCK:
|
||||
case GL_PROGRAM_INPUT:
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
case GL_BUFFER_VARIABLE:
|
||||
case GL_SHADER_STORAGE_BLOCK:
|
||||
case GL_ATOMIC_COUNTER_BUFFER:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER:
|
||||
case GL_VERTEX_SUBROUTINE:
|
||||
case GL_TESS_CONTROL_SUBROUTINE:
|
||||
case GL_TESS_EVALUATION_SUBROUTINE:
|
||||
case GL_GEOMETRY_SUBROUTINE:
|
||||
case GL_FRAGMENT_SUBROUTINE:
|
||||
case GL_COMPUTE_SUBROUTINE:
|
||||
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsNamedInterface(GLenum programInterface) {
|
||||
// GL 4.6 §7.3.1.2: the two buffer interfaces have no resource names, and asking for
|
||||
// one is INVALID_ENUM (deliberately asymmetric with GetProgramInterfaceiv, which
|
||||
// does count them).
|
||||
return IsInterfaceEnum(programInterface) && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
|
||||
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
}
|
||||
|
||||
Bool InterfaceHasLocations(GLenum programInterface) {
|
||||
switch (programInterface) {
|
||||
case GL_UNIFORM:
|
||||
case GL_PROGRAM_INPUT:
|
||||
case GL_PROGRAM_OUTPUT:
|
||||
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsResourceProp(GLenum prop) {
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
case GL_TYPE:
|
||||
case GL_ARRAY_SIZE:
|
||||
case GL_OFFSET:
|
||||
case GL_BLOCK_INDEX:
|
||||
case GL_ARRAY_STRIDE:
|
||||
case GL_MATRIX_STRIDE:
|
||||
case GL_IS_ROW_MAJOR:
|
||||
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||
case GL_BUFFER_BINDING:
|
||||
case GL_BUFFER_DATA_SIZE:
|
||||
case GL_NUM_ACTIVE_VARIABLES:
|
||||
case GL_ACTIVE_VARIABLES:
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||
case GL_LOCATION:
|
||||
case GL_LOCATION_INDEX:
|
||||
case GL_IS_PER_PATCH:
|
||||
case GL_LOCATION_COMPONENT:
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||
case GL_COMPATIBLE_SUBROUTINES:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 Table 7.2, transcribed row by row: which interfaces each property applies to.
|
||||
// Too tight a table turns a currently-answered prop into a fresh INVALID_OPERATION, so
|
||||
// the rows below are deliberately no narrower than the spec's.
|
||||
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop) {
|
||||
const Bool isSubroutine =
|
||||
programInterface == GL_VERTEX_SUBROUTINE || programInterface == GL_TESS_CONTROL_SUBROUTINE ||
|
||||
programInterface == GL_TESS_EVALUATION_SUBROUTINE || programInterface == GL_GEOMETRY_SUBROUTINE ||
|
||||
programInterface == GL_FRAGMENT_SUBROUTINE || programInterface == GL_COMPUTE_SUBROUTINE;
|
||||
const Bool isSubroutineUniform =
|
||||
programInterface == GL_VERTEX_SUBROUTINE_UNIFORM ||
|
||||
programInterface == GL_TESS_CONTROL_SUBROUTINE_UNIFORM ||
|
||||
programInterface == GL_TESS_EVALUATION_SUBROUTINE_UNIFORM ||
|
||||
programInterface == GL_GEOMETRY_SUBROUTINE_UNIFORM ||
|
||||
programInterface == GL_FRAGMENT_SUBROUTINE_UNIFORM || programInterface == GL_COMPUTE_SUBROUTINE_UNIFORM;
|
||||
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
return programInterface != GL_ATOMIC_COUNTER_BUFFER && programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
case GL_TYPE:
|
||||
case GL_ARRAY_SIZE:
|
||||
return programInterface == GL_UNIFORM || programInterface == GL_PROGRAM_INPUT ||
|
||||
programInterface == GL_PROGRAM_OUTPUT || programInterface == GL_BUFFER_VARIABLE ||
|
||||
programInterface == GL_TRANSFORM_FEEDBACK_VARYING ||
|
||||
(prop == GL_ARRAY_SIZE && isSubroutineUniform);
|
||||
case GL_OFFSET:
|
||||
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE ||
|
||||
programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||
case GL_BLOCK_INDEX:
|
||||
case GL_ARRAY_STRIDE:
|
||||
case GL_MATRIX_STRIDE:
|
||||
case GL_IS_ROW_MAJOR:
|
||||
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE;
|
||||
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||
return programInterface == GL_UNIFORM;
|
||||
case GL_BUFFER_BINDING:
|
||||
case GL_NUM_ACTIVE_VARIABLES:
|
||||
case GL_ACTIVE_VARIABLES:
|
||||
// Table 7.2 lists GL_TRANSFORM_FEEDBACK_BUFFER on these three rows too. This
|
||||
// implementation enumerates no resources on that interface, so the query still
|
||||
// ends in an error - but INVALID_VALUE for the out-of-range index, not the
|
||||
// INVALID_OPERATION a narrower table would invent.
|
||||
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||
programInterface == GL_SHADER_STORAGE_BLOCK ||
|
||||
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
case GL_BUFFER_DATA_SIZE:
|
||||
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||
programInterface == GL_SHADER_STORAGE_BLOCK;
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
return programInterface == GL_UNIFORM || programInterface == GL_UNIFORM_BLOCK ||
|
||||
programInterface == GL_ATOMIC_COUNTER_BUFFER || programInterface == GL_BUFFER_VARIABLE ||
|
||||
programInterface == GL_SHADER_STORAGE_BLOCK || programInterface == GL_PROGRAM_INPUT ||
|
||||
programInterface == GL_PROGRAM_OUTPUT || isSubroutineUniform;
|
||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||
return programInterface == GL_BUFFER_VARIABLE;
|
||||
case GL_LOCATION:
|
||||
return InterfaceHasLocations(programInterface);
|
||||
case GL_LOCATION_INDEX:
|
||||
return programInterface == GL_PROGRAM_OUTPUT;
|
||||
case GL_IS_PER_PATCH:
|
||||
case GL_LOCATION_COMPONENT:
|
||||
return programInterface == GL_PROGRAM_INPUT || programInterface == GL_PROGRAM_OUTPUT;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||
return programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||
return programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||
case GL_COMPATIBLE_SUBROUTINES:
|
||||
return isSubroutineUniform;
|
||||
default:
|
||||
(void)isSubroutine;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface) {
|
||||
const Model model = BuildModel(program);
|
||||
return static_cast<Int>(Select(model, programInterface).size());
|
||||
}
|
||||
|
||||
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface) {
|
||||
if (!IsNamedInterface(programInterface)) return 0;
|
||||
const Model model = BuildModel(program);
|
||||
SizeT longest = 0;
|
||||
for (const Resource& resource : Select(model, programInterface)) {
|
||||
longest = std::max(longest, resource.name.length() + 1);
|
||||
}
|
||||
return static_cast<Int>(longest);
|
||||
}
|
||||
|
||||
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface) {
|
||||
const Model model = BuildModel(program);
|
||||
SizeT longest = 0;
|
||||
for (const Resource& resource : Select(model, programInterface)) {
|
||||
longest = std::max(longest, resource.activeVariables.size());
|
||||
}
|
||||
return static_cast<Int>(longest);
|
||||
}
|
||||
|
||||
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||
if (name == nullptr || name[0] == '\0') return GL_INVALID_INDEX;
|
||||
const Model model = BuildModel(program);
|
||||
const ResourceList& resources = Select(model, programInterface);
|
||||
const String query = name;
|
||||
// The layout controls of an interleaved capture are enumerable but not addressable
|
||||
// by name (GL 4.6 §7.3.1.1).
|
||||
if (programInterface == GL_TRANSFORM_FEEDBACK_VARYING &&
|
||||
(query == "gl_NextBuffer" ||
|
||||
(query.size() == 18 && query.compare(0, 17, "gl_SkipComponents") == 0))) {
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
for (SizeT i = 0; i < resources.size(); ++i) {
|
||||
if (NamesMatch(resources[i].name, query)) return static_cast<GLuint>(i);
|
||||
}
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
|
||||
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName) {
|
||||
const Model model = BuildModel(program);
|
||||
const ResourceList& resources = Select(model, programInterface);
|
||||
if (index >= resources.size()) return false;
|
||||
outName = resources[index].name;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||
Vector<GLint>& outValues) {
|
||||
const Model model = BuildModel(program);
|
||||
const ResourceList& resources = Select(model, programInterface);
|
||||
if (index >= resources.size()) return false;
|
||||
const Resource& resource = resources[index];
|
||||
|
||||
const auto referencedBy = [&resource](EShLanguage stage) {
|
||||
return (resource.stages & static_cast<Uint32>(1u << stage)) != 0 ? GL_TRUE : GL_FALSE;
|
||||
};
|
||||
|
||||
switch (prop) {
|
||||
case GL_NAME_LENGTH:
|
||||
outValues.push_back(static_cast<GLint>(resource.name.length() + 1));
|
||||
break;
|
||||
case GL_TYPE:
|
||||
outValues.push_back(static_cast<GLint>(resource.type));
|
||||
break;
|
||||
case GL_ARRAY_SIZE:
|
||||
outValues.push_back(resource.arraySize);
|
||||
break;
|
||||
case GL_OFFSET:
|
||||
outValues.push_back(resource.offset);
|
||||
break;
|
||||
case GL_BLOCK_INDEX:
|
||||
outValues.push_back(resource.blockIndex);
|
||||
break;
|
||||
case GL_ARRAY_STRIDE:
|
||||
outValues.push_back(resource.arrayStride);
|
||||
break;
|
||||
case GL_MATRIX_STRIDE:
|
||||
outValues.push_back(resource.matrixStride);
|
||||
break;
|
||||
case GL_IS_ROW_MAJOR:
|
||||
outValues.push_back(resource.isRowMajor);
|
||||
break;
|
||||
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||
outValues.push_back(resource.atomicCounterBufferIndex);
|
||||
break;
|
||||
case GL_BUFFER_BINDING:
|
||||
outValues.push_back(resource.bufferBinding);
|
||||
break;
|
||||
case GL_BUFFER_DATA_SIZE:
|
||||
outValues.push_back(resource.bufferDataSize);
|
||||
break;
|
||||
case GL_NUM_ACTIVE_VARIABLES:
|
||||
outValues.push_back(static_cast<GLint>(resource.activeVariables.size()));
|
||||
break;
|
||||
case GL_ACTIVE_VARIABLES:
|
||||
for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast<GLint>(variable));
|
||||
break;
|
||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangVertex));
|
||||
break;
|
||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangTessControl));
|
||||
break;
|
||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangTessEvaluation));
|
||||
break;
|
||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangGeometry));
|
||||
break;
|
||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangFragment));
|
||||
break;
|
||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||
outValues.push_back(referencedBy(EShLangCompute));
|
||||
break;
|
||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||
outValues.push_back(resource.topLevelArraySize);
|
||||
break;
|
||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||
outValues.push_back(resource.topLevelArrayStride);
|
||||
break;
|
||||
case GL_LOCATION:
|
||||
outValues.push_back(resource.location);
|
||||
break;
|
||||
case GL_LOCATION_INDEX:
|
||||
outValues.push_back(resource.locationIndex);
|
||||
break;
|
||||
case GL_IS_PER_PATCH:
|
||||
outValues.push_back(resource.isPerPatch);
|
||||
break;
|
||||
case GL_LOCATION_COMPONENT:
|
||||
outValues.push_back(0);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||
outValues.push_back(resource.xfbBufferIndex);
|
||||
break;
|
||||
default:
|
||||
outValues.push_back(0);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||
if (name == nullptr || name[0] == '\0') return -1;
|
||||
const String query = name;
|
||||
|
||||
String base;
|
||||
Uint element = 0;
|
||||
Bool malformed = false;
|
||||
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||
if (malformed) return -1;
|
||||
|
||||
const Model model = BuildModel(program);
|
||||
const ResourceList& resources = Select(model, programInterface);
|
||||
for (const Resource& resource : resources) {
|
||||
if (NamesMatch(resource.name, query)) return resource.location;
|
||||
}
|
||||
if (!subscripted || element == 0) return -1;
|
||||
// "d[1]" addresses the second element of an array resource enumerated as "d[0]".
|
||||
for (const Resource& resource : resources) {
|
||||
if (!NamesMatch(resource.name, base)) continue;
|
||||
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||
return resource.location + static_cast<GLint>(element);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||
if (programInterface != GL_PROGRAM_OUTPUT || name == nullptr || name[0] == '\0') return -1;
|
||||
const String query = name;
|
||||
String base;
|
||||
Uint element = 0;
|
||||
Bool malformed = false;
|
||||
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||
if (malformed) return -1;
|
||||
|
||||
const Model model = BuildModel(program);
|
||||
for (const Resource& resource : model.programOutputs) {
|
||||
if (NamesMatch(resource.name, query)) return resource.locationIndex;
|
||||
}
|
||||
if (!subscripted) return -1;
|
||||
for (const Resource& resource : model.programOutputs) {
|
||||
if (!NamesMatch(resource.name, base)) continue;
|
||||
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||
return resource.locationIndex;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||
@@ -0,0 +1,66 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class ProgramObject;
|
||||
}
|
||||
|
||||
// The GL program interface (ARB_program_interface_query / GL 4.3 §7.3.1) as a frontend
|
||||
// resource model.
|
||||
//
|
||||
// WHY IT IS HERE AND NOT IN A BACKEND. glGetProgramResource* describes the program the
|
||||
// APPLICATION wrote, in the application's namespace. Neither backend program is in that
|
||||
// namespace: DirectGLES compiles SPIRV-Cross-generated ESSL where default-block uniforms
|
||||
// live inside the synthesized MGL_GLOBAL_UBO (so a GL_UNIFORM location query against it is
|
||||
// structurally -1) and stage in/out names are rewritten; DirectVulkan has no GL-level
|
||||
// reflection at all and can only re-derive a partial, diverging copy. The one authoritative
|
||||
// source is the frontend glslang reflection a link already produced, which is the same
|
||||
// place glGetActiveUniform answers from. This layer generalizes that rule to every
|
||||
// interface, so the six entry points never consult gBackendFunctionsTable.
|
||||
//
|
||||
// NAMING RULES LIVE HERE, NOT IN ProgramObject. The interface query spells resources
|
||||
// differently from glGetActiveUniform / glGetActiveAttrib (an array is "name[0]", a lookup
|
||||
// accepts both "name" and "name[0]", a subscript must be a strict decimal). Those two
|
||||
// getters are what GL30-33 exercises and they must not move, so every normalization is
|
||||
// applied on the way in and out of THIS file.
|
||||
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||
|
||||
// <programInterface> is one of the GL 4.6 Table 7.1 interfaces.
|
||||
Bool IsInterfaceEnum(GLenum programInterface);
|
||||
// Interfaces whose resources have names (everything except GL_ATOMIC_COUNTER_BUFFER).
|
||||
Bool IsNamedInterface(GLenum programInterface);
|
||||
// <prop> is a property token GetProgramResourceiv knows at all (else GL_INVALID_ENUM).
|
||||
Bool IsResourceProp(GLenum prop);
|
||||
// <prop> applies to <programInterface> (else GL_INVALID_OPERATION).
|
||||
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop);
|
||||
// Interfaces GetProgramResourceLocation accepts (else GL_INVALID_ENUM).
|
||||
Bool InterfaceHasLocations(GLenum programInterface);
|
||||
|
||||
// GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH / GL_MAX_NUM_ACTIVE_VARIABLES. All three
|
||||
// report zero for an interface this implementation cannot enumerate and for a program
|
||||
// that has not linked successfully - which is what the spec requires of a program with
|
||||
// no active resources.
|
||||
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface);
|
||||
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface);
|
||||
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface);
|
||||
|
||||
// GL_INVALID_INDEX when <name> names no active resource of the interface.
|
||||
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||
// False when <index> is out of range for the interface (the caller raises INVALID_VALUE).
|
||||
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName);
|
||||
// Appends the value(s) of <prop> for the resource; GL_ACTIVE_VARIABLES appends several.
|
||||
// False when <index> is out of range.
|
||||
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||
Vector<GLint>& outValues);
|
||||
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name);
|
||||
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "GL_Sampler.h"
|
||||
#include "Validators.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -268,9 +269,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
|
||||
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
|
||||
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
|
||||
// matter what the driver reports.
|
||||
static GLint GetSamplerBindableTextureUnitCount() {
|
||||
GLint maxTextureUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
}
|
||||
|
||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
|
||||
if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
if (static_cast<Uint64>(unit) >= static_cast<Uint64>(GetSamplerBindableTextureUnitCount())) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
|
||||
@@ -309,6 +321,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
|
||||
return;
|
||||
}
|
||||
// ARB_multi_bind: the whole [first, first + count) range is checked up front and a
|
||||
// range that runs past the last texture unit is INVALID_OPERATION - not the
|
||||
// INVALID_VALUE the single-bind BindSampler_State reports per element, and nothing is
|
||||
// bound when it fails. Both gates read the same limit (see
|
||||
// GetSamplerBindableTextureUnitCount), so an out-of-range multi-bind can no longer slip
|
||||
// past this check and be caught one element at a time with the wrong error class.
|
||||
const GLint maxTextureUnits = GetSamplerBindableTextureUnitCount();
|
||||
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(maxTextureUnits)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers",
|
||||
"first + count exceeds the number of texture units."));
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
||||
|
||||
@@ -75,7 +75,11 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
break;
|
||||
|
||||
case GL_TEXTURE_COMPARE_FUNC:
|
||||
if (param < GL_LEQUAL || param > GL_ALWAYS) {
|
||||
// The eight depth-compare functions are contiguous from GL_NEVER (0x0200) to
|
||||
// GL_ALWAYS (0x0207); GL_LEQUAL sits in the middle of that block, so starting
|
||||
// the range there rejected NEVER/LESS/EQUAL and let GREATER/NOTEQUAL/GEQUAL
|
||||
// through only by accident of them being above LEQUAL.
|
||||
if (param < GL_NEVER || param > GL_ALWAYS) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerParam",
|
||||
"Invalid compare function parameter"));
|
||||
|
||||
@@ -106,6 +106,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
||||
}
|
||||
|
||||
// The two ARB_vertex_attrib_binding per-attribute queries. They do not live on the
|
||||
// resolved VertexAttribute (which is the flat, already-combined view) but on the VAO's
|
||||
// binding-point mapping, so they need the object, not the attribute.
|
||||
static bool TryGetVertexAttribBindingQuery(GLuint index, GLenum pname, GLint& out) {
|
||||
if (pname != GL_VERTEX_ATTRIB_BINDING && pname != GL_VERTEX_ATTRIB_RELATIVE_OFFSET) return false;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
out = 0;
|
||||
return true;
|
||||
}
|
||||
out = pname == GL_VERTEX_ATTRIB_BINDING ? static_cast<GLint>(vao->GetAttributeBindingIndex(index))
|
||||
: static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||
return true;
|
||||
}
|
||||
|
||||
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
|
||||
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
|
||||
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
||||
@@ -179,6 +194,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
||||
// ARB_vertex_attrib_binding (core since GL 4.3). The binding-point view is real
|
||||
// state on the VAO (GetAttributeBindingIndex / GetAttributeRelativeOffset), so
|
||||
// both of its per-attribute queries are answerable.
|
||||
case GL_VERTEX_ATTRIB_BINDING:
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||
return true;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -944,6 +964,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLfloat>(attr->Divisor);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_BINDING:
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||
GLint value = 0;
|
||||
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||
params[0] = static_cast<GLfloat>(value);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -1007,6 +1034,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLdouble>(attr->Divisor);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_BINDING:
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||
GLint value = 0;
|
||||
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||
params[0] = static_cast<GLdouble>(value);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -1066,6 +1100,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLint>(attr->Divisor);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_BINDING:
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||
TryGetVertexAttribBindingQuery(index, pname, params[0]);
|
||||
return;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -1204,6 +1242,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_BINDING:
|
||||
*param = static_cast<GLint>(vao->GetAttributeBindingIndex(index));
|
||||
return;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
|
||||
@@ -147,6 +147,30 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The integer path takes exactly the six signed/unsigned integer types (GL 4.6
|
||||
// core 10.3.2): BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, INT, UNSIGNED_INT.
|
||||
// A blacklist could not express that: GL_FLOAT, GL_HALF_FLOAT,
|
||||
// GL_DOUBLE and GL_FIXED all convert to a perfectly valid DataType, so they slipped
|
||||
// through and were recorded as integer attributes.
|
||||
if (integerPath) {
|
||||
switch (type) {
|
||||
case DataType::Int8:
|
||||
case DataType::Uint8:
|
||||
case DataType::Int16:
|
||||
case DataType::Uint16:
|
||||
case DataType::Int32:
|
||||
case DataType::Uint32:
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", fn,
|
||||
std::format("Type is not an integer vertex attribute type (attribute {}).", index)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sizeRaw == static_cast<GLint>(GL_BGRA)) {
|
||||
// GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and
|
||||
// normalized == GL_TRUE. On the integer path it is simply an out-of-range size.
|
||||
|
||||
@@ -206,8 +206,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
|
||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
||||
"Cannot upload sub data while buffer is non-persistently mapped.");
|
||||
// GL 4.6 core 6.5 forbids only the OVERLAPPING write: a glBufferSubData that stays
|
||||
// clear of a non-persistent mapping is legal, and the frontend lets it through.
|
||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent) ||
|
||||
atOffset >= m_mappedRange.end || atOffset + data.size <= m_mappedRange.start,
|
||||
"Cannot upload sub data overlapping a non-persistent mapping.");
|
||||
MOBILEGL_ASSERT(atOffset + data.size <= m_size,
|
||||
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
||||
data.size, m_size);
|
||||
|
||||
@@ -1057,6 +1057,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Bool ProgramLinkTask::ResolveTransformFeedbackVaryings() {
|
||||
artifacts.xfbVaryings.clear();
|
||||
// The GL_TRANSFORM_FEEDBACK_VARYING interface enumerates the request verbatim -
|
||||
// pseudo-varyings included - while xfbVaryings below keeps only what is actually
|
||||
// captured. Snapshot it before the loop consumes gl_NextBuffer/gl_SkipComponentsN.
|
||||
artifacts.xfbInterfaceNames = in.requestedXfbVaryings;
|
||||
artifacts.xfbStrides.clear();
|
||||
artifacts.xfbBufferMode = in.requestedXfbBufferMode;
|
||||
artifacts.xfbVaryingNameMaxLength = 0;
|
||||
@@ -1127,15 +1131,45 @@ namespace MobileGL::MG_State::GLState {
|
||||
bytesPerElement = 4;
|
||||
resolved = true;
|
||||
} else if (linkerObjects != nullptr) {
|
||||
// GL lets a capture name a single element of an output array ("b[0]"), which
|
||||
// captures one element of the element type - not the whole array. Strip a
|
||||
// trailing strict-decimal subscript and look the base declaration up.
|
||||
String declaredName = name;
|
||||
Bool singleElement = false;
|
||||
Uint element = 0;
|
||||
if (name.size() > 3 && name.back() == ']') {
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket != String::npos && bracket + 1 < name.size() - 1) {
|
||||
Bool digitsOnly = true;
|
||||
for (SizeT c = bracket + 1; c + 1 < name.size(); ++c) {
|
||||
if (name[c] < '0' || name[c] > '9') {
|
||||
digitsOnly = false;
|
||||
break;
|
||||
}
|
||||
element = element * 10 + static_cast<Uint>(name[c] - '0');
|
||||
}
|
||||
if (digitsOnly) {
|
||||
declaredName = name.substr(0, bracket);
|
||||
singleElement = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto* node : linkerObjects->getSequence()) {
|
||||
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
||||
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
||||
continue;
|
||||
}
|
||||
if (symbol->getName() != name.c_str()) {
|
||||
if (symbol->getName() != declaredName.c_str()) {
|
||||
continue;
|
||||
}
|
||||
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
|
||||
if (resolved && singleElement) {
|
||||
if (static_cast<Int>(element) >= varying.size) {
|
||||
resolved = false;
|
||||
break;
|
||||
}
|
||||
varying.size = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.explicitOpaqueUniformBindings.clear();
|
||||
artifacts.uniformBlockIndexByName.clear();
|
||||
artifacts.uniformBlockBinding.clear();
|
||||
artifacts.shaderStorageBlockBinding.clear();
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.uniformSizesInBytes.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
@@ -127,6 +128,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.attribInNameMaxLength = 0;
|
||||
artifacts.uniformBlockNameMaxLength = 0;
|
||||
artifacts.xfbVaryings.clear();
|
||||
artifacts.xfbInterfaceNames.clear();
|
||||
artifacts.xfbStrides.clear();
|
||||
artifacts.xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
artifacts.xfbVaryingNameMaxLength = 0;
|
||||
|
||||
@@ -82,6 +82,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return -1;
|
||||
}
|
||||
if (name.length() < 4) return -1;
|
||||
// An array of arrays is keyed by its full "[0]"-terminated spelling
|
||||
// ("a[2][1][0]"), so a query that already ends in a subscript may still be the
|
||||
// NAME of an array rather than an element of one. Try that first; only then
|
||||
// treat the trailing subscript as an element index.
|
||||
{
|
||||
const auto arrayOfArraysIt = Artifacts().uniformLocations.find(name + "[0]");
|
||||
if (arrayOfArraysIt != Artifacts().uniformLocations.end()) return (Int)arrayOfArraysIt->second;
|
||||
}
|
||||
const SizeT bracket = name.rfind('[');
|
||||
// Require at least one digit between the brackets.
|
||||
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
||||
@@ -130,6 +138,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
|
||||
return Artifacts().tProgramUniformIndexToGl[tIndex];
|
||||
}
|
||||
// GL uniform-block index -> glslang TProgram block index (the inverse of
|
||||
// GlBlockIndexFromTProgram). The interface-query layer needs it to reach block
|
||||
// properties glslang exposes but no typed getter here does.
|
||||
Int TProgramBlockIndex(Uint glBlockIndex) const {
|
||||
return glBlockIndex < Artifacts().glBlockIndexToTProgram.size()
|
||||
? Artifacts().glBlockIndexToTProgram[glBlockIndex]
|
||||
: -1;
|
||||
}
|
||||
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
|
||||
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(Artifacts().tProgramBlockIndexToGl.size())) return -1;
|
||||
return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
|
||||
@@ -529,9 +545,42 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
|
||||
|
||||
// Set by glShaderStorageBlockBinding, keyed by the block's GL name rather than by any
|
||||
// index. A shader storage block has THREE index spaces - the frontend interface-query
|
||||
// enumeration, DirectVulkan's SPIR-V descriptor order and DirectGLES's real-driver
|
||||
// order - and the name is the only coordinate all three agree on. Absent from the map
|
||||
// means "never rebound", and the shader's declared binding still stands.
|
||||
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
|
||||
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
|
||||
}
|
||||
// -1 when the block has never been rebound. `blockName` is the interface-query
|
||||
// spelling; an arrayed block's elements ("B[0]", "B[1]") are separate GL resources
|
||||
// with separate bindings, so they are separate keys.
|
||||
Int GetShaderStorageBlockBindingOverride(const String& blockName) const {
|
||||
const auto it = Artifacts().shaderStorageBlockBinding.find(blockName);
|
||||
if (it != Artifacts().shaderStorageBlockBinding.end()) return it->second;
|
||||
// A backend that collapses an arrayed block down to one resource knows it only by
|
||||
// the bare block name; answer that with element zero's binding.
|
||||
const auto zeroth = Artifacts().shaderStorageBlockBinding.find(blockName + "[0]");
|
||||
return zeroth != Artifacts().shaderStorageBlockBinding.end() ? zeroth->second : -1;
|
||||
}
|
||||
// Every rebinding recorded so far, for a backend that has to REPLAY them onto a
|
||||
// driver program it just (re)built. Empty for the overwhelming majority of programs -
|
||||
// check .empty() before doing any per-block work.
|
||||
const UnorderedMap<String, Int>& GetShaderStorageBlockBindingOverrides() const {
|
||||
return Artifacts().shaderStorageBlockBinding;
|
||||
}
|
||||
|
||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
|
||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
|
||||
|
||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||
// lists no typed getter above exposes: the GL program-interface query layer
|
||||
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
|
||||
// blocks, buffer variables, atomic counters and per-stage reference masks. Null
|
||||
// until a link has succeeded. Read through the join gate like everything else.
|
||||
const glslang::TProgram* GetReflection() const { return Artifacts().program.get(); }
|
||||
|
||||
Int GetShaderIndexByStage(ShaderStage stage) const {
|
||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
|
||||
return shader->GetShaderStage() == stage;
|
||||
@@ -611,6 +660,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
|
||||
// Need to be reflected after linking of SPIR-V binary
|
||||
Vector<Uint> uniformOffsets;
|
||||
@@ -629,6 +681,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||
// GL-thread-owned side).
|
||||
Vector<XfbVarying> xfbVaryings;
|
||||
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||
Vector<String> xfbInterfaceNames;
|
||||
Vector<Uint32> xfbStrides;
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
@@ -711,6 +769,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr;
|
||||
}
|
||||
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return Artifacts().xfbVaryings; }
|
||||
// The GL_TRANSFORM_FEEDBACK_VARYING resource list: every name the last successful
|
||||
// link was asked to capture, in request order, pseudo-varyings included.
|
||||
const Vector<String>& GetTransformFeedbackInterfaceNames() const { return Artifacts().xfbInterfaceNames; }
|
||||
// Stride of one captured vertex in the given capture buffer slot.
|
||||
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
|
||||
return bufferIndex < Artifacts().xfbStrides.size() ? Artifacts().xfbStrides[bufferIndex] : 0;
|
||||
|
||||
@@ -455,16 +455,24 @@ TEST_F(BufferTest, BindBufferBaseZeroUnbindsBindingPoint) {
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, BindBufferRangeZeroUnbindsBindingPoint) {
|
||||
// GL_SHADER_STORAGE_BUFFER offsets must be a multiple of
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, so the offset cannot be a literal.
|
||||
GLint ssboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
ASSERT_GT(ssboAlignment, 0);
|
||||
const GLintptr offset = ssboAlignment;
|
||||
const GLsizeiptr size = 8;
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, 16, nullptr, GL_DYNAMIC_DRAW);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, offset + size, nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, buffer, 4, 8);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, buffer, offset, size);
|
||||
auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 3);
|
||||
ASSERT_NE(point.GetBoundObject(), nullptr);
|
||||
EXPECT_EQ(point.GetRange().start, 4);
|
||||
EXPECT_EQ(point.GetRange().end, 12);
|
||||
EXPECT_EQ(point.GetRange().start, static_cast<SizeT>(offset));
|
||||
EXPECT_EQ(point.GetRange().end, static_cast<SizeT>(offset + size));
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, 0, 0, 0);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr);
|
||||
@@ -540,6 +548,305 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
|
||||
// GL 4.6 core 6.5: glBufferSubData fails only when the written range OVERLAPS the mapped range.
|
||||
// A second, wrong test used to sit next to the correct one and reject any write whose end reached
|
||||
// the start of the mapping - which killed every legal disjoint update in front of a mapped tail.
|
||||
TEST_F(BufferTest, BufferSubDataRejectsOnlyRangesOverlappingTheMapping) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, 64, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
void* mapped = MobileGL::MG_Impl::GLImpl::MapBufferRange(GL_ARRAY_BUFFER, 32, 32, GL_MAP_WRITE_BIT);
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Entirely before the mapping: legal, and the bytes must land.
|
||||
const Uint32 payload[4] = {1u, 2u, 3u, 4u};
|
||||
MobileGL::MG_Impl::GLImpl::BufferSubData(GL_ARRAY_BUFFER, 0, sizeof(payload), payload);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Touching the first mapped byte: overlap, so INVALID_OPERATION.
|
||||
MobileGL::MG_Impl::GLImpl::BufferSubData(GL_ARRAY_BUFFER, 16, 32, payload);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
EXPECT_TRUE(MobileGL::MG_Impl::GLImpl::UnmapBuffer(GL_ARRAY_BUFFER));
|
||||
|
||||
Vector<Uint32> actual(4, 0);
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), sizeof(payload));
|
||||
EXPECT_EQ(actual, (Vector<Uint32>{1u, 2u, 3u, 4u}));
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 6.2: "no buffer bound to target" outranks a bad size or bad flags, so the binding has
|
||||
// to be resolved before either is validated. It used to be checked last, which turned every
|
||||
// unbound-target call into INVALID_VALUE.
|
||||
TEST_F(BufferTest, BufferStorageReportsTheUnboundTargetBeforeSizeAndFlags) {
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// Both a zero size and a nonsense flag set are present; the unbound target still wins.
|
||||
MobileGL::MG_Impl::GLImpl::BufferStorage(GL_ARRAY_BUFFER, 0, nullptr, GL_MAP_PERSISTENT_BIT);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// With a buffer bound, the size check is reachable again.
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferStorage(GL_ARRAY_BUFFER, 0, nullptr, GL_MAP_READ_BIT);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 6.1.1: glBindBufferRange on GL_SHADER_STORAGE_BUFFER must reject an offset that is
|
||||
// not a multiple of GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT.
|
||||
TEST_F(BufferTest, BindBufferRangeRejectsMisalignedShaderStorageOffset) {
|
||||
GLint ssboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
ASSERT_GT(ssboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset";
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 4, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, 1, ssboAlignment);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 1);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr) << "a rejected bind must not take effect";
|
||||
|
||||
// The uniform target has its own alignment and must not inherit the SSBO rule's rejection.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, ssboAlignment, ssboAlignment);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_NE(point.GetBoundObject(), nullptr);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, 0, 0, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// ARB_multi_bind: the [first, first + count) range is checked up front and reports
|
||||
// INVALID_OPERATION - not the per-element INVALID_VALUE a naive loop over glBindBufferBase would
|
||||
// produce, and nothing may be bound when it fails.
|
||||
TEST_F(BufferTest, BindBuffersBaseChecksTheWholeRangeBeforeBindingAnything) {
|
||||
GLint maxBindings = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxBindings);
|
||||
ASSERT_GT(maxBindings, 1);
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, 16, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// first is in range but first + count is not: one error, of the multi-bind class.
|
||||
const GLuint first = static_cast<GLuint>(maxBindings - 1);
|
||||
const GLuint buffers[2] = {buffer, buffer};
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffersBase(GL_SHADER_STORAGE_BUFFER, first, 2, buffers);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, first);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr) << "the in-range prefix must not be bound either";
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, first, 2, buffers, nullptr, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
// A range that fits binds normally.
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffersBase(GL_SHADER_STORAGE_BUFFER, first, 1, buffers);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
ASSERT_NE(point.GetBoundObject(), nullptr);
|
||||
EXPECT_EQ(point.GetBoundObject()->GetExternalIndex(), buffer);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, first, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// These limits were reachable only through glGetInteger64v (SSBO block size) or not at all (the
|
||||
// atomic-counter pair), so glGetIntegerv answered them with INVALID_ENUM out of its default arm.
|
||||
TEST_F(BufferTest, GetIntegervAnswersSsboAndAtomicCounterLimits) {
|
||||
GLint ssboBlockSize = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &ssboBlockSize);
|
||||
EXPECT_GT(ssboBlockSize, 0);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The 32-bit query saturates rather than truncating what glGetInteger64v reports.
|
||||
GLint64 ssboBlockSize64 = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetInteger64v(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &ssboBlockSize64);
|
||||
EXPECT_EQ(static_cast<GLint64>(ssboBlockSize), std::min<GLint64>(ssboBlockSize64, INT32_MAX));
|
||||
|
||||
GLint atomicBindings = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &atomicBindings);
|
||||
EXPECT_GE(atomicBindings, 1);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint atomicBufferSize = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &atomicBufferSize);
|
||||
EXPECT_GE(atomicBufferSize, 32); // GL 4.6 table 23.63 minimum
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// KHR_debug requires these to be legal even while the debug entry points are stubs.
|
||||
GLint debugGroupDepth = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_GROUP_STACK_DEPTH, &debugGroupDepth);
|
||||
EXPECT_GE(debugGroupDepth, 64);
|
||||
GLint debugLoggedMessages = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_LOGGED_MESSAGES, &debugLoggedMessages);
|
||||
EXPECT_GE(debugLoggedMessages, 1);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 6.1.1: glBindBufferRange validates the (offset, size) pair before it writes any
|
||||
// state. Nothing validated either one, so a negative offset reached Range1D(offset, offset + size)
|
||||
// - which has no ordering check of its own - and a zero or negative size installed an empty or
|
||||
// backwards range on the binding point.
|
||||
TEST_F(BufferTest, BindBufferRangeRejectsNegativeOffsetAndNonPositiveSize) {
|
||||
GLint ssboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
ASSERT_GT(ssboAlignment, 0);
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 4, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 2);
|
||||
|
||||
// A negative offset is INVALID_VALUE - including one that is a multiple of the alignment, which
|
||||
// the modulo gate alone waves through (-alignment % alignment == 0).
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, -ssboAlignment, ssboAlignment);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr) << "a rejected bind must not take effect";
|
||||
|
||||
// size must be strictly positive.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, 0, 0);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, 0, -4);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr);
|
||||
|
||||
// The well-formed bind still goes through.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, ssboAlignment, ssboAlignment);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
ASSERT_NE(point.GetBoundObject(), nullptr);
|
||||
EXPECT_EQ(point.GetRange().start, static_cast<SizeT>(ssboAlignment));
|
||||
EXPECT_EQ(point.GetRange().end, static_cast<SizeT>(ssboAlignment * 2));
|
||||
|
||||
// Buffer 0 detaches with offset and size ignored: the one case the size rule must not fire on,
|
||||
// and the shape glBindBuffersRange uses to reset an element.
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, 0, 0, 0);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(point.GetBoundObject(), nullptr);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 6.1.1 gives GL_UNIFORM_BUFFER its own offset alignment
|
||||
// (GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT) and requires BOTH offset and size to be multiples of 4 on
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER. Only the shader-storage half of the rule was implemented, so a
|
||||
// misaligned uniform range bound happily.
|
||||
TEST_F(BufferTest, BindBufferRangeEnforcesUniformAndTransformFeedbackAlignment) {
|
||||
GLint uboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &uboAlignment);
|
||||
ASSERT_GT(uboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset";
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_UNIFORM_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_UNIFORM_BUFFER, uboAlignment * 4, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
auto& uniformPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, 1);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, buffer, 1, uboAlignment);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(uniformPoint.GetBoundObject(), nullptr) << "a misaligned uniform range must not bind";
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, buffer, uboAlignment, uboAlignment);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_NE(uniformPoint.GetBoundObject(), nullptr);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, 0, 0, 0);
|
||||
EXPECT_EQ(uniformPoint.GetBoundObject(), nullptr);
|
||||
|
||||
// Transform feedback captures 32-bit components: offset and size are both constrained, and the
|
||||
// size half has no analogue on any other target.
|
||||
auto& feedbackPoint =
|
||||
MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 2, 4);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(feedbackPoint.GetBoundObject(), nullptr);
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 4, 2);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(feedbackPoint.GetBoundObject(), nullptr);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 4, 4);
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_NE(feedbackPoint.GetBoundObject(), nullptr);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0, 0, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// ARB_multi_bind checks offsets and sizes separately for each binding point: the offending element
|
||||
// is left unchanged and reports INVALID_VALUE while every other element still binds. Only the
|
||||
// [first, first + count) range is the up-front, all-or-nothing check - so glBindBuffersRange gets
|
||||
// the new gates by looping over the single-bind entry point, and must keep going after one fails.
|
||||
TEST_F(BufferTest, BindBuffersRangeAppliesTheOffsetAndSizeGatesPerElement) {
|
||||
GLint ssboAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
ASSERT_GT(ssboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset";
|
||||
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 8, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
auto& firstPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0);
|
||||
auto& secondPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 1);
|
||||
const GLuint buffers[2] = {buffer, buffer};
|
||||
|
||||
// Element 0 is misaligned; element 1 is well formed and must still be bound.
|
||||
const GLintptr misalignedOffsets[2] = {1, ssboAlignment};
|
||||
const GLsizeiptr sizes[2] = {ssboAlignment, ssboAlignment};
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, 2, buffers, misalignedOffsets, sizes);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(firstPoint.GetBoundObject(), nullptr) << "the rejected element must not bind";
|
||||
ASSERT_NE(secondPoint.GetBoundObject(), nullptr) << "a per-element error must not abort the rest of the range";
|
||||
EXPECT_EQ(secondPoint.GetRange().start, static_cast<SizeT>(ssboAlignment));
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Same for a non-positive size, on the other element this time.
|
||||
const GLintptr offsets[2] = {0, ssboAlignment};
|
||||
const GLsizeiptr badSizes[2] = {ssboAlignment, 0};
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, 2, buffers, offsets, badSizes);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_NE(firstPoint.GetBoundObject(), nullptr);
|
||||
EXPECT_EQ(secondPoint.GetBoundObject(), nullptr);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
using namespace MobileGL::MG_Impl::GLImpl;
|
||||
|
||||
class GeneralBufferTest : public ::testing::Test {
|
||||
|
||||
@@ -130,6 +130,22 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
ProgramInterfaceTest
|
||||
ProgramInterfaceTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ProgramInterfaceTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
ProgramInterfaceTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_include_directories(ProgramTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
@@ -144,6 +160,7 @@ target_link_libraries(
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
||||
# compile pool so there is something in flight to race against.
|
||||
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3091,3 +3091,148 @@ TEST_F(ProgramTest, PreprocessCacheOverflowKeepsCompilingCorrectly) {
|
||||
EXPECT_GE(GetUniformLocation(programB, "uColor"), 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}fv and their twelve glProgramUniformMatrix* twins were
|
||||
// validate-only no-ops: they never took the value pointer at all. They upload column-at-a-time at
|
||||
// the std140 16-byte column stride, honouring `transpose`, and glGetUniformfv undoes that padding.
|
||||
TEST_F(ProgramTest, NonSquareMatrixUniformsRoundTripThroughTheGlobalUbo) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
uniform mat2x3 uM2x3;
|
||||
uniform mat3x2 uM3x2;
|
||||
uniform mat4x3 uM4x3;
|
||||
uniform mat2 uM2;
|
||||
void main() {
|
||||
vec3 a = uM2x3 * vec2(1.0);
|
||||
vec2 b = uM3x2 * vec3(1.0);
|
||||
vec3 c = uM4x3 * vec4(1.0);
|
||||
vec2 d = uM2 * vec2(1.0);
|
||||
gl_Position = vec4(a.xy + b + c.xy + d, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* fsSource = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
UseProgram(program);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// matCxR is C columns of R rows, column-major: value[c * R + r].
|
||||
const GLfloat m2x3[6] = {1, 2, 3, 4, 5, 6};
|
||||
const GLfloat m3x2[6] = {1, 2, 3, 4, 5, 6};
|
||||
const GLfloat m4x3[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
|
||||
|
||||
const GLint loc2x3 = GetUniformLocation(program, "uM2x3");
|
||||
const GLint loc3x2 = GetUniformLocation(program, "uM3x2");
|
||||
const GLint loc4x3 = GetUniformLocation(program, "uM4x3");
|
||||
ASSERT_GE(loc2x3, 0);
|
||||
ASSERT_GE(loc3x2, 0);
|
||||
ASSERT_GE(loc4x3, 0);
|
||||
|
||||
UniformMatrix2x3fv(loc2x3, 1, GL_FALSE, m2x3);
|
||||
UniformMatrix3x2fv(loc3x2, 1, GL_FALSE, m3x2);
|
||||
UniformMatrix4x3fv(loc4x3, 1, GL_FALSE, m4x3);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLfloat readBack[12] = {};
|
||||
GetUniformfv(program, loc2x3, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, m2x3, sizeof(m2x3)), 0);
|
||||
std::memset(readBack, 0, sizeof(readBack));
|
||||
GetUniformfv(program, loc3x2, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, m3x2, sizeof(m3x2)), 0);
|
||||
std::memset(readBack, 0, sizeof(readBack));
|
||||
GetUniformfv(program, loc4x3, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, m4x3, sizeof(m4x3)), 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// transpose = GL_TRUE means the source is row-major: a mat3x2 (3 columns, 2 rows) is then
|
||||
// given as 2 rows of 3, so {1,2,3, 4,5,6} is the column-major {1,4, 2,5, 3,6}.
|
||||
UniformMatrix3x2fv(loc3x2, 1, GL_TRUE, m3x2);
|
||||
const GLfloat expectedTransposed3x2[6] = {1, 4, 2, 5, 3, 6};
|
||||
std::memset(readBack, 0, sizeof(readBack));
|
||||
GetUniformfv(program, loc3x2, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, expectedTransposed3x2, sizeof(expectedTransposed3x2)), 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// The glProgramUniform* twin writes the same bytes without the program being current.
|
||||
UseProgram(0);
|
||||
const GLfloat other2x3[6] = {9, 8, 7, 6, 5, 4};
|
||||
ProgramUniformMatrix2x3fv(program, loc2x3, 1, GL_FALSE, other2x3);
|
||||
std::memset(readBack, 0, sizeof(readBack));
|
||||
GetUniformfv(program, loc2x3, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, other2x3, sizeof(other2x3)), 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A mat2 is not four contiguous floats in the global UBO: std140 pads each column vector out to
|
||||
// 16 bytes, so column 1 starts at byte 16. Writing it packed put column 1 on top of column 0's
|
||||
// padding, where the shader never reads it.
|
||||
TEST_F(ProgramTest, Mat2UniformUsesTheStd140ColumnStride) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
uniform mat2 uM2;
|
||||
void main() { gl_Position = vec4(uM2 * vec2(1.0), 0.0, 1.0); }
|
||||
)";
|
||||
const char* fsSource = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
UseProgram(program);
|
||||
const GLint loc = GetUniformLocation(program, "uM2");
|
||||
ASSERT_GE(loc, 0);
|
||||
|
||||
const GLfloat m2[4] = {1, 2, 3, 4};
|
||||
UniformMatrix2fv(loc, 1, GL_FALSE, m2);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// The GL-visible value is tightly packed...
|
||||
GLfloat readBack[4] = {};
|
||||
GetUniformfv(program, loc, readBack);
|
||||
EXPECT_EQ(std::memcmp(readBack, m2, sizeof(m2)), 0);
|
||||
|
||||
// ...while the bytes in the UBO put column 1 at offset 16, not 8.
|
||||
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
ASSERT_NE(programObject, nullptr);
|
||||
const auto* ubo = static_cast<const char*>(programObject->MapUBO());
|
||||
ASSERT_NE(ubo, nullptr);
|
||||
const Uint offset = programObject->GetUniformOffset(static_cast<Uint>(loc));
|
||||
ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset);
|
||||
GLfloat column0[2] = {};
|
||||
GLfloat column1[2] = {};
|
||||
std::memcpy(column0, ubo + offset, sizeof(column0));
|
||||
std::memcpy(column1, ubo + offset + 16, sizeof(column1));
|
||||
EXPECT_FLOAT_EQ(column0[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(column0[1], 2.0f);
|
||||
EXPECT_FLOAT_EQ(column1[0], 3.0f);
|
||||
EXPECT_FLOAT_EQ(column1[1], 4.0f);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM - it used to be
|
||||
// reported as INVALID_VALUE. glCreateShaderProgramv adds a count < 0 gate ahead of everything.
|
||||
TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasses) {
|
||||
while (GetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
EXPECT_EQ(CreateShader(GL_FLOAT), 0u);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
|
||||
const char* source = "#version 330 core\nvoid main() { gl_Position = vec4(1.0); }\n";
|
||||
EXPECT_EQ(CreateShaderProgramv(GL_FLOAT, 1, &source), 0u);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
|
||||
EXPECT_EQ(CreateShaderProgramv(GL_VERTEX_SHADER, -1, &source), 0u);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
|
||||
// A well-formed call still works.
|
||||
const GLuint program = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &source);
|
||||
EXPECT_NE(program, 0u);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -2672,3 +2672,166 @@ TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) {
|
||||
EXPECT_EQ(rgba[3], 2u);
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.18: GL_TEXTURE_COMPARE_FUNC takes the whole eight-function depth-compare
|
||||
// range. The validator used to start it at GL_LEQUAL, which sits in the middle of the contiguous
|
||||
// GL_NEVER..GL_ALWAYS block, so NEVER/LESS/EQUAL were rejected while GREATER/NOTEQUAL/GEQUAL only
|
||||
// got through because they happen to be numerically above LEQUAL.
|
||||
TEST_F(TextureTest, SamplerCompareFuncAcceptsTheWholeNeverToAlwaysRange) {
|
||||
GLuint sampler = 0;
|
||||
MG_Impl::GLImpl::GenSamplers(1, &sampler);
|
||||
ASSERT_NE(sampler, 0u);
|
||||
|
||||
const GLenum compareFuncs[] = {GL_NEVER, GL_LESS, GL_EQUAL, GL_LEQUAL,
|
||||
GL_GREATER, GL_NOTEQUAL, GL_GEQUAL, GL_ALWAYS};
|
||||
for (GLenum func : compareFuncs) {
|
||||
MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, static_cast<GLint>(func));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "compare func " << func << " was rejected";
|
||||
GLint readBack = 0;
|
||||
MG_Impl::GLImpl::GetSamplerParameteriv(sampler, GL_TEXTURE_COMPARE_FUNC, &readBack);
|
||||
EXPECT_EQ(static_cast<GLenum>(readBack), func);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Just outside the block on both sides is still INVALID_ENUM.
|
||||
MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, GL_NEVER - 1);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, GL_ALWAYS + 1);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
MG_Impl::GLImpl::DeleteSamplers(1, &sampler);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.19: GL_TEXTURE_BINDING_* and GL_SAMPLER_BINDING are per-texture-unit, so
|
||||
// glGetIntegeri_v must answer for unit `index` - not fall through to the backend, which knows
|
||||
// nothing about the frontend's binding state.
|
||||
TEST_F(TextureTest, GetIntegeriVReportsPerUnitTextureAndSamplerBindings) {
|
||||
GLuint textures[2] = {0, 0};
|
||||
MG_Impl::GLImpl::GenTextures(2, textures);
|
||||
ASSERT_NE(textures[0], 0u);
|
||||
ASSERT_NE(textures[1], 0u);
|
||||
|
||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]);
|
||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE3);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[1]);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint binding = -1;
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 0, &binding);
|
||||
EXPECT_EQ(static_cast<GLuint>(binding), textures[0]);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 3, &binding);
|
||||
EXPECT_EQ(static_cast<GLuint>(binding), textures[1]);
|
||||
// An unbound unit reports 0, and a target nothing was bound to reports 0 as well.
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 2, &binding);
|
||||
EXPECT_EQ(binding, 0);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_3D, 0, &binding);
|
||||
EXPECT_EQ(binding, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The non-indexed query keeps reporting the ACTIVE unit, which is still unit 3.
|
||||
GLint activeUnitBinding = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &activeUnitBinding);
|
||||
EXPECT_EQ(static_cast<GLuint>(activeUnitBinding), textures[1]);
|
||||
|
||||
GLuint sampler = 0;
|
||||
MG_Impl::GLImpl::GenSamplers(1, &sampler);
|
||||
ASSERT_NE(sampler, 0u);
|
||||
MG_Impl::GLImpl::BindSampler(2, sampler);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SAMPLER_BINDING, 2, &binding);
|
||||
EXPECT_EQ(static_cast<GLuint>(binding), sampler);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SAMPLER_BINDING, 1, &binding);
|
||||
EXPECT_EQ(binding, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Out of range is INVALID_VALUE, not a backend passthrough.
|
||||
GLint maxUnits = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
|
||||
ASSERT_GT(maxUnits, 0);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, static_cast<GLuint>(maxUnits) + 1024u, &binding);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::BindSampler(2, 0);
|
||||
MG_Impl::GLImpl::DeleteSamplers(1, &sampler);
|
||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
|
||||
MG_Impl::GLImpl::DeleteTextures(2, textures);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// glGetFloati_v / glGetDoublei_v were no-op stubs: they left the caller's buffer holding whatever
|
||||
// was on the stack. They are converters over the integer indexed query.
|
||||
TEST_F(TextureTest, GetFloatiVAndGetDoubleiVConvertTheIndexedIntegerQuery) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
ASSERT_NE(texture, 0u);
|
||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE1);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLfloat asFloat = -1.0f;
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_TEXTURE_BINDING_2D, 1, &asFloat);
|
||||
EXPECT_FLOAT_EQ(asFloat, static_cast<GLfloat>(texture));
|
||||
|
||||
GLdouble asDouble = -1.0;
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_TEXTURE_BINDING_2D, 1, &asDouble);
|
||||
EXPECT_DOUBLE_EQ(asDouble, static_cast<GLdouble>(texture));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// GL 3.3 core 3.8.2: the unit glBindSampler accepts is bounded by
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS. The gate read the frontend's MAX_TEXTURE_IMAGE_UNITS
|
||||
// instead - the capacity of the unit array, 192 - so every unit the backend does not have was
|
||||
// accepted, and the single-bind path disagreed with the multi-bind twin about where the units end.
|
||||
// The backend is stood in so the two limits are distinguishable no matter what the real one
|
||||
// advertises.
|
||||
TEST_F(TextureTest, BindSamplerRejectsUnitsBeyondMaxCombinedTextureImageUnits) {
|
||||
GLuint sampler = 0;
|
||||
MG_Impl::GLImpl::GenSamplers(1, &sampler);
|
||||
ASSERT_NE(sampler, 0u);
|
||||
|
||||
constexpr GLint kCombinedUnits = 24;
|
||||
static_assert(kCombinedUnits < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS,
|
||||
"the stand-in limit has to be below the unit array capacity to tell the two apart");
|
||||
auto backend = MakeUnique<FormatCapabilityBackend>();
|
||||
FormatCapabilityBackend::MutableDynamicParameters().MaxCombinedTextureImageUnits = kCombinedUnits;
|
||||
ScopedBackendOverride backendOverride(Move(backend));
|
||||
|
||||
GLint reportedUnits = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &reportedUnits);
|
||||
ASSERT_EQ(reportedUnits, kCombinedUnits);
|
||||
|
||||
// The last unit that exists still binds.
|
||||
const GLuint lastUnit = static_cast<GLuint>(kCombinedUnits - 1);
|
||||
MG_Impl::GLImpl::BindSampler(lastUnit, sampler);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_NE(MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(lastUnit)).GetSamplerObject(), nullptr);
|
||||
|
||||
// One past it does not - this is the unit the old gate accepted.
|
||||
MG_Impl::GLImpl::BindSampler(static_cast<GLuint>(kCombinedUnits), sampler);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(kCombinedUnits).GetSamplerObject(), nullptr);
|
||||
|
||||
// Past the unit array as well is the same error, not an out-of-bounds index.
|
||||
MG_Impl::GLImpl::BindSampler(
|
||||
static_cast<GLuint>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) + 4u, sampler);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// Both gates now read the same limit: a multi-bind that ends exactly at it binds, and one that
|
||||
// runs a single unit past it is the multi-bind's INVALID_OPERATION, reported up front - not the
|
||||
// single-bind INVALID_VALUE from somewhere inside the loop.
|
||||
const GLuint samplers[2] = {sampler, sampler};
|
||||
MG_Impl::GLImpl::BindSamplers(static_cast<GLuint>(kCombinedUnits - 2), 2, samplers);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::BindSamplers(lastUnit, 2, samplers);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
MG_Impl::GLImpl::BindSampler(lastUnit, 0);
|
||||
MG_Impl::GLImpl::BindSampler(static_cast<GLuint>(kCombinedUnits - 2), 0);
|
||||
MG_Impl::GLImpl::DeleteSamplers(1, &sampler);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
@@ -135,16 +135,18 @@ TEST(ShaderCompilePoolLifecycle, DetectedThreadCountIsPositive) {
|
||||
EXPECT_GE(DetectShaderCompileThreadCount(), 1u);
|
||||
}
|
||||
|
||||
TEST(ShaderCompilePoolLifecycle, AsyncIsOffByDefaultAndTheOverrideDecidesEitherWay) {
|
||||
// The shipped default is still off, and an unset MOBILEGL_ASYNC_SHADER_COMPILE resolves
|
||||
// to it. If the first expectation ever fails without the constant having been
|
||||
// deliberately flipped, something enabled async by accident.
|
||||
TEST(ShaderCompilePoolLifecycle, AsyncIsOnByDefaultAndTheOverrideDecidesEitherWay) {
|
||||
// The shipped default flipped to ON at stage 7 (the GL30-40 + parallel_shader_compile
|
||||
// gate found zero async-attributable failures), and an unset
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE resolves to it. If the first expectation ever fails
|
||||
// without the constant having been deliberately flipped back, something disabled async
|
||||
// by accident - the kill switch below is the supported way off.
|
||||
//
|
||||
// Driven through Features rather than read from it: from stage 3 on, the whole suite is
|
||||
// also run with MOBILEGL_ASYNC_SHADER_COMPILE=1 exported, so a test that simply asserted
|
||||
// "the resolved answer is false" would either fail there or - worse - silently pass in a
|
||||
// Driven through Features rather than read from it: the suite is also run with
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE exported both ways, so a test that simply asserted
|
||||
// the resolved answer would fail in one of those runs or - worse - silently pass in a
|
||||
// binary that never loaded the config and prove nothing at all.
|
||||
EXPECT_FALSE(kAsyncShaderCompileDefault);
|
||||
EXPECT_TRUE(kAsyncShaderCompileDefault);
|
||||
|
||||
const MG_Config::QuirkOverride saved = MG_Config::Features.AsyncShaderCompile;
|
||||
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::Auto;
|
||||
|
||||
@@ -1290,3 +1290,77 @@ TEST_F(GeneralVertexArrayTest, ArrayFormat_IntegerPathRejectsPackedAndBgra) {
|
||||
EXPECT_FALSE(a0.IsBgra);
|
||||
}
|
||||
|
||||
|
||||
// The integer path takes exactly the eight signed/unsigned integer types (GL 4.6 core 10.3.2).
|
||||
// The old check was a blacklist of Unknown + the packed types, so GL_FLOAT / GL_HALF_FLOAT /
|
||||
// GL_DOUBLE / GL_FIXED all converted to a valid DataType and were silently recorded as integer
|
||||
// attributes.
|
||||
TEST_F(GeneralVertexArrayTest, ArrayFormat_IntegerPathRejectsFloatTypes) {
|
||||
CreateVAO();
|
||||
CreateVBO(GL_ARRAY_BUFFER, 64);
|
||||
|
||||
// Establish a known-good integer format first, so a rejected call is visible as "unchanged".
|
||||
VertexAttribIPointer(0, 4, GL_INT, 0, nullptr);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
const GLenum floatTypes[] = {GL_FLOAT, GL_HALF_FLOAT, GL_DOUBLE, GL_FIXED};
|
||||
for (GLenum type : floatTypes) {
|
||||
VertexAttribIPointer(0, 4, type, 0, nullptr);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_ENUM) << "type " << type << " was accepted on the integer path";
|
||||
const auto& attribute = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0);
|
||||
EXPECT_EQ(attribute.Type, DataType::Int32) << "a rejected format must not take effect";
|
||||
EXPECT_TRUE(attribute.IsInteger);
|
||||
}
|
||||
|
||||
// The float path still accepts them.
|
||||
VertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0).Type, DataType::Float32);
|
||||
}
|
||||
|
||||
// ARB_vertex_attrib_binding's two per-attribute queries. They were missing from
|
||||
// ValidateVertexAttribPname (so glGetVertexAttribiv answered INVALID_ENUM) and from
|
||||
// glGetVertexArrayIndexediv's switch (GL_VERTEX_ATTRIB_BINDING only).
|
||||
TEST_F(GeneralVertexArrayTest, ArrayFormat_BindingAndRelativeOffsetAreQueryable) {
|
||||
const GLuint vao = CreateVAO();
|
||||
CreateVBO(GL_ARRAY_BUFFER, 256);
|
||||
|
||||
VertexAttribFormat(2, 3, GL_FLOAT, GL_FALSE, 12);
|
||||
VertexAttribBinding(2, 5);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint binding = -1;
|
||||
GetVertexAttribiv(2, GL_VERTEX_ATTRIB_BINDING, &binding);
|
||||
EXPECT_EQ(binding, 5);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint relativeOffset = -1;
|
||||
GetVertexAttribiv(2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &relativeOffset);
|
||||
EXPECT_EQ(relativeOffset, 12);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// The float and double views convert the same value.
|
||||
GLfloat bindingAsFloat = -1.0f;
|
||||
GetVertexAttribfv(2, GL_VERTEX_ATTRIB_BINDING, &bindingAsFloat);
|
||||
EXPECT_FLOAT_EQ(bindingAsFloat, 5.0f);
|
||||
GLdouble offsetAsDouble = -1.0;
|
||||
GetVertexAttribdv(2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &offsetAsDouble);
|
||||
EXPECT_DOUBLE_EQ(offsetAsDouble, 12.0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// The by-name (DSA) indexed query answers both as well.
|
||||
GLint namedBinding = -1;
|
||||
GetVertexArrayIndexediv(vao, 2, GL_VERTEX_ATTRIB_BINDING, &namedBinding);
|
||||
EXPECT_EQ(namedBinding, 5);
|
||||
GLint namedRelativeOffset = -1;
|
||||
GetVertexArrayIndexediv(vao, 2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &namedRelativeOffset);
|
||||
EXPECT_EQ(namedRelativeOffset, 12);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// An attribute nobody re-bound keeps the default attribute-i -> binding-i mapping.
|
||||
GetVertexAttribiv(1, GL_VERTEX_ATTRIB_BINDING, &binding);
|
||||
EXPECT_EQ(binding, 1);
|
||||
GetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &relativeOffset);
|
||||
EXPECT_EQ(relativeOffset, 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
// time. Do not add one here.
|
||||
|
||||
namespace MobileGL::MG_Util::Async {
|
||||
// Stage 1 ships the whole machinery switched off: the pool is constructible and tested,
|
||||
// but nothing in the GL pipeline posts to it. The flip to true happens only after the
|
||||
// real-client soak in the final stage, because the riskiest part of asynchronous
|
||||
// compilation is not the joins - it is that Iris and Sodium change their submission
|
||||
// schedule the moment GL_KHR_parallel_shader_compile is advertised, and a recorded trace
|
||||
// can never cover that path.
|
||||
inline constexpr Bool kAsyncShaderCompileDefault = false;
|
||||
// Stage 7: on by default. The gate behind the flip (2026-08-09, headless Mesa, both
|
||||
// backends): GL30-40 mustpass + KHR-GL46.parallel_shader_compile at async=1 with the
|
||||
// extension advertised - 58,344 case-runs, 8 failures, and every one of the 8 also
|
||||
// fails standalone at async=0 and under the pre-P1 library, i.e. zero async-attributable
|
||||
// deltas. The risk this comment used to name - Iris and Sodium changing their submission
|
||||
// schedule the moment GL_KHR_parallel_shader_compile is advertised - remains the one
|
||||
// thing a recorded trace cannot cover, which is why the kill switch below stays.
|
||||
inline constexpr Bool kAsyncShaderCompileDefault = true;
|
||||
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE forces the answer either way; unset keeps the built-in
|
||||
// default above. Falsy is a complete kill switch: it reverts the threading *and*
|
||||
|
||||
Reference in New Issue
Block a user