Compare commits

..
57 Commits
Author SHA1 Message Date
swung0x48 01d20e5c96 [Test] (Review): border-colour clamping, a readback across a mip gap, and blending on renderbuffer formats with no exact VkFormat 2026-08-27 08:35:06 -04:00
swung0x48 b04c67d9a8 [Test] (Review): pin the sampler getter matrix, the shadow-readback refusals, and the proxy and by-name parameter targets 2026-08-27 08:35:05 -04:00
swung0x48 685d83e3ec [Fix] (Review): bound a texture readback by the image's mip count, refuse cross-format depth/stencil copies, and probe the renderbuffer's real VkFormat 2026-08-27 08:35:05 -04:00
swung0x48 009b140691 [Fix] (Review): clamp integer and floating-point border colours to the sampled format's representable range 2026-08-27 08:35:05 -04:00
swung0x48 1f44e5bc1d [Fix] (Review): convert every scalar sampler query to the type the caller asked for instead of the other type's bit pattern 2026-08-27 08:35:04 -04:00
swung0x48 d9aebcba26 [Fix] (Review): refuse a CPU-shadow readback whose layout the pack path cannot produce, and apply the parameter-target rule to proxy and by-name spellings 2026-08-27 08:35:04 -04:00
swung0x48 0db666897e [Fix] (DirectGLES): key the border-colour sync memo on the authoritative representation, not just the float one 2026-08-27 08:35:03 -04:00
swung0x48 5f445e499f [Test] (Integration): integer border-colour sampling and clear-tex-image on a texture with no level 0 2026-08-27 08:35:03 -04:00
swung0x48 c52ebd5bf6 [Test] (Texture): pin the border-colour forms, the GL 4.6 conversion pair and the texparameter validation gaps 2026-08-27 08:35:02 -04:00
swung0x48 8e072bc793 [Fix] (DirectVulkan): answer a texture-image query from the CPU shadow when the texture has no VkImage 2026-08-27 08:35:02 -04:00
swung0x48 88ee75be0e [Fix] (DirectVulkan): resolve renderbuffer VkFormats through the shared texture table and police copy size-compatibility 2026-08-27 08:35:01 -04:00
swung0x48 0dbb4ceba8 [Fix] (DirectVulkan): resolve arbitrary and integer border colours through VK_EXT_custom_border_color, clamped to the sampled format 2026-08-27 08:35:01 -04:00
swung0x48 a94b3e0bd5 [Fix] (DirectGLES): forward an integer border colour through glTexParameterIiv/glSamplerParameterIiv instead of flattening it to float 2026-08-27 08:35:01 -04:00
swung0x48 764b6e044d [Fix] (Texture): reject illegal texparameter targets and sampler enum values, invert the integer border-colour read, and report multisample sampler state as INVALID_ENUM 2026-08-27 08:35:00 -04:00
swung0x48 c1d89de729 [Fix] (Sampler): carry GL_TEXTURE_BORDER_COLOR with its form, convert per GL 4.6 eq 2.2/2.3, and unify the name and scalar-pname error classes 2026-08-27 08:35:00 -04:00
swung0x48 c136384f97 [CI] (TraceReplay): keep the rd12-odinlite perf fixture out of the CI matrices until its archive is published 2026-08-27 08:23:04 -04:00
swung0x48 1920a3d16f [Test] (Review): pin the composite's linked capture list, the capture-stage rule, and the matrix uniform forms' link check 2026-08-27 06:00:54 -04:00
swung0x48 11f4b4bd3b [Fix] (Review): honour a SPIR-V module's transform-feedback decorations, take the composite's capture list from the linked snapshot, and reorder every glProgramUniformMatrix* link check 2026-08-27 05:56:45 -04:00
swung0x48 9ef33f4274 [Fix] (Review): bound copies by the requested level, reach every cube face, keep array layer counts, and give glSpecializeShader its spec error surface 2026-08-27 05:51:58 -04:00
swung0x48 e430e1b3be [Fix] (Texture): give glTexBuffer the sized-format check its TODO deferred and the target-taking forms their own INVALID_ENUM 2026-08-27 05:37:18 -04:00
swung0x48 e315d9e798 [Test] (GlSpirv): unit and headless-GPU coverage for glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state 2026-08-27 05:37:18 -04:00
swung0x48 6f299372c6 [Feat] (Program): implement GL_ARB_gl_spirv - glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state, feeding the module into the ordinary compile pipeline 2026-08-27 05:37:17 -04:00
swung0x48 be7bf21eb8 [Fix] (ShaderTranspiler): enforce the layout(binding) range rule for samplers, images and uniform/atomic-counter blocks, not only for SSBOs 2026-08-27 05:37:17 -04:00
swung0x48 0e4302b399 [Feat] (RenderState): implement glClipControl, glPolygonOffsetClamp and glTextureBarrier instead of stubbing them 2026-08-27 05:37:16 -04:00
swung0x48 c9c2dcb42a [Feat] (Query): accept the ARB_pipeline_statistics_query targets and report zero counter bits for them 2026-08-27 05:37:15 -04:00
swung0x48 2d938971b9 [Fix] (Getter): saturate combined uniform components in 64-bit and answer the missing GL4 state tokens 2026-08-27 05:37:15 -04:00
swung0x48 f7e23d5d83 [Fix] (DirectVulkan): generate mipmaps for cube-map-array and 1D-array targets, and stop shrinking an array's layer count down the chain 2026-08-27 05:37:14 -04:00
swung0x48 02fbb816e9 [Fix] (Texture): cube-map-array shape rules on glTexImage3D, GL_TEXTURE_SHARED_SIZE, and the unimplemented glCopyTexSubImage1D/3D 2026-08-27 05:37:14 -04:00
swung0x48 0e0882cfc6 [Test] (IntegrationTest): read cube faces back through a per-face FBO, which glGetTexImage's face token does not distinguish on DirectVulkan 2026-08-27 05:07:38 -04:00
swung0x48 de09646d5e [Fix] (DirectVulkan): one shared attachment layer count for the render pass and the clear key, and direct VkResult checks in the render-pass builder 2026-08-27 05:04:44 -04:00
swung0x48 747864777e [Test] (IntegrationTest): pin the layered 3D and cube-map-array attachment shapes and their per-layer routing 2026-08-27 04:37:29 -04:00
swung0x48 6fc3504bd9 [Fix] (Framebuffer): carry the layered flag through the GL_DEPTH_STENCIL_ATTACHMENT split 2026-08-27 04:37:28 -04:00
swung0x48 df1bcdba09 [Fix] (DirectVulkan): legal view types for layered 3D/cube attachments, and a real failure channel for the render-pass builder 2026-08-27 04:19:06 -04:00
swung0x48 843c61dee1 [Chore] (ShaderTranspiler): pin glslang fork at 7e255451 (GL_ARB_cull_distance registration and version-gated availability) 2026-08-27 04:01:43 -04:00
swung0x48 c0a3f4cc50 [Docs] (Getter): restate the uniform-binding airtightness argument against the per-stage sum, not the combined limit 2026-08-27 03:54:17 -04:00
swung0x48 06744fde7f [Fix] (Getter): close the review findings - six-stage combined uniform blocks, bounded uniform-block bindings, honest vertex-stream count, per-format sample ceilings 2026-08-27 03:54:16 -04:00
swung0x48 6cc9faf772 [Feat] (Program): report the geometry and tessellation link properties glGetProgramiv had no source for 2026-08-27 03:49:38 -04:00
swung0x48 7168f2ef77 [Fix] (Getter): answer the GL 4.6 limit surface honestly - tess/cull/subroutine pnames, TBuiltInResource drift, 84 UBO binding points, 64-bit GL_MAX_ELEMENT_INDEX, per-category sample truth 2026-08-27 03:47:16 -04:00
swung0x48 07669aacd4 [Fix] (ShaderTranspiler): fan #extension implications out to both new gates and bound the mid-line #version probe to its line 2026-08-27 03:35:58 -04:00
swung0x48 d52a3b2196 [Test] (IntegrationTest): sample-shading state reaches both backends without disturbing the draw 2026-08-27 03:35:58 -04:00
swung0x48 9e23016dd4 [Perf] (ShaderTranspiler): memchr the mid-line #version scan and gate it on an accepted directive 2026-08-27 03:35:57 -04:00
swung0x48 f1354dc25e [Fix] (DirectVulkan): hash the sample-shading state into the pipeline memo key 2026-08-27 03:35:56 -04:00
swung0x48 b7a694711a [Test] (IntegrationTest): gl_NumSamples scenario - the value must follow the draw framebuffer, not the link 2026-08-27 03:35:56 -04:00
swung0x48 2ae848ca19 [Fix] (ShaderTranspiler): route restored ES extension macros through glslang's custom preamble and detect a mid-line repeated #version 2026-08-27 03:35:55 -04:00
swung0x48 acf86d1fb6 [Feat] (RenderState): implement glMinSampleShading and the GL_SAMPLE_SHADING enable on both backends 2026-08-27 03:35:55 -04:00
swung0x48 07a0408a28 [Fix] (ShaderTranspiler): lower gl_NumSamples onto a reserved global-UBO uniform, restore ES preamble extension macros, tolerate a repeated #version 2026-08-27 03:35:47 -04:00
Swung0x48 8cf2e2aea9 [Fix] (Getter): answer GL_PATCH_DEFAULT_*_LEVEL from the float state in glGetBooleanv and write every component in glGetInteger64v 2026-08-27 03:18:15 -04:00
Swung0x48 31252cf0da [Fix] (Tessellation): bound the pass-through control-stage cache and stop baking "draw nothing" for levels GL clamps 2026-08-27 03:18:14 -04:00
Swung0x48 2635fe84b6 [Fix] (Tessellation): compare the default patch levels by bit pattern, so a NaN level stops re-linking the program on every draw 2026-08-27 03:18:13 -04:00
Swung0x48 e3163233a5 [Fix] (Framebuffer): apply the glFramebufferTexture error conditions to the 2D/3D/Layer attach paths and bound a view by its own level count 2026-08-27 03:18:12 -04:00
Swung0x48 eb9e4fdac1 [Fix] (DirectVulkan): report the index the arbitrary-restart rewrite cannot represent instead of silently drawing a different vertex 2026-08-27 03:18:10 -04:00
Swung0x48 90b7a689c5 [Fix] (Backend): resolve primitive restart per draw - never for a non-indexed one, and never on an index the type cannot hold 2026-08-27 03:18:09 -04:00
Swung0x48 e69e939d1a [Fix] (ProgramLink): fail the link when a tessellation control stage declares more output vertices than GL_MAX_PATCH_VERTICES 2026-08-27 02:11:23 -04:00
Swung0x48 b675e2a0b0 [Fix] (Drawing): refuse a draw whose program runs a geometry or tessellation stage with no vertex shader 2026-08-27 02:11:21 -04:00
Swung0x48 6979926a6f [Fix] (Framebuffer): the four glFramebufferTexture error conditions the DSA sibling already implemented 2026-08-27 02:11:20 -04:00
Swung0x48 d5286e69b6 [Feat] (Tessellation): implement glPatchParameterfv and bake the default levels into both pass-through control stages 2026-08-27 02:11:19 -04:00
Swung0x48 0f2fcbc469 [Fix] (Backend): honour desktop GL_PRIMITIVE_RESTART with an arbitrary index instead of throwing through the C GL ABI 2026-08-27 02:11:18 -04:00
101 changed files with 13467 additions and 800 deletions
+13
View File
@@ -389,6 +389,19 @@ namespace MobileGL {
// where there is no device to be honest about and BuildTBuiltInResource still has to // where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances. // hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8; Int MaxClipDistances = 8;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16; Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a // GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes // primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
@@ -307,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
return capabilities.MaxColorTextureSamples; return capabilities.MaxColorTextureSamples;
} }
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
// in the GLES loader) and the walk below would never run past one sample, recording {1}
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
// reports 4. Once the frontend validates against this list, that would reject every
// multisample renderbuffer on such a context.
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
GLenum imageFormat) {
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
}
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLuint texture, TextureInternalFormat format) { GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0; GLuint framebuffer = 0;
@@ -717,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex, AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat)); GetRenderbufferFeatureCaps(logicalFormat));
const Int maxSamples = const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat); GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples); ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
} else { } else {
@@ -731,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo); LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
} }
const Int maxSamples = const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat); GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts( cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples); gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
} }
@@ -1465,6 +1482,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
// would reject.
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports; m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX // Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test // for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
+402 -33
View File
@@ -351,9 +351,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
// Only sync up to the high-water mark of app-touched points; the fixed array is 36 // Only sync up to the high-water mark of app-touched points; the fixed array is 84
// deep but apps bind a handful, so the never-touched tail is already at GL default 0. // deep but apps bind a handful, so the never-touched tail is already at GL default 0.
auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target); auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
// by stopping: this frontend-indexed pass exists for the compute path, and the
// per-program rebind in BindCurrentProgramWithResources - which is what actually feeds
// a shader - remaps every block a program declares onto a compacted ES point, so a
// block bound at GL point 83 still reaches its shader.
if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) {
bindingPointCnt = std::min(bindingPointCnt,
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
}
for (SizeT i = 0; i < bindingPointCnt; ++i) { for (SizeT i = 0; i < bindingPointCnt; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i); auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
auto& obj = point.GetBoundObject(); auto& obj = point.GetBoundObject();
@@ -2309,6 +2320,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core)
// Both halves are gated on the same entry point rather than on a version check:
// GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core /
// OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would
// only push an INVALID_ENUM into the driver's queue. This is NOT part of the
// SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to
// put a guard.
if (g_GLESFuncs.glMinSampleShading) {
if (forceFullPush ||
parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) {
if (parameters.SampleShadingEnabled) {
g_GLESFuncs.glEnable(GL_SAMPLE_SHADING);
} else {
g_GLESFuncs.glDisable(GL_SAMPLE_SHADING);
}
}
if (forceFullPush || parameters.MinSampleShadingValue !=
g_syncedRenderStateParameters.MinSampleShadingValue) {
g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue);
}
}
}
g_syncedRenderStateVersion = currentRenderStateVersion; g_syncedRenderStateVersion = currentRenderStateVersion;
// Byte copy, not member copy: it also clones the frontend struct's padding bytes, // Byte copy, not member copy: it also clones the frontend struct's padding bytes,
// which is what lets the span memcmps above answer "unchanged" exactly instead of // which is what lets the span memcmps above answer "unchanged" exactly instead of
@@ -2461,9 +2495,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the // `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
// built program wrong. -1 is "this program needed no such stage", which compares // built program wrong. -1 is "this program needed no such stage", which compares
// equal to itself and costs every other program one integer test. // equal to itself and costs every other program one integer test.
//
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL are baked into the same stage for the same
// reason (ES has neither the state nor an entry point), so glPatchParameterfv
// makes it stale too. Both level comparisons sit INSIDE the >= 0 guard: a program
// with a control stage of its own - which is nearly all of them - still pays only
// the one integer test.
//
// Compared by BIT PATTERN, matching what DirectVulkan hashes into its module key.
// A float compare here would never settle for a NaN level - NaN != NaN - and every
// draw of that program would re-transpile, re-compile and re-link a byte-identical
// shader. glPatchParameterfv accepts NaN by design.
//
// The gl_PerVertex MEMBER SET needs no clause of its own here, and that asymmetry
// with DirectVulkan is deliberate rather than an omission. It can only change with
// the evaluation stage, i.e. across a relink - which the link-version test at the
// top of this condition already catches - and this backend never invents the shape
// in the first place: AttachPassthroughTessControlStage extracts the member text
// out of the neighbouring stages' emitted ESSL on every rebuild
// (ExtractPerVertexBlockMembers, "mirrored, never invented"). DirectVulkan needs
// the mask in its key precisely because it does NOT mirror - it redeclares from a
// member set it has to be told.
(twin->GetPassthroughTessControlPatchVertices() >= 0 && (twin->GetPassthroughTessControlPatchVertices() >= 0 &&
twin->GetPassthroughTessControlPatchVertices() != (twin->GetPassthroughTessControlPatchVertices() !=
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) { static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
twin->SyncToBackend(currentProgram); twin->SyncToBackend(currentProgram);
} }
g_currentDrawFrontendProgram = currentProgram.get(); g_currentDrawFrontendProgram = currentProgram.get();
@@ -3524,6 +3583,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer, const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
GLsizei drawcount, GLsizei stride, const char* label) { GLsizei drawcount, GLsizei stride, const char* label) {
(void)label; (void)label;
// An indirect command's firstIndex/count live in GPU memory, so the substitution has
// to rewrite the whole element array buffer rather than this draw's range - which is
// exactly what it does when no CPU-known count is handed to it. Held for the whole
// command loop so every command in the batch reads the rewritten copy.
//
// firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not
// survive is the type and the element size, which are re-taken from the substitution
// below for both the native and the CPU-unrolled path.
const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr);
if (!restart.DrawIsValid()) return;
type = restart.IndexType();
indexSize = MG_Util::GetGLTypeSize(type);
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
if (useNative) { if (useNative) {
// gl_BaseInstance must observe GPU-written command fields; expose the indirect // gl_BaseInstance must observe GPU-written command fields; expose the indirect
@@ -3829,29 +3900,308 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed all-ones value). If the app // ---------------------------------------------------------------------------
// enabled the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index, hard-fail at this draw with // Arbitrary-index primitive restart
// the reason (a fallback would silently drop restarts and corrupt geometry). //
void CheckPrimitiveRestartSupported(GLenum indexType) { // Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only
// ever restarts on the all-ones value of the index type. When the two agree - which
// includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at
// SyncRenderState is the whole implementation and nothing here does any work. When they
// disagree the index DATA is rewritten into a scratch element array buffer.
//
// This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI
// and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp
// already call out - so an application that merely asked for a legal desktop feature
// died rather than got an error.
// ---------------------------------------------------------------------------
namespace {
struct RestartScratchBuffer {
Uint id = 0;
SizeT capacity = 0;
};
RestartScratchBuffer g_restartIndices;
Vector<Uint8> g_restartStaging;
// Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY
// draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of
// trying: a draw that renders nothing is recoverable, a stall of that size is not.
constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB
// The index type one step wider than this one, or 0 when there is none. Widening is how
// an all-ones value that is a REAL vertex index keeps its meaning while the all-ones
// value of the destination type serves as the restart sentinel: a source that cannot
// spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16.
GLenum WiderIndexType(GLenum indexType) {
switch (indexType) {
case GL_UNSIGNED_BYTE: return GL_UNSIGNED_SHORT;
case GL_UNSIGNED_SHORT: return GL_UNSIGNED_INT;
default: return 0;
}
}
Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) {
switch (indexSize) {
case 1: return source[i];
case 2: {
Uint16 narrow = 0;
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
return narrow;
}
default: {
Uint32 wide = 0;
std::memcpy(&wide, source + i * 4, sizeof(wide));
return wide;
}
}
}
void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) {
switch (indexSize) {
case 1: destination[i] = static_cast<Uint8>(value); break;
case 2: {
const Uint16 narrow = static_cast<Uint16>(value);
std::memcpy(destination + i * 2, &narrow, sizeof(narrow));
break;
}
default: std::memcpy(destination + i * 4, &value, sizeof(value)); break;
}
}
// True when any index in the range already holds the type's all-ones value, i.e. when
// that value is doing double duty as a real vertex index and so cannot also be the
// restart sentinel. Only asked on the rare substitution path.
Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize,
Uint32 fixedMax) {
for (SizeT i = 0; i < indexCount; ++i) {
if (ReadIndex(source, i, indexSize) == fixedMax) return true;
}
return false;
}
// Copies index data, replacing every occurrence of the application's restart index with
// the all-ones value of the DESTINATION type - the only one GLES restarts on. The
// destination may be wider than the source, which is what makes the copy lossless: a
// source index equal to the source's all-ones value zero-extends to something the wider
// sentinel can never equal, so it stays the vertex it was.
//
// Same width in and out is the degenerate case, used when the source contains no
// all-ones index at all (nothing to protect) or when there is no wider type to move to.
// In that last case only - a GL_UNSIGNED_INT stream that really does use index
// 0xFFFFFFFF while asking to restart on a different one - a legal index has to be
// nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it;
// it is the one input this feature cannot represent.
void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize,
SizeT destinationIndexSize, Uint32 applicationRestartIndex,
Uint32 destinationFixedMax, Vector<Uint8>& output) {
output.resize(indexCount * destinationIndexSize);
for (SizeT i = 0; i < indexCount; ++i) {
Uint32 value = ReadIndex(source, i, sourceIndexSize);
if (value == applicationRestartIndex) {
value = destinationFixedMax;
} else if (value == destinationFixedMax) {
// Only reachable when no widening was possible; see above.
value = destinationFixedMax - 1;
}
WriteIndex(output.data(), i, destinationIndexSize, value);
}
}
// Whole-buffer respecify through the manager-wide staging target, so binding it
// disturbs no VAO state. glBufferData orphans the previous store, so the upload
// never waits on a draw still reading the old contents out of the same name.
Bool UploadRestartScratch(SizeT bytes, const void* data) {
if (g_restartIndices.id == 0) {
GLuint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) return false;
g_restartIndices.id = id;
g_restartIndices.capacity = 0;
}
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id);
SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity;
while (capacity < bytes) capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
g_restartIndices.capacity = capacity;
if (data != nullptr && bytes != 0) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
}
return true;
}
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject();
}
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what the
// substitution has to put back.
Uint BoundElementArrayBufferId() {
const auto& ibo = BoundElementArrayBuffer();
if (!ibo) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
return resource ? resource->id : 0;
}
} // namespace
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
return RestartSubstitutionKind::None;
}
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
if (fixedMax == 0) return RestartSubstitutionKind::None;
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
// nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data
// becoming "restart on 0" - turns the most common index in any mesh into a restart.
if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart;
return RestartSubstitutionKind::RewriteIndices;
}
void OnRestartSubstitutionContextDestroyed() {
g_restartIndices = {};
g_restartStaging.clear();
g_restartStaging.shrink_to_fit();
}
ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) {
if (kind != RestartSubstitutionKind::SuppressRestart) return;
// SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART
// is enabled; for this draw's index type it would restart on a value the application
// never named. Toggled directly rather than through the render-state shadow, and put back
// in the destructor, so the shadow stays true and the next draw pays nothing.
g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
m_suppressed = true;
}
ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() {
if (!m_suppressed) return;
g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
}
ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count,
const void* indices)
: m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices),
m_indexType(indexType) {
if (m_kind != RestartSubstitutionKind::RewriteIndices) {
return; return;
} }
Uint32 fixedMax = 0; const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
switch (indexType) { const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
case GL_UNSIGNED_BYTE: fixedMax = 0xFFu; break; const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
case GL_UNSIGNED_SHORT: fixedMax = 0xFFFFu; break; const auto& indexBuffer = BoundElementArrayBuffer();
case GL_UNSIGNED_INT: fixedMax = 0xFFFFFFFFu; break;
default: return; const Uint8* source = nullptr;
SizeT indexCount = 0;
SizeT sourceByteOffset = 0;
if (indexBuffer) {
// The WHOLE buffer is rewritten, not just this draw's range, so that every index
// keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be
// adjusted from here. It is an ELEMENT index, so it survives widening unchanged.
const SizeT sizeBytes = indexBuffer->GetSize();
if (sizeBytes < sourceIndexSize) {
return; // Nothing to restart on; let the driver see the draw unchanged.
}
if (sizeBytes > kMaxRestartRewriteBytes) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element "
"array buffer rewritten every draw, which is past the %zu-byte ceiling. Use "
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones "
"value of the index type.",
applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes);
m_valid = false;
return;
}
// The shadow is the source of truth for CPU reads, but a persistent map or a
// shader write may have moved past it since the last sync.
indexBuffer->SyncPersistentMappedRange();
indexBuffer->SyncGpuWrites();
source = indexBuffer->MappedData();
if (source == nullptr) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
"the bound element array buffer and none is available.",
applicationRestartIndex);
m_valid = false;
return;
}
indexCount = sizeBytes / sourceIndexSize;
sourceByteOffset = reinterpret_cast<SizeT>(indices);
} else {
// No element array buffer: `indices` is a client pointer, so only the draw's own
// range is readable and an indirect draw has nothing to read at all.
if (count <= 0 || indices == nullptr || sourceIndexSize == 0) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element "
"array buffer or a client index array with a CPU-known count.",
applicationRestartIndex);
m_valid = false;
return;
}
if (static_cast<SizeT>(count) * sourceIndexSize > kMaxRestartRewriteBytes) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte "
"ceiling.",
static_cast<SizeT>(count) * sourceIndexSize, kMaxRestartRewriteBytes);
m_valid = false;
return;
}
source = static_cast<const Uint8*>(indices);
indexCount = static_cast<SizeT>(count);
} }
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
if (restartIndex != fixedMax) { // Widen only when the source really does use the all-ones value as a vertex index -
THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + std::to_string(restartIndex) + // otherwise the sentinel is free and the copy stays the caller's width, which keeps the
") is not supported by the GLES backend, which only restarts on the fixed index value (" + // common substitution allocation-for-allocation identical to the narrow form.
std::to_string(fixedMax) + GLenum destinationType = indexType;
") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX or set glPrimitiveRestartIndex " SizeT destinationIndexSize = sourceIndexSize;
"to that value."); if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) {
const GLenum wider = WiderIndexType(indexType);
// An element-array offset that is not a whole number of indices cannot be rescaled
// into the widened copy, so such a draw keeps the narrow (lossy) form.
const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0;
if (wider != 0 && offsetIsWholeIndices &&
indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) {
destinationType = wider;
destinationIndexSize = MG_Util::GetGLTypeSize(wider);
} else {
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the "
"all-ones index %u: this index type cannot spell both, so every all-ones index is "
"drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones "
"value out of the index data.",
applicationRestartIndex, fixedMax, fixedMax - 1);
}
} }
const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType);
RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex,
destinationFixedMax, g_restartStaging);
if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) {
MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART "
"index substitution.");
m_valid = false;
return;
}
m_previousBinding = BoundElementArrayBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
m_substituted = true;
m_indexType = destinationType;
// The rewritten copy starts at byte 0 of the scratch buffer and holds one
// destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT
// offset (rescaled to the new width) and a client-memory draw reads from the front.
m_indices = indexBuffer
? reinterpret_cast<const void*>((sourceByteOffset / sourceIndexSize) * destinationIndexSize)
: nullptr;
}
ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() {
if (!m_substituted) return;
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding);
} }
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
@@ -3860,9 +4210,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif #endif
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type); const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, count, type, indices); g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices());
}); });
} }
@@ -3890,10 +4241,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif #endif
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type); const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex); SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex); g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex);
}); });
SetCurrentBaseVertex(0); SetCurrentBaseVertex(0);
} }
@@ -4158,9 +4510,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* indices, GLint basevertex) { const void* indices, GLint basevertex) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex); SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(),
basevertex);
}); });
SetCurrentBaseVertex(0); SetCurrentBaseVertex(0);
} }
@@ -4168,8 +4523,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices); g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices());
}); });
} }
@@ -4191,14 +4548,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance)); const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseInstance(baseinstance); SetCurrentBaseInstance(baseinstance);
SetCurrentBaseVertex(basevertex); SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) { if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount, g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(),
restart.Indices(), instancecount,
basevertex, baseinstance); basevertex, baseinstance);
} else { } else {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(),
instancecount, basevertex);
} }
}); });
SetCurrentBaseVertex(0); SetCurrentBaseVertex(0);
@@ -4209,9 +4570,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei instancecount, GLint basevertex) { GLsizei instancecount, GLint basevertex) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex); SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount,
basevertex);
}); });
SetCurrentBaseVertex(0); SetCurrentBaseVertex(0);
} }
@@ -4221,13 +4585,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance)); const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseInstance(baseinstance); SetCurrentBaseInstance(baseinstance);
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) { if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount, g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(),
baseinstance); instancecount, baseinstance);
} else { } else {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
} }
}); });
SetCurrentBaseInstance(0); SetCurrentBaseInstance(0);
@@ -4236,8 +4602,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit); PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
ForEachViewportRoutingPass([&] { ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
}); });
} }
@@ -9996,6 +10364,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::OnBackendContextDestroyed(); BufferImpl::OnBackendContextDestroyed();
XfbImpl::OnBackendContextDestroyed(); XfbImpl::OnBackendContextDestroyed();
MultiDrawImpl::OnBackendContextDestroyed(); MultiDrawImpl::OnBackendContextDestroyed();
OnRestartSubstitutionContextDestroyed();
ScratchFBOImpl::OnBackendContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed();
ReleasePackedWordScratchTexture(); ReleasePackedWordScratchTexture();
FramebufferImpl::InvalidateFramebufferBindingCache(); FramebufferImpl::InvalidateFramebufferBindingCache();
+88 -15
View File
@@ -4605,12 +4605,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver // GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver
// without it every such call is INVALID_ENUM, so the parameter is simply not synced. // without it every such call is INVALID_ENUM, so the parameter is simply not synced.
//
// The FORM has to be forwarded along with the value. A border colour set through
// glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the
// border returns whatever the driver's integer border register holds - so pushing it
// through glTexParameterfv handed the driver float 255.0 and the shader read back
// 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core
// beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate;
// the entry-point null check covers a driver that advertises the extension without them.
// The redundancy filter has to look at the AUTHORITATIVE representation, not just the
// float one: two integer borders that differ above 2^24 (16777216 and 16777217, say)
// collapse onto the same float, so a float-only comparison would skip the second sync and
// leave the driver holding the first value forever.
const auto borderColorForm = stateTextureObject->GetBorderColorForm();
if (!isMultisampleTarget && g_GLESCapabilities.SupportsTextureBorderClamp && if (!isMultisampleTarget && g_GLESCapabilities.SupportsTextureBorderClamp &&
m_cacheBorderColor != stateTextureObject->GetBorderColor()) { (m_cacheBorderColor != stateTextureObject->GetBorderColor() ||
const auto& borderColor = stateTextureObject->GetBorderColor(); m_cacheBorderColorI != stateTextureObject->GetBorderColorI() ||
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()}; m_cacheBorderColorUI != stateTextureObject->GetBorderColorUI() ||
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray); m_cacheBorderColorForm != borderColorForm)) {
m_cacheBorderColor = borderColor; if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) {
const auto& borderColorI = stateTextureObject->GetBorderColorI();
const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(),
borderColorI.w()};
g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
} else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) {
const auto& borderColorUI = stateTextureObject->GetBorderColorUI();
const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(),
borderColorUI.w()};
g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
} else {
const auto& borderColor = stateTextureObject->GetBorderColor();
const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(),
borderColor.w()};
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
}
m_cacheBorderColor = stateTextureObject->GetBorderColor();
m_cacheBorderColorI = stateTextureObject->GetBorderColorI();
m_cacheBorderColorUI = stateTextureObject->GetBorderColorUI();
m_cacheBorderColorForm = borderColorForm;
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
}); });
@@ -6730,6 +6762,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
? MG_State::pGLContext->GetPatchVertices() ? MG_State::pGLContext->GetPatchVertices()
: 3u; : 3u;
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices); m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
// into the same stage (ES has no such state and no entry point to forward them to), so
// they are recorded and compared alongside the patch size - the two move together, as
// BuildPassthroughTessControlEssl's contract says.
m_passthroughTessControlOuterLevel = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetPatchDefaultOuterLevel()
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetPatchDefaultInnerLevel()
: FloatVec2(1.0f, 1.0f);
if (tessEvalShaderIndex < 0 || if (tessEvalShaderIndex < 0 ||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) { static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
@@ -6770,8 +6812,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const String outMembers = const String outMembers =
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String()); ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
const String source = const String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers); inMembers, outMembers,
m_passthroughTessControlOuterLevel,
m_passthroughTessControlInnerLevel);
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER); const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
if (backendShaderId == 0) { if (backendShaderId == 0) {
@@ -6861,8 +6905,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop(); m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// Re-established by AttachPassthroughTessControlStage below when this program needs // Re-established by AttachPassthroughTessControlStage below when this program needs
// one; cleared first so a program that stops needing one (a relink that now attaches // one; cleared first so a program that stops needing one (a relink that now attaches
// a real control stage) does not keep comparing against a stale patch size. // a real control stage) does not keep comparing against a stale patch size. The
// default levels are re-established from the same call and gated on the same -1.
m_passthroughTessControlPatchVertices = -1; m_passthroughTessControlPatchVertices = -1;
m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
// The same shape again for image FORMATS: what a format-less image declaration // The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built // compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch). // against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
@@ -7901,15 +7948,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy; m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
} }
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor) { if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor ||
// Same gate as the texture-side border colour above. m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI ||
if (g_GLESCapabilities.SupportsTextureBorderClamp && g_GLESFuncs.glSamplerParameterfv) { m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI ||
const GLfloat borderColorArray[4] = { m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) {
samplerParams.borderColor.x(), samplerParams.borderColor.y(), // Same gate as the texture-side border colour above, and the same reason for
samplerParams.borderColor.z(), samplerParams.borderColor.w()}; // branching on the form: an integer border colour must reach the driver through
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, borderColorArray); // glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern
// back instead of the value.
if (g_GLESCapabilities.SupportsTextureBorderClamp) {
if (samplerParams.borderColorForm == BorderColorForm::Int &&
g_GLESFuncs.glSamplerParameterIiv) {
const GLint borderColorArray[4] = {
samplerParams.borderColorI.x(), samplerParams.borderColorI.y(),
samplerParams.borderColorI.z(), samplerParams.borderColorI.w()};
g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (samplerParams.borderColorForm == BorderColorForm::Uint &&
g_GLESFuncs.glSamplerParameterIuiv) {
const GLuint borderColorArray[4] = {
samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(),
samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()};
g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (g_GLESFuncs.glSamplerParameterfv) {
const GLfloat borderColorArray[4] = {
samplerParams.borderColor.x(), samplerParams.borderColor.y(),
samplerParams.borderColor.z(), samplerParams.borderColor.w()};
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
}
} }
m_cacheSamplerParameters.borderColor = samplerParams.borderColor; m_cacheSamplerParameters.borderColor = samplerParams.borderColor;
m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI;
m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI;
m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm;
} }
#undef SYNC_SAMPLER_PARAM_IF_CHANGED #undef SYNC_SAMPLER_PARAM_IF_CHANGED
m_isInitialized = true; m_isInitialized = true;
+114 -3
View File
@@ -102,9 +102,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Brings the whole draw-relevant frontend state onto the native ES context and binds // Brings the whole draw-relevant frontend state onto the native ES context and binds
// the program; every GL draw entry point calls it exactly once before issuing draws. // the program; every GL draw entry point calls it exactly once before issuing draws.
void PrepareForDraw(DrawSyncFlags syncBits); void PrepareForDraw(DrawSyncFlags syncBits);
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled // What an indexed draw has to do about primitive restart before it can be issued.
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type. //
void CheckPrimitiveRestartSupported(GLenum indexType); // Desktop GL restarts on an application-chosen index (glPrimitiveRestartIndex under
// GL_PRIMITIVE_RESTART); GLES core restarts only on the all-ones value of the index type
// (GL_PRIMITIVE_RESTART_FIXED_INDEX), which the render-state push enables for BOTH caps.
// That leaves three cases, and the difference between the last two is not cosmetic - one
// adds restarts, the other has to take away restarts the driver would otherwise make.
enum class RestartSubstitutionKind : Uint8 {
// Nothing to do: restart is off, the fixed-index cap is on, or the application's
// restart index already IS the type's all-ones value. The overwhelmingly common answer.
None,
// The application's index is representable in this index type and differs from the
// all-ones value: the index DATA has to be rewritten so the driver restarts where the
// application asked.
RewriteIndices,
// The application's index cannot be held by this index type at all. GL 4.6 core 10.3.6
// compares the fetched index, zero-extended, against the full 32-bit
// PRIMITIVE_RESTART_INDEX, so no index can match and the draw restarts NOWHERE - but the
// render-state push has already enabled the driver's fixed-index restart, so the
// all-ones value has to be un-restarted for the duration of the draw.
SuppressRestart,
};
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType);
// Turns the driver's fixed-index restart off for one draw and back on afterwards, for the
// SuppressRestart case above. Separate from the substitution below because the multi-draw
// tiers need it on its own: they rewrite the index stream themselves and only ever need the
// cap half. Inert for every other kind, and it never touches the render-state shadow - it
// puts the driver back exactly where SyncRenderState left it.
class ScopedSuppressedPrimitiveRestart {
public:
explicit ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind);
~ScopedSuppressedPrimitiveRestart();
ScopedSuppressedPrimitiveRestart(const ScopedSuppressedPrimitiveRestart&) = delete;
ScopedSuppressedPrimitiveRestart& operator=(const ScopedSuppressedPrimitiveRestart&) = delete;
private:
Bool m_suppressed = false;
};
// Swaps in a scratch element array buffer holding a copy of the index data in which the
// application's restart index has been replaced by the value GLES restarts on. Inert
// (and free) unless ResolveRestartSubstitution asks for it. The swap lives for the
// object's lifetime, so it covers every pass of a viewport-routed draw, and the previous
// GL_ELEMENT_ARRAY_BUFFER name is restored on destruction - which matters beyond tidiness,
// because the VAO twin memoises that it already synced that binding.
//
// The copy may be WIDER than the source (see IndexType): when the source already contains
// the type's all-ones value as an ordinary vertex index, that value cannot double as the
// restart sentinel, and widening is the only way to keep both meanings. Callers must
// therefore take the index type from this object, not from their own argument.
class ScopedRestartIndexSubstitution {
public:
// count/indices describe the draw's index range when the CPU knows it. Pass
// count == 0 for an indirect draw, whose count lives in GPU memory: the whole bound
// element array buffer is rewritten instead, so every element keeps its position and
// a GPU-resident firstIndex - an ELEMENT index, so it survives widening too - still
// addresses the index it named.
ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices);
~ScopedRestartIndexSubstitution();
ScopedRestartIndexSubstitution(const ScopedRestartIndexSubstitution&) = delete;
ScopedRestartIndexSubstitution& operator=(const ScopedRestartIndexSubstitution&) = delete;
// False only when a substitution was needed and could not be made. The draw must
// then be skipped: issuing it would let the driver silently drop every restart and
// weld the primitives on either side together, which is worse than drawing nothing.
Bool DrawIsValid() const { return m_valid; }
// The element-array offset (or client pointer) the draw must use. Identical to what
// was passed in unless a substitution was made.
const void* Indices() const { return m_indices; }
// The index type the draw must be issued with. Identical to the constructor's unless
// the copy had to be widened to keep an all-ones vertex index distinguishable from the
// restart sentinel.
GLenum IndexType() const { return m_indexType; }
private:
// Declared before m_capOverride so it is initialised first (members initialise in
// declaration order): the whole decision is made once, and both the cap override and the
// constructor body read the same answer.
RestartSubstitutionKind m_kind = RestartSubstitutionKind::None;
ScopedSuppressedPrimitiveRestart m_capOverride;
const void* m_indices = nullptr;
GLenum m_indexType = 0;
Uint m_previousBinding = 0;
Bool m_substituted = false;
Bool m_valid = true;
};
// Drops the scratch element array buffer the substitution above stages through. Like
// MultiDrawImpl's scratch names it is abandoned rather than deleted: the name belongs to
// the dead ES context, and deleting it would target whatever its successor handed out.
void OnRestartSubstitutionContextDestroyed();
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation // Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
// uniforms. All are no-ops when the program does not read the corresponding builtin. // uniforms. All are no-ops when the program does not read the corresponding builtin.
void SetCurrentBaseInstance(Uint32 baseInstance); void SetCurrentBaseInstance(Uint32 baseInstance);
@@ -961,7 +1050,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint16 m_syncedShapeParamsVersion = 0; Uint16 m_syncedShapeParamsVersion = 0;
SamplerParameters m_cacheSamplerParameters; SamplerParameters m_cacheSamplerParameters;
UintVec2 m_cacheLodRange = {0, 1000}; UintVec2 m_cacheLodRange = {0, 1000};
// All three representations plus the form, because none of them alone identifies the
// border colour the driver texture is holding: two integer borders can share one float
// (anything differing above 2^24), and a Float -> Int transition can leave every number
// unchanged while still needing a different driver entry point.
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f}; FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 m_cacheBorderColorI = {0, 0, 0, 0};
UintVec4 m_cacheBorderColorUI = {0, 0, 0, 0};
BorderColorForm m_cacheBorderColorForm = BorderColorForm::Float;
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha}; TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a // GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
@@ -1447,6 +1543,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int GetPassthroughTessControlPatchVertices() const { Int GetPassthroughTessControlPatchVertices() const {
return m_passthroughTessControlPatchVertices; return m_passthroughTessControlPatchVertices;
} }
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL the same synthesized stage was built with, for
// the same reason: ES has neither the state nor an entry point to forward it to, so
// glPatchParameterfv's values are compiled in as literals and a program built with one
// set is stale for another. Meaningless (and never read) when the patch-vertices field
// above is -1, which is the gate the draw path tests first.
const FloatVec4& GetPassthroughTessControlOuterLevel() const {
return m_passthroughTessControlOuterLevel;
}
const FloatVec2& GetPassthroughTessControlInnerLevel() const {
return m_passthroughTessControlInnerLevel;
}
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; } Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1547,6 +1654,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built // all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
// with. See GetPassthroughTessControlPatchVertices. // with. See GetPassthroughTessControlPatchVertices.
Int m_passthroughTessControlPatchVertices = -1; Int m_passthroughTessControlPatchVertices = -1;
// The default tessellation levels baked into that same stage. Only meaningful while
// the field above is not -1.
FloatVec4 m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
FloatVec2 m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
Bool m_isInitialized = false; Bool m_isInitialized = false;
Bool m_backendProgramUsable = false; Bool m_backendProgramUsable = false;
// Set by SyncToBackend every time it relinks the driver program, cleared by the // Set by SyncToBackend every time it relinks the driver program, cleared by the
+48 -14
View File
@@ -29,16 +29,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
} }
} }
// The all-ones value of an index type, which is what GL restarts on once // The index value this batch restarts on, compared at 32 bits against the zero-extended
// primitive restart is in play. CheckPrimitiveRestartSupported has already // source index. Normally the all-ones value of the source type, which is what
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled // GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
// restart always restarts here and nowhere else. // GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
// driver restarts on.
//
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
// restart index the source type cannot hold simply matches nothing, so returning it
// verbatim is already "this batch restarts nowhere".
Uint32 RestartSentinelFor(GLenum type) { Uint32 RestartSentinelFor(GLenum type) {
switch (type) { if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
case GL_UNSIGNED_BYTE: return 0xFFu; return MG_State::pGLContext->GetPrimitiveRestartIndex();
case GL_UNSIGNED_SHORT: return 0xFFFFu;
default: return 0xFFFFFFFFu;
} }
return MG_Util::FixedRestartIndexForGLType(type);
} }
Bool RestartActive() { Bool RestartActive() {
@@ -275,10 +280,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// its remaining feasibility checks inside its implementation, where the data it // its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand. // has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex, GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer) { Bool hasIndexBuffer, Bool arbitraryRestart) {
ResolveTierOnce(); ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier; GLESMultiDrawMode tier = g_resolvedTier;
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
// only ever restarts on the all-ones value. Every tier but the rebased one hands
// the application's own index data to the driver, which would then see no restarts
// at all and weld the primitives together. The rebased tier is the one that
// REWRITES the stream, and RestartSentinelFor already tells it which value to
// translate, so it is the only tier this batch can take.
if (arbitraryRestart) {
return GLESMultiDrawMode::DrawElements;
}
// Batched tiers issue one driver entry for the whole batch, so the emulated // Batched tiers issue one driver entry for the whole batch, so the emulated
// gl_DrawID uniform can only hold one value across every sub-draw. A program // gl_DrawID uniform can only hold one value across every sub-draw. A program
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its // that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
@@ -488,6 +503,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Bool restartActive = RestartActive(); const Bool restartActive = RestartActive();
const Uint32 restartSentinel = RestartSentinelFor(type); const Uint32 restartSentinel = RestartSentinelFor(type);
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
// a real vertex index while restarting on a different one is the one shape 32 bits
// cannot express - the same corner the single-draw substitution reports.
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
"has no wider sentinel to move to.",
restartSentinel);
}
g_indexStaging.resize(total); g_indexStaging.resize(total);
SizeT cursor = 0; SizeT cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
@@ -852,8 +877,14 @@ void main() {
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) { GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return; if (drawcount <= 0 || !count || !indices) return;
// State-independent and possibly throwing, so it runs before any GL work. // Read before any GL work, because it decides the tier below: a desktop restart index
CheckPrimitiveRestartSupported(type); // the driver does not know about can only be honoured by the tier that rewrites the
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
// needs no rewrite at all - nothing can match it - but it does need the driver's own
// fixed-index restart held off for the batch, which is what the scope below does.
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr; const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
@@ -889,7 +920,8 @@ void main() {
// the tier choice and the per-sub-draw feeds use those, not the guess above. // the tier choice and the per-sub-draw feeds use those, not the guess above.
const Bool feedDrawID = CurrentProgramReadsDrawID(); const Bool feedDrawID = CurrentProgramReadsDrawID();
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex(); const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer); const GLESMultiDrawMode tier =
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
Bool drawn = false; Bool drawn = false;
switch (tier) { switch (tier) {
@@ -921,8 +953,10 @@ void main() {
// Every tier above may decline a batch whose shape it cannot express. The two // Every tier above may decline a batch whose shape it cannot express. The two
// below are the floor: a base-vertex replay where the driver has one, and the // below are the floor: a base-vertex replay where the driver has one, and the
// rewritten index stream where it does not. Both are safe for any batch these // rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive. // entry points can receive - except that the base-vertex replay hands the
if (!drawn) { // application's own indices to the driver, which cannot restart on a desktop
// restart index, so that batch has only the rewriting floor.
if (!drawn && !arbitraryRestart) {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex); drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
} }
if (!drawn) { if (!drawn) {
+12 -7
View File
@@ -26,6 +26,7 @@
#include <cmath> #include <cmath>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <format>
#include <regex> #include <regex>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
@@ -836,7 +837,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices, String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
const String& inPerVertexMembers, const String& inPerVertexMembers,
const String& outPerVertexMembers) { const String& outPerVertexMembers,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
@@ -866,12 +869,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize // was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
// from a tessellation stage is a separate capability on both targets. // from a tessellation stage is a separate capability on both targets.
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n"; source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n"; for (Uint i = 0; i < 4; ++i) {
source += " gl_TessLevelOuter[1] = 1.0;\n"; source += " gl_TessLevelOuter[" + std::to_string(i) +
source += " gl_TessLevelOuter[2] = 1.0;\n"; "] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
source += " gl_TessLevelOuter[3] = 1.0;\n"; }
source += " gl_TessLevelInner[0] = 1.0;\n"; for (Uint i = 0; i < 2; ++i) {
source += " gl_TessLevelInner[1] = 1.0;\n"; source += " gl_TessLevelInner[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
}
source += "}\n"; source += "}\n";
return source; return source;
} }
+8 -6
View File
@@ -368,11 +368,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// //
// All four outer levels and both inner levels are written unconditionally: writing a // All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves // level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain. They are literal 1.0 because that is the GL // this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
// default and glPatchParameterfv - their only setter - is a stub in this frontend // GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making // glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
// the levels a parameter here AND part of what makes a built program stale, exactly as // That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
// PATCH_VERTICES already is; the two must move together, so they are named together. // is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
// //
// The same stage, for the same reason, that DirectVulkan synthesizes in // The same stage, for the same reason, that DirectVulkan synthesizes in
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both // ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
@@ -382,7 +382,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// VkShaderModule against a driver shader object. // VkShaderModule against a driver shader object.
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices, String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
const String& inPerVertexMembers, const String& inPerVertexMembers,
const String& outPerVertexMembers); const String& outPerVertexMembers,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel);
// Prefix of the writeonly half a read+write image uniform is split into (see // Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name. // SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_"; constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
@@ -1006,6 +1006,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// it the limit describes a capacity no shader may use, so report none. // it the limit describes a capacity no shader may use, so report none.
m_dynamicParameters.MaxClipDistances = m_dynamicParameters.MaxClipDistances =
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0; m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
// The cull pair, gated on its own feature. shaderCullDistance is separate from
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
// gate rather than riding on the clip one.
m_dynamicParameters.MaxCullDistances =
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
// with only one of the two features must not report a combined capacity that implies the
// other, so the gate is "either feature" and the value never drops below what is enabled.
m_dynamicParameters.MaxCombinedClipAndCullDistances =
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
m_dynamicParameters.MaxCullDistances})
: 0;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports; m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
// Assigned explicitly rather than left to the struct's defaults, like every other // Assigned explicitly rather than left to the struct's defaults, like every other
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is // parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
@@ -201,11 +201,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY( XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
sizeof(payload.passthroughTessControlKey)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
@@ -435,6 +439,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples; ms.rasterizationSamples = payload.rasterizationSamples;
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -37,11 +37,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1; Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
// identity and both are hashed. The renderer leaves the enable false unless the
// device's sampleRateShading feature was enabled
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f;
Uint32 subpass = 0; Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false; Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology. // GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3; Uint32 patchControlPoints = 3;
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
// the levels glPatchParameterfv set are compiled INTO that module and are not a
// function of the program or of patchControlPoints - see the note on
// passthroughTessControlStage.
Uint64 passthroughTessControlKey = 0;
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for // How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
// every program that never assigns gl_ViewportIndex, which is all of them outside the // every program that never assigns gl_ViewportIndex, which is all of them outside the
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state // conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
@@ -87,8 +100,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// renderer could not build one, and CreatePipeline refuses the pipeline - the same // renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated. // refusal it applies when `stages` itself is half-tessellated.
// //
// NOT hashed: it is a pure function of the program and of patchControlPoints, both // NOT hashed directly: it is a pure function of the program, of patchControlPoints and
// of which ComputeHash already mixes in. // of the default tessellation levels - the first two of which ComputeHash already
// mixes in, and the third of which arrives through passthroughTessControlKey above.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{}; VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr; const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path. // Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
@@ -13,7 +13,10 @@
#include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h" #include "MG_Util/ShaderTranspiler/Types.h"
#include <algorithm> #include <algorithm>
#include <bit>
#include <cmath>
#include <cstring> #include <cstring>
#include <format>
#include <map> #include <map>
#include <utility> #include <utility>
#include <spirv-tools/libspirv.h> #include <spirv-tools/libspirv.h>
@@ -3594,18 +3597,136 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) { Uint64 ProgramFactory::ComputePassthroughTessControlKey(Uint32 patchVertices,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers) {
// A plain 32-byte blob of exactly what the generator reads, hashed once. Deliberately over
// the RAW BITS rather than the values: two levels that compare unequal must key apart, and
// a NaN level - which glPatchParameterfv accepts - compares unequal to itself.
struct Blob {
Uint32 patchVertices;
Uint32 outerBits[4];
Uint32 innerBits[2];
Uint32 perVertexMembers;
} blob{};
blob.patchVertices = patchVertices;
for (Uint32 i = 0; i < 4; ++i) blob.outerBits[i] = std::bit_cast<Uint32>(defaultOuterLevel[i]);
for (Uint32 i = 0; i < 2; ++i) blob.innerBits[i] = std::bit_cast<Uint32>(defaultInnerLevel[i]);
blob.perVertexMembers = perVertexMembers;
return XXH64(&blob, sizeof(blob), 0);
}
// The member list a gl_PerVertex redeclaration must spell, derived from the mask. Order is
// glslang's declaration order and is load-bearing: a redeclaration whose members are the same
// set in a different order is a different block.
static String BuildPerVertexMemberDeclarations(Uint32 perVertexMembers) {
using Bit = ProgramFactory::PerVertexMemberBit;
String members;
if (perVertexMembers & static_cast<Uint32>(Bit::Position)) members += " vec4 gl_Position;\n";
if (perVertexMembers & static_cast<Uint32>(Bit::PointSize)) members += " float gl_PointSize;\n";
// Sized at one, not left unsized: an unsized built-in array in a redeclared block is
// implicitly sized by use, and this stage never indexes either distance array.
if (perVertexMembers & static_cast<Uint32>(Bit::ClipDistance)) members += " float gl_ClipDistance[1];\n";
if (perVertexMembers & static_cast<Uint32>(Bit::CullDistance)) members += " float gl_CullDistance[1];\n";
return members;
}
Uint32 ProgramFactory::ReflectPerVertexInputMembers(const Vector<Uint>& spirv) {
// Minimal, self-contained SPIR-V walk. SPIRV-Reflect is deliberately NOT used: for an
// array of interface blocks it reports built_in == -1 on the block and leaves every
// member's built_in at 0 (which is SpvBuiltInPosition), so a member walk through it reads
// "Position, Position, Position" - the same trap ReflectPassthroughTessControlNeed
// documents. The decorations below are unambiguous.
constexpr SizeT kHeaderWords = 5;
constexpr Uint32 kOpName = 5;
constexpr Uint32 kOpDecorate = 71;
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kOpTypeArray = 28;
constexpr Uint32 kOpTypePointer = 32;
constexpr Uint32 kOpVariable = 59;
constexpr Uint32 kDecorationBlock = 2;
constexpr Uint32 kDecorationBuiltIn = 11;
constexpr Uint32 kStorageClassInput = 1;
constexpr Uint32 kBuiltInPosition = 0;
constexpr Uint32 kBuiltInPointSize = 1;
constexpr Uint32 kBuiltInClipDistance = 3;
constexpr Uint32 kBuiltInCullDistance = 4;
(void)kOpName;
if (spirv.size() <= kHeaderWords) return 0;
UnorderedMap<Uint32, Uint32> arrayElementType; // array id -> element type id
UnorderedMap<Uint32, Pair<Uint32, Uint32>> pointerPointee; // pointer id -> (storage class, pointee)
UnorderedMap<Uint32, Uint32> structMembers; // struct id -> PerVertexMemberBit mask
std::set<Uint32> blockStructs;
Vector<Uint32> inputVariablePointerTypes;
for (SizeT i = kHeaderWords; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
const Uint32* words = &spirv[i];
switch (opcode) {
case kOpTypeArray:
if (wordCount >= 4) arrayElementType[words[1]] = words[2];
break;
case kOpTypePointer:
if (wordCount >= 4) pointerPointee[words[1]] = {words[2], words[3]};
break;
case kOpVariable:
if (wordCount >= 4 && words[3] == kStorageClassInput) inputVariablePointerTypes.push_back(words[1]);
break;
case kOpDecorate:
if (wordCount >= 3 && words[2] == kDecorationBlock) blockStructs.insert(words[1]);
break;
case kOpMemberDecorate:
if (wordCount >= 5 && words[3] == kDecorationBuiltIn) {
Uint32 bit = 0;
switch (words[4]) {
case kBuiltInPosition: bit = static_cast<Uint32>(PerVertexMemberBit::Position); break;
case kBuiltInPointSize: bit = static_cast<Uint32>(PerVertexMemberBit::PointSize); break;
case kBuiltInClipDistance: bit = static_cast<Uint32>(PerVertexMemberBit::ClipDistance); break;
case kBuiltInCullDistance: bit = static_cast<Uint32>(PerVertexMemberBit::CullDistance); break;
default: break;
}
structMembers[words[1]] |= bit;
}
break;
default:
break;
}
i += wordCount;
}
// The one Input variable whose type is an array of a Block-decorated struct IS gl_in;
// gl_TessCoord and friends are plain scalars/vectors and never match.
for (const Uint32 pointerType : inputVariablePointerTypes) {
const auto pointer = pointerPointee.find(pointerType);
if (pointer == pointerPointee.end()) continue;
const auto array = arrayElementType.find(pointer->second.second);
if (array == arrayElementType.end()) continue;
if (!blockStructs.contains(array->second)) continue;
const auto members = structMembers.find(array->second);
if (members == structMembers.end()) continue;
return members->second;
}
return 0;
}
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers) {
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no // The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
// control shader: "the input patch is passed through unmodified", the output patch has // control shader: "the input patch is passed through unmodified", the output patch has
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the // as many vertices as the input one (PATCH_VERTICES), and the levels come from the
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. // PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
// //
// Those two levels default to 1.0 and are baked here as literals because // Those two levels are baked in as literals - Vulkan has no equivalent dynamic state, so
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a // compiling them in is the only way to honour glPatchParameterfv. That makes them part of
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means // this module's identity: GetOrCreatePassthroughTessControlStage keys its cache on them,
// making the levels a parameter of this source AND of the cache key in // and PipelineFactory hashes them into the pipeline key. The three must move together.
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
// together here.
// //
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that // gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds // reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
@@ -3619,60 +3740,91 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// this from having to know the domain. // this from having to know the domain.
String source = "#version 450 core\n"; String source = "#version 450 core\n";
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n"; source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs // gl_in and gl_out are redeclared to the exact gl_PerVertex the NEIGHBOURING EVALUATION
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan // STAGE carries, because Vulkan matches built-in interface blocks by their whole shape,
// matches built-in interface blocks by their whole shape, and the two obvious spellings // and the two obvious spellings are both wrong:
// are both wrong:
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of // * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
// zeroes (degenerate triangles, nothing rasterized), and // zeroes (degenerate triangles, nothing rasterized), and
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members - // * taking glslang's DEFAULT block for a standalone control stage yields whatever THIS
// it appends gl_CullDistance - where a linked vertex+evaluation program has three. // source's #version implies, which is unrelated to the evaluation stage's.
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a //
// vertex+evaluation program through this same compiler and fails if the two shapes ever // The member set is a PARAMETER rather than a constant, and that is the whole point: it
// stop agreeing, rather than letting the mismatch show up as a black frame. // was hardcoded to {gl_Position, gl_PointSize, gl_ClipDistance[1]}, which is the shape a
// program carries only below #version 450. glslang appends gl_CullDistance to the block
// from 450 upward, so every 450/460 program - and every ESSL program, which the source
// processor rewrites to "#version 460 core" - carried FOUR members against this stage's
// three and got the black-frame-no-error case described above. The mask comes from
// ReflectPerVertexInputMembers, read off the evaluation stage's own SPIR-V.
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
// links the program at both 430 and 460.
// //
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately: // Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize // writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
// feature, which this renderer does not enable, so a program whose evaluation stage reads // feature, which this renderer does not enable, so a program whose evaluation stage reads
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap // gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
// this trades for not making every tessellated pipeline depend on an optional feature. // this trades for not making every tessellated pipeline depend on an optional feature.
source += "in gl_PerVertex {\n" const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
" vec4 gl_Position;\n" source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
" float gl_PointSize;\n" source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
" float gl_ClipDistance[1];\n"
"} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\n"
" float gl_ClipDistance[1];\n"
"} gl_out[];\n";
source += "void main() {\n"; source += "void main() {\n";
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n"; source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n"; for (Uint32 i = 0; i < 4; ++i) {
source += " gl_TessLevelOuter[1] = 1.0;\n"; source += " gl_TessLevelOuter[" + std::to_string(i) +
source += " gl_TessLevelOuter[2] = 1.0;\n"; "] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
source += " gl_TessLevelOuter[3] = 1.0;\n"; }
source += " gl_TessLevelInner[0] = 1.0;\n"; for (Uint32 i = 0; i < 2; ++i) {
source += " gl_TessLevelInner[1] = 1.0;\n"; source += " gl_TessLevelInner[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
}
source += "}\n"; source += "}\n";
return source; return source;
} }
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) { VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(
Uint32 patchVertices, const FloatVec4& defaultOuterLevel, const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers) {
// Everything compiled into the stage, folded into one key. The patch size alone stopped
// being enough once glPatchParameterfv could change the levels: two modules that differ
// only in a baked-in level are different modules, and pipelines built from either may be
// alive at the same time. The gl_PerVertex member set joins it for the same reason - two
// programs at different GLSL versions need differently-shaped blocks.
const Uint64 key =
ComputePassthroughTessControlKey(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a // A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
// generator that cannot compile from re-running glslang on every draw. // generator that cannot compile from re-running glslang on every draw.
const auto cached = m_passthroughTessControlStages.find(patchVertices); const auto cached = m_passthroughTessControlStages.find(key);
if (cached != m_passthroughTessControlStages.end()) { if (cached != m_passthroughTessControlStages.end()) {
return cached->second; return cached->second;
} }
// The key stopped being bounded when the levels joined it: patchVertices alone could only
// take 32 values, but six unclamped application floats can take any number, and an
// application that ramps a level per frame would retain one VkShaderModule per frame for
// the lifetime of the device. Flushed wholesale rather than aged: a module is not
// referenced by the pipelines built from it (Vulkan copies what it needs at
// vkCreateGraphicsPipelines), everything here runs on the GL thread, and an application
// that can overflow this cap is already recompiling every frame - so the flush costs it
// nothing it was not paying anyway.
if (m_passthroughTessControlStages.size() >= kMaxPassthroughTessControlStages) {
MGLOG_D("ProgramFactory: flushing %zu pass-through tessellation control stages; the application has "
"used more than %zu distinct (patch size, default level) combinations",
m_passthroughTessControlStages.size(), kMaxPassthroughTessControlStages);
for (auto& entry : m_passthroughTessControlStages) {
if (entry.second.module != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
}
}
m_passthroughTessControlStages.clear();
}
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
stage.module = VK_NULL_HANDLE; stage.module = VK_NULL_HANDLE;
stage.pName = "main"; stage.pName = "main";
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
const String source = BuildPassthroughTessControlSource(patchVertices); const String source =
BuildPassthroughTessControlSource(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
// Same compile configuration as every other stage of every other program: this runs on // Same compile configuration as every other stage of every other program: this runs on
// the GL thread (the draw path), so the live compile env is the right one, and flags=0 // the GL thread (the draw path), so the live compile env is the right one, and flags=0
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds). // is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
@@ -3686,7 +3838,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for " MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s", "patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
patchVertices, compiled.error().log.c_str()); patchVertices, compiled.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage); m_passthroughTessControlStages.emplace(key, stage);
return stage; return stage;
} }
@@ -3696,7 +3848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!linked) { if (!linked) {
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for " MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str()); "patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage); m_passthroughTessControlStages.emplace(key, stage);
return stage; return stage;
} }
@@ -3705,7 +3857,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!binary || binary.value().empty() || binary.value().front().empty()) { if (!binary || binary.value().empty() || binary.value().front().empty()) {
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage " MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
"for patchVertices=%u", patchVertices); "for patchVertices=%u", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage); m_passthroughTessControlStages.emplace(key, stage);
return stage; return stage;
} }
@@ -3727,14 +3879,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control " MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices); "stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage); m_passthroughTessControlStages.emplace(key, stage);
return stage; return stage;
} }
stage.module = module; stage.module = module;
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u " MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices); "(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage); m_passthroughTessControlStages.emplace(key, stage);
return stage; return stage;
} }
@@ -3744,6 +3896,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const { VkProgramObject& entry) const {
entry.needsPassthroughTessControl = false; entry.needsPassthroughTessControl = false;
entry.passthroughTessControlEmulatable = false; entry.passthroughTessControlEmulatable = false;
entry.passthroughPerVertexMembers = 0;
Bool hasTessEval = false; Bool hasTessEval = false;
Bool hasTessControl = false; Bool hasTessControl = false;
@@ -3763,6 +3916,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return; if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
const auto& module = spirv[tessEvalModuleIndex]; const auto& module = spirv[tessEvalModuleIndex];
// The shape the synthesized control stage has to redeclare. Read here because this is the
// only place that holds the evaluation stage's module; a zero mask means the walk found
// no input per-vertex block at all, in which case the pre-450 shape is the safe stand-in
// (it is what every program carried before gl_CullDistance joined the block).
const Uint32 perVertexMembers = ReflectPerVertexInputMembers(module);
entry.passthroughPerVertexMembers = perVertexMembers != 0 ? perVertexMembers : kDefaultPerVertexMembers;
if (perVertexMembers == 0) {
MGLOG_W("ProgramFactory: could not read the evaluation stage's gl_PerVertex block shape; the "
"pass-through control stage falls back to the pre-450 three-member form");
}
SpvReflectShaderModule reflectModule{}; SpvReflectShaderModule reflectModule{};
const SpvReflectResult createResult = const SpvReflectResult createResult =
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using CompileOptionFlags = Flags<CompileOptionBit>; using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64; using HashType = Uint64;
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
// in the order glslang declares them - which is the order a redeclaration must use.
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
enum class PerVertexMemberBit : Uint32 {
Position = 1u << 0,
PointSize = 1u << 1,
ClipDistance = 1u << 2,
CullDistance = 1u << 3,
};
// What a program parsed below #version 450 carries, and the fallback when a module's
// block cannot be read.
static constexpr Uint32 kDefaultPerVertexMembers =
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
struct UpdateAfterBindLimits { struct UpdateAfterBindLimits {
Bool enabled = false; Bool enabled = false;
Uint32 maxPerStageSamplers = 0; Uint32 maxPerStageSamplers = 0;
@@ -194,6 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is // instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed. // skipped). See ReflectPassthroughTessControlNeed.
Bool passthroughTessControlEmulatable = false; Bool passthroughTessControlEmulatable = false;
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
// which the source processor rewrites to "#version 460 core" - carries four members
// where a 430 program carries three. A fixed three-member pass-through fed the
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
// this whole family is written around.
Uint32 passthroughPerVertexMembers = 0;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives // Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised // cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse). // entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -249,6 +275,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin; writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl; needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable; passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame; lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -267,6 +294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.writesViewportIndexBuiltin = false; other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false; other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false; other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0; other.lastUsedFrame = 0;
} }
VkProgramObject& operator=(VkProgramObject&& other) noexcept { VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -311,6 +339,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin; writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl; needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable; passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame; lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -329,6 +358,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.writesViewportIndexBuiltin = false; other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false; other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false; other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0; other.lastUsedFrame = 0;
return *this; return *this;
} }
@@ -485,18 +515,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the caller then has no control stage to inject, and CreatePipeline refuses the // the caller then has no control stage to inject, and CreatePipeline refuses the
// pipeline rather than handing the driver a half-tessellated one. // pipeline rather than handing the driver a half-tessellated one.
// //
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES, // Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
// which is draw state, not link state - the CTS case that motivated this links at the // member set, because all three decide what the generator emits. The size comes from
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints, // PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
// so the module a pipeline was built with is part of that pipeline's identity. // - draw state rather than link state, and the CTS case that motivated this links at the
// Compiling is bounded by the number of distinct patch sizes a program draws with // default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever // own SPIR-V, so two programs at different GLSL versions need different modules. The
// happens for the rare program that has no control stage at all. // pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices); // part of that pipeline's identity. Compiling is bounded by the number of distinct
// (size, levels, members) combinations a program draws with - one or two in practice -
// and only ever happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers);
// Source of the module above. Exposed for tests: the generated GLSL is the whole // Source of the module above. Exposed for tests: the generated GLSL is the whole
// contract with the evaluation stage, so it is worth pinning independently of a device. // contract with the evaluation stage, so it is worth pinning independently of a device.
static String BuildPassthroughTessControlSource(Uint32 patchVertices); static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The identity of one such module: everything the generator bakes in, folded into a
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
// same value into the pipeline hash so a pipeline can never be handed a module built for
// different levels or a different block shape.
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
// Input variable that is an array of a Block-decorated struct). Zero when the module has
// no such block. Exposed for tests, which is the only way to pin the shape agreement
// without a device.
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
private: private:
struct ProgramLookupCache { struct ProgramLookupCache {
@@ -556,11 +608,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match. // See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1; Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr; IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by input patch size. Never evicted: at most // Pass-through tessellation control stages by the identity of what was compiled into
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline // them - the input patch size and the six default tessellation levels, folded into one
// ever built from one keeps referencing its module. A failed build is cached as // 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
// cannot simply be keyed on the patch size any more). A failed build is cached as
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw. // VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages; //
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
// nothing, so an application that recomputes a level per frame mints a new key per frame.
// Reaching the cap destroys every module and starts over (see the flush in
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
// and adds only a handful of values, so it does not move the cap in practice.
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,10 @@
#include "VkClearManager.h" #include "VkClearManager.h"
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
// clear key's layer span has to be the same one the render pass builds its attachment view from.
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ResolveAttachmentBaseArrayLayer(uploadTarget); return ResolveAttachmentBaseArrayLayer(uploadTarget);
} }
static Uint32 ResolveAttachmentLayerCount( // ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
const MG_State::GLState::FramebufferAttachmentObject& attachment) { // raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
if (attachment.IsLayered()) { // not a cosmetic difference: the count below is not key-only, it is written straight into
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1)); // VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
} // the entry - so a layered cube map's glClear reached one face and the other five were lost
return 1u; // for good, while the very same queued clear cleared all six through the render pass's
} // LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment( static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) { const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
@@ -86,25 +86,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ToStorageArrayLayer(texture, face); return ToStorageArrayLayer(texture, face);
} }
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the // ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this // VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a // VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
// geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's
// upper layers were never written at all.
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
return 1u;
}
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
// type is not a legal answer for several of the targets GL can attach, and returning it
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
// or dereferenced - by the driver inside vkCreateFramebuffer.
//
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
//
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
// the only honest answer would be to decline the attachment - and every driver this has run on,
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
static VkImageViewType ResolveAttachmentViewType( static VkImageViewType ResolveAttachmentViewType(
const MG_State::GLState::FramebufferAttachmentObject& attachment, const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) { const VkTextureManager::TextureResource& resource) {
if (attachment.IsLayered()) { if (attachment.IsLayered()) {
return resource.viewType; switch (resource.viewType) {
case VK_IMAGE_VIEW_TYPE_3D:
case VK_IMAGE_VIEW_TYPE_CUBE:
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
default:
return resource.viewType;
}
} }
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever // A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
// the image's own view type is. The cube-face upload targets always meant this; a cube map // the image's own view type is. The cube-face upload targets always meant this; a cube map
@@ -112,8 +126,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path // a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
// produces a non-layered cube attachment without a face upload target - and is kept for // produces a non-layered cube attachment without a face upload target - and is kept for
// symmetry with CUBE_ARRAY. // symmetry with CUBE_ARRAY.
//
// 3D belongs in the same list and was missing from it, which is why the "per-slice
// attachment view is a 2D view whose array layer is the slice" branch in
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) || if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) { resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
return VK_IMAGE_VIEW_TYPE_2D; return VK_IMAGE_VIEW_TYPE_2D;
} }
return resource.viewType; return resource.viewType;
@@ -334,47 +355,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const auto internalFormat = renderbuffer->GetInternalFormat(); const auto internalFormat = renderbuffer->GetInternalFormat();
// Three-channel color formats widen to their RGBA twin exactly like textures do // ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a // renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
// renderbuffer and a texture of the same GL format then see one VkFormat. // `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
const VkFormat format = [&]() -> VkFormat { // UPLOAD, and a renderbuffer has none, so only `.format` is taken.
switch (internalFormat) { //
case TextureInternalFormat::RGB: // This used to be a hand-maintained second copy of that table, and it was missing exactly
case TextureInternalFormat::RGB8: // four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
case TextureInternalFormat::R3G3B2: // VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
case TextureInternalFormat::RGB4: // VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
case TextureInternalFormat::RGB5: // through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
return VK_FORMAT_R8G8B8A8_UNORM; // a size-incompatible vkCmdCopyImage.
case TextureInternalFormat::SRGB8: const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format); const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the // Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer), // usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
@@ -772,7 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex, Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil) { Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the // Resolve the default-FBO depth flavor (see the header comment): keep the
@@ -858,7 +850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto activeIt = m_renderPasses.find(activeRenderPass->hash); auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) { if (activeIt != m_renderPasses.end()) {
activeIt->second.lastUsedFrame = m_frameCounter; activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return &activeIt->second;
} }
} }
@@ -882,13 +874,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastRenderPassHash = activeRenderPass->hash; m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment; m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter; activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return &activeIt->second;
} }
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil); auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash); auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) { if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter; it->second.lastUsedFrame = m_frameCounter;
return it->second; return &it->second;
} }
Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -1011,8 +1003,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
textureResources.emplace_back(nullptr); textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view); : rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i); MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
"%u on FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
colorAttachmentRefs[i].attachment = rbAttachmentIndex; colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue; continue;
@@ -1100,8 +1096,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]); attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
} else { } else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture); auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
MOBILEGL_ASSERT(textureResource, if (textureResource == nullptr) {
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i); // SyncTextureResource legitimately declines - an unsupported format,
// sample count or image-flag combination, or a vkCreateImage the driver
// refused. There is no image to attach, so there is no render pass.
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
"attachment %u on FBO %u; declining the render pass",
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
textureResources.emplace_back(textureResource); textureResources.emplace_back(textureResource);
desc.format = ResolveSrgbAttachmentWriteFormat( desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format, textureResource->format,
@@ -1122,8 +1125,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back( attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel( m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType)); *texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i); MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
"render pass",
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
fbo.GetExternalIndex());
return nullptr;
}
} }
desc.samples = attachmentSampleCount; desc.samples = attachmentSampleCount;
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex()); adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
@@ -1216,8 +1226,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) { } else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture(); auto& texture = *selectedDepthStencilAttachment->GetTexture();
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture); depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(depthTextureResource, if (depthTextureResource == nullptr) {
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthTextureResource->layout; trackedDepthLayout = depthTextureResource->layout;
depthAttachmentDescription.format = depthTextureResource->format; depthAttachmentDescription.format = depthTextureResource->format;
depthAttachmentSampleCount = depthTextureResource->sampleCount; depthAttachmentSampleCount = depthTextureResource->sampleCount;
@@ -1229,8 +1243,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else { } else {
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer(); const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer); depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
MOBILEGL_ASSERT(depthRenderbufferResource, if (depthRenderbufferResource == nullptr) {
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthRenderbufferResource->layout; trackedDepthLayout = depthRenderbufferResource->layout;
depthAttachmentDescription.format = depthRenderbufferResource->format; depthAttachmentDescription.format = depthRenderbufferResource->format;
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount; depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
@@ -1304,8 +1322,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back( attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel( m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType)); texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) { if (width == 0 || height == 0) {
width = attachmentExtent.x(); width = attachmentExtent.x();
height = attachmentExtent.y(); height = attachmentExtent.y();
@@ -1327,6 +1351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}); });
textureResources.emplace_back(nullptr); textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(depthRenderbufferResource->view); attachmentViews.emplace_back(depthRenderbufferResource->view);
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) { if (width == 0 || height == 0) {
width = attachmentExtent.x(); width = attachmentExtent.x();
height = attachmentExtent.y(); height = attachmentExtent.y();
@@ -1424,8 +1454,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.dependencyCount = 2; renderPassCreateInfo.dependencyCount = 2;
renderPassCreateInfo.pDependencies = subpassDependencies; renderPassCreateInfo.pDependencies = subpassDependencies;
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
// line on every draw for the life of the process - a decline caches nothing, so every
// later draw to the same framebuffer re-enters this path and fails again.
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass)); const VkResult renderPassResult =
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
"render pass",
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
fbo.GetExternalIndex());
return nullptr;
}
// Framebuffer // Framebuffer
VkFramebufferCreateInfo framebufferCreateInfo; VkFramebufferCreateInfo framebufferCreateInfo;
@@ -1438,8 +1485,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
framebufferCreateInfo.width = width; framebufferCreateInfo.width = width;
framebufferCreateInfo.height = height; framebufferCreateInfo.height = height;
framebufferCreateInfo.layers = framebufferLayers; framebufferCreateInfo.layers = framebufferLayers;
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
VkFramebuffer framebuffer = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer)); const VkResult framebufferResult =
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
// The render pass has no entry to own it yet, so it is destroyed here rather than
// leaked - RenderPassEntry's destructor is the only other thing that would.
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
"%u attachments, %u layers); declining the render pass",
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
fbo.GetExternalIndex(), width, height,
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
vkDestroyRenderPass(m_device, renderPass, nullptr);
return nullptr;
}
IntVec2 extent = {width, height}; IntVec2 extent = {width, height};
RenderPassEntry renderPassEntry { RenderPassEntry renderPassEntry {
hash, hash,
@@ -1464,7 +1524,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.y()); extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry)); auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter; insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second; return &insertedIt->second;
} }
void VkRenderPassManager::OnPresent() { void VkRenderPassManager::OnPresent() {
@@ -243,9 +243,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// draw against a depth-less active pass resolves to a new (incompatible) // draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split; // entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along). // the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, //
Uint32 swapchainImageIndex, // Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
Bool drawUsesDepthStencil = true); // all - a texture the texture manager declined to back (an unsupported format or sample
// count), or an attachment view it cannot construct (a layer span the image has no room
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
// unrepresentable: the function returned a reference, so the only thing the two fallible
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
//
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
// already drops a draw whose sampler descriptor could not be resolved
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
// themselves, so a caller needs no message of its own.
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo); const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload, void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder; sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
} }
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
// family and an integer family and requires the sampler's choice to match the image view's
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
enum class BorderColorDomain {
Float,
SignedInteger,
UnsignedInteger
};
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R32I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA32I:
return BorderColorDomain::SignedInteger;
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return BorderColorDomain::UnsignedInteger;
default:
return BorderColorDomain::Float;
}
}
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
// format in which texture components are stored. For signed and unsigned normalized
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
// floating-point and integer formats, border values are clamped to the representable range of
// the format." Every clause of that sentence is a real case here - the clamp is not just the
// normalized one.
//
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
// so a negative border on them must come back as 0 rather than as a negative number the
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
struct FloatBorderRange {
Bool clamped = true;
Float minValue = 0.0f;
Float maxValue = 1.0f;
};
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
switch (format) {
case TextureInternalFormat::R32F:
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGBA32F:
return {false, 0.0f, 0.0f};
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG16F:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGBA16F:
return {true, -65504.0f, 65504.0f};
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
// that matters for correctness is the lower one, and a single conservative upper bound
// costs nothing a real border colour will ever notice.
case TextureInternalFormat::R11FG11FB10F:
return {true, 0.0f, 64512.0f};
case TextureInternalFormat::RGB9E5:
return {true, 0.0f, 65408.0f};
default:
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
}
}
// Per-component representable range of an integer texture format, as Int64 so that the whole
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
// whose alpha is narrower than its colour channels.
struct IntegerBorderRange {
Int64 rgbMin = 0;
Int64 rgbMax = 0;
Int64 alphaMin = 0;
Int64 alphaMax = 0;
};
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGBA8I:
return uniform(-128, 127);
case TextureInternalFormat::R16I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGBA16I:
return uniform(-32768, 32767);
case TextureInternalFormat::R8UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGBA8UI:
return uniform(0, 255);
case TextureInternalFormat::R16UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGBA16UI:
return uniform(0, 65535);
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA32UI:
return uniform(0, 4294967295LL);
case TextureInternalFormat::RGB10A2UI:
return {0, 1023, 0, 3};
default:
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
// clamp that cannot alter a value the GL entry points could have carried.
return uniform(-2147483648LL, 2147483647LL);
}
}
Bool IsDepthTextureFormat(TextureInternalFormat format) { Bool IsDepthTextureFormat(TextureInternalFormat format) {
switch (format) { switch (format) {
case TextureInternalFormat::DepthComponent: case TextureInternalFormat::DepthComponent:
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_config = initInfo.config; m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported; m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f); m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
m_customBorderColorSupported = initInfo.customBorderColorSupported;
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
m_customBorderColorSamplerCount = 0;
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr, MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info"); "VkSamplerManager::Initialize failed: invalid initialization info");
return true; return true;
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE; m_device = VK_NULL_HANDLE;
m_config = nullptr; m_config = nullptr;
m_frameBoundaryCounter = 0; m_frameBoundaryCounter = 0;
m_customBorderColorSupported = false;
m_maxCustomBorderColorSamplers = 0;
m_customBorderColorSamplerCount = 0;
} }
void VkSamplerManager::OnFrameBoundary() { void VkSamplerManager::OnFrameBoundary() {
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) { if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr); vkDestroySampler(m_device, entry.handle, nullptr);
} }
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
it = m_samplers.erase(it); it = m_samplers.erase(it);
} else { } else {
++it; ++it;
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, Bool forceNearestFiltering, Bool singleLevelView,
Bool forceNearestFiltering, Bool singleLevelView) const { const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = sampler.GetSamplerCompareFunc(); const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture); // The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor))); // colour itself: two samplers that differ only in a custom border colour carry the same enum
// and would otherwise collide onto whichever one was created first.
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
if (borderColor.isCustom) {
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
}
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// allocation for a genuinely single-level image) and faults the GPU - the same failure // allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample. // the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1; const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView); // Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
auto it = m_samplers.find(key); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter; it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Must match BuildSamplerKey's resolution exactly. // Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView); samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod); samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture); samplerInfo.borderColor = borderColor.color;
samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.unnormalizedCoordinates = VK_FALSE;
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
// customBorderColorWithoutFormat was required alongside customBorderColors at device
// creation - a GL sampler object has no idea which texture it will be paired with.
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
if (borderColor.isCustom) {
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
customBorderColorInfo.customBorderColor = borderColor.customValue;
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
customBorderColorInfo.pNext = samplerInfo.pNext;
samplerInfo.pNext = &customBorderColorInfo;
}
VkSampler vkSampler = VK_NULL_HANDLE; VkSampler vkSampler = VK_NULL_HANDLE;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)"); VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.externalIndex = sampler.GetExternalIndex(); entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion(); entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter; entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.usesCustomBorderColor = borderColor.isCustom;
if (entry.usesCustomBorderColor) {
++m_customBorderColorSamplerCount;
}
m_samplers[key] = entry; m_samplers[key] = entry;
return vkSampler; return vkSampler;
} }
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::ITextureObject& texture) { const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
ResolvedBorderColor resolved{};
if (!UsesBorderColor(sampler)) { if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
} }
// Border colour is sampler state: a bound sampler object supplies its own, and a texture // Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns. // with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor(); const auto format = texture.GetFormat();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat()); const auto domain = ResolveBorderColorDomain(format);
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
if (isDepthTexture) { if (domain != BorderColorDomain::Float) {
// An integer image view REQUIRES an integer border colour, whatever the value is - even
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
// representation (a plain cast) is as good an answer as any.
//
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
//
// Which representation to start from is the TEXTURE's domain, not the entry-point form
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
// an integer internal data type and does not define a sign conversion between the two
// integer forms, so the stored bits are reinterpreted in the sampled format's own
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
// the representation by the FORM instead would answer 0 here, which is a defensible
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
// agreement decides it.
const auto range = ResolveIntegerBorderRange(format);
const auto& borderColorI = sampler.GetBorderColorI();
const auto& borderColorUI = sampler.GetBorderColorUI();
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
Int64 clamped[4];
for (SizeT channel = 0; channel < 4; ++channel) {
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
: static_cast<Int64>(borderColorI[channel]);
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
clamped[channel] = std::clamp(raw, low, high);
}
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
// the palette entry it clamps to rather than missing every one of them.
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
if (allZeroRgb && clamped[3] == 0) {
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
return resolved;
}
if (allZeroRgb && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
return resolved;
}
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
return resolved;
}
if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
resolved.isCustom = true;
for (SizeT channel = 0; channel < 4; ++channel) {
if (domain == BorderColorDomain::UnsignedInteger) {
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
} else {
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
}
}
return resolved;
}
// No custom colour available: pick the nearest of the three integer palette entries
// rather than always answering transparent black, which is what turned an integer border
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
const Bool opaque = clamped[3] != 0;
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
return resolved;
}
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
// format, so it is clamped to that format's representable range first. Without the clamp the
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
// and fell through to transparent black - every border texel sampled 0 where the test wanted
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
// 32-bit float formats are unclamped.
FloatVec4 borderColor = sampler.GetBorderColor();
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
std::clamp(borderColor.y(), range.minValue, range.maxValue),
std::clamp(borderColor.z(), range.minValue, range.maxValue),
std::clamp(borderColor.w(), range.minValue, range.maxValue));
}
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
if (IsDepthTextureFormat(format)) {
if (NearlyEqual(borderColor.x(), 1.0f)) { if (NearlyEqual(borderColor.x(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
} }
if (NearlyEqual(borderColor.x(), 0.0f)) { if (NearlyEqual(borderColor.x(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
} }
} }
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) && const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
NearlyEqual(borderColor.z(), 0.0f); NearlyEqual(borderColor.z(), 0.0f);
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) { if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
return resolved;
} }
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) { if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
} }
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) && if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) { NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
} }
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
resolved.isCustom = true;
resolved.customValue.float32[0] = borderColor.x();
resolved.customValue.float32[1] = borderColor.y();
resolved.customValue.float32[2] = borderColor.z();
resolved.customValue.float32[3] = borderColor.w();
return resolved;
}
// Nearest of the three float palette entries. Transparent black stays the answer for a
// transparent border, which is what the old unconditional fallback got right by accident.
const Bool opaque = borderColor.w() >= 0.5f;
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
return resolved;
} }
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -28,6 +28,13 @@ public:
Bool samplerAnisotropySupported = false; Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. // VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f; Float maxSamplerAnisotropy = 1.0f;
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
Bool customBorderColorSupported = false;
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
// and falls back to the snapped predefined value once it is reached.
Uint32 maxCustomBorderColorSamplers = 0;
}; };
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
@@ -52,6 +59,21 @@ public:
// boundaries. // boundaries.
void OnFrameBoundary(); void OnFrameBoundary();
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
//
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
// custom colour was affordable is decided before the key is built, not twice with a budget
// change in between.
struct ResolvedBorderColor {
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
VkClearColorValue customValue{};
Bool isCustom = false;
};
private: private:
struct SamplerCacheEntry { struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE; VkSampler handle = VK_NULL_HANDLE;
@@ -60,17 +82,18 @@ private:
// Frame boundary of the last cache hit; entries idle past the // Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed. // OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0; Uint64 lastUsedFrameBoundary = 0;
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
Bool usesCustomBorderColor = false;
}; };
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
const MG_State::GLState::ITextureObject& texture, Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func); static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture); const MG_State::GLState::ITextureObject& texture) const;
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and // The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
@@ -82,6 +105,12 @@ private:
const VulkanRendererConfig* m_config = nullptr; const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false; Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f; Float m_maxSamplerAnisotropy = 1.0f;
Bool m_customBorderColorSupported = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
// and Shutdown.
Uint32 m_customBorderColorSamplerCount = 0;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers; UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0; Uint64 m_frameBoundaryCounter = 0;
@@ -46,13 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return mipLevelCount; return mipLevelCount;
} }
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
struct TextureShapeInfo { struct TextureShapeInfo {
VkImageType imageType = VK_IMAGE_TYPE_2D; VkImageType imageType = VK_IMAGE_TYPE_2D;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
@@ -380,7 +373,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) { TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
switch (format) { switch (format) {
case TextureInternalFormat::RGB: case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8: case TextureInternalFormat::RGB8:
@@ -921,18 +914,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (mipLevel >= resource->mipLevels) { if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice // A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
// attachment view is a 2D view whose "array layer" is the slice - legal only on a // view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which // non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
// SyncTextureResource asks for and may have had refused per format. // which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) { // means). BOTH spellings are legal only on a 2D-array-compatible image
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
// may have had refused per format.
//
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u); const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 || if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) { layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, " // Not an error line: the render-pass builder turns the null view into one
"2D-array-compatible=%d)", // MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
"slices, 2D-array-compatible=%d)",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(), __func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
mipLevel, sliceCount, static_cast<Int>(viewType), mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0)); (int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
@@ -2173,12 +2176,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture && if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) { (imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this // Losing 2D-array compatibility only costs framebuffer attachment of this format's
// format; failing creation would lose the texture entirely. Remembered so later syncs // 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
// neither reprobe nor flag-mismatch against this image and recreate it. // the z axis; failing creation would lose the texture entirely. Recorded here (the
// per-format set below) so later syncs neither reprobe nor flag-mismatch against this
// image and recreate it, and so GetOrCreateAttachmentViewAtMipLevel declines rather
// than handing back a view that cannot exist - the render-pass builder then turns
// that decline into a skipped draw instead of a null VkImageView in pAttachments.
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d " MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be " "textureId=%d; creating without it (per-slice and layered framebuffer "
"unavailable for it)", "attachment of 3D textures in this format will be unavailable)",
__func__, static_cast<Int>(format), texture.GetExternalIndex()); __func__, static_cast<Int>(format), texture.GetExternalIndex());
m_2dArrayCompatibleUnsupported.insert(format); m_2dArrayCompatibleUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
@@ -10,8 +10,10 @@
#include "../VkIncludes.h" #include "../VkIncludes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <algorithm>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -22,6 +24,31 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8; enum class SamplerNumericDomain : Uint8;
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
// because Vulkan devices rarely support the 3-channel layouts.
//
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
// `.format` and ignore the rest.
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what // A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level // glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in // as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
@@ -41,6 +68,37 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
return glTexelSize; return glTexelSize;
} }
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
//
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
// depending only on which path happens to consume it first - and the materialise path then POPS
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
// that split gets introduced; keep them the same function.
//
// Two shapes make this more than `size.z()`:
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
// just above), so z reads 1 and every layer above the first was silently dropped.
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsLayered()) {
return 1u;
}
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
if (target == TextureTarget::TextureCubeMap) {
return 6u;
}
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture // A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they // the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they // are relative to the VIEW, and have to be shifted into the storage image's numbering before they
@@ -19,6 +19,7 @@
#include "MG_State/GLState/SamplerState/SamplerObject.h" #include "MG_State/GLState/SamplerState/SamplerObject.h"
#include "MG_State/GLState/TextureState/TextureObject.h" #include "MG_State/GLState/TextureState/TextureObject.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h" #include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Impl/GLImpl/Texture/GL_Texture.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
// Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the // Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the
// missing include never broke a default build and did break every WARN/DEBUG-level one. // missing include never broke a default build and did break every WARN/DEBUG-level one.
@@ -30,6 +31,7 @@
#include "MG_Util/Texture/PixelStoreProcessor.h" #include "MG_Util/Texture/PixelStoreProcessor.h"
#include <Config.h> #include <Config.h>
#include <algorithm> #include <algorithm>
#include <bit>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <vulkan/utility/vk_format_utils.h> #include <vulkan/utility/vk_format_utils.h>
@@ -1417,6 +1419,48 @@ void main() {
return {width, height, depth}; return {width, height, depth};
} }
// How many components of a GL-space texel size actually halve down the mip chain. An array
// texture's LAYER count is not a dimension of the image (GL 4.6 core 8.14.3): it stays put
// all the way down, and GetMipmapTexelSize parks it in the slot after the image's own
// dimensions. This is the same split IsMipmapCompleteForFilter applies, and the two have to
// agree - allocating a chain whose layer count shrinks builds levels the completeness rule
// then rejects. Vulkan-space extents need none of this: layers live in arrayLayers there,
// so resource->depth is already 1 for every array target.
static Int MipShrinkingComponentCount(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1DArray:
return 1;
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
return 2;
default:
return 3;
}
}
static IntVec3 ComputeMipTexelSizeWithFixedComponents(const IntVec3& baseTexelSize, Uint32 relativeMipLevel,
Int shrinkingComponents) {
IntVec3 size = baseTexelSize;
for (Int component = 0; component < shrinkingComponents && component < 3; ++component) {
size[component] = std::max<Int>(size[component] >> static_cast<Int>(relativeMipLevel), 1);
}
return size;
}
static Uint32 ComputeFullMipLevelCountWithFixedComponents(const IntVec3& baseTexelSize,
Int shrinkingComponents) {
Int maxDimension = 1;
for (Int component = 0; component < shrinkingComponents && component < 3; ++component) {
maxDimension = std::max<Int>(maxDimension, baseTexelSize[component]);
}
Uint32 mipLevelCount = 1;
while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1);
++mipLevelCount;
}
return mipLevelCount;
}
static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture, static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture,
Uint32 baseMipLevel) { Uint32 baseMipLevel) {
const Uint32 existingMipLevelCount = static_cast<Uint32>(texture.GetMipmapLevelCount()); const Uint32 existingMipLevelCount = static_cast<Uint32>(texture.GetMipmapLevelCount());
@@ -1429,6 +1473,8 @@ void main() {
return false; return false;
} }
const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget());
for (const auto uploadTarget : uploadTargets) { for (const auto uploadTarget : uploadTargets) {
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel); const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel);
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel); const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel);
@@ -1445,13 +1491,15 @@ void main() {
} }
const SizeT bytesPerTexel = baseByteSize / baseTexelCount; const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize); const Uint32 requiredMipLevelCount =
baseMipLevel + ComputeFullMipLevelCountWithFixedComponents(baseTexelSize, shrinkingComponents);
if (existingMipLevelCount >= requiredMipLevelCount) { if (existingMipLevelCount >= requiredMipLevelCount) {
continue; continue;
} }
for (Uint32 level = existingMipLevelCount; level < requiredMipLevelCount; ++level) { for (Uint32 level = existingMipLevelCount; level < requiredMipLevelCount; ++level) {
const IntVec3 levelTexelSize = ComputeMipTexelSize(baseTexelSize, level - baseMipLevel); const IntVec3 levelTexelSize = ComputeMipTexelSizeWithFixedComponents(
baseTexelSize, level - baseMipLevel, shrinkingComponents);
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) * const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) * static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z()); static_cast<SizeT>(levelTexelSize.z());
@@ -3126,7 +3174,9 @@ void main() {
m_samplerManager = MakeUnique<VkSamplerManager>(); m_samplerManager = MakeUnique<VkSamplerManager>();
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed."); MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled, succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
m_physicalDevice.properties.limits.maxSamplerAnisotropy}); m_physicalDevice.properties.limits.maxSamplerAnisotropy,
m_customBorderColorFeatureEnabled,
m_maxCustomBorderColorSamplers});
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed."); MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
succeeded = InitializeBlitResources(); succeeded = InitializeBlitResources();
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed."); MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
@@ -3915,10 +3965,15 @@ void main() {
// Copies index data, replacing every occurrence of the application's arbitrary restart // Copies index data, replacing every occurrence of the application's arbitrary restart
// index with the fixed all-ones value of the index type - the only one Vulkan restarts // index with the fixed all-ones value of the index type - the only one Vulkan restarts
// on. An index that already equals the fixed value would then be indistinguishable from // on. An index that already equals the fixed value would then be indistinguishable from
// a restart, so it is nudged to the next-lowest value: it can only be a real index (the // a restart, so it is nudged to the next-lowest value, which silently draws the wrong
// application's restart index is a different number), and the vertex it selects is // vertex. That is a real (if narrow) loss and it is reported once rather than left
// outside any well-defined draw anyway, whereas leaving it alone would tear the // invisible; DirectGLES avoids it for 8- and 16-bit indices by widening the copy instead,
// primitive in two. // and the same treatment here is follow-up work.
//
// The caller guarantees applicationRestartIndex fits the index type, so no truncating
// cast is needed - and none may be used: truncating turns glPrimitiveRestartIndex(0x100)
// over 8-bit indices into "restart on index 0", which shreds every primitive that
// references vertex 0.
void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType, void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType,
Uint32 applicationRestartIndex, Vector<Uint8>& output) { Uint32 applicationRestartIndex, Vector<Uint8>& output) {
output.resize(sizeBytes); output.resize(sizeBytes);
@@ -3932,6 +3987,12 @@ void main() {
if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) { if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) {
indices[i] = fixedMax; indices[i] = fixedMax;
} else if (indices[i] == fixedMax) { } else if (indices[i] == fixedMax) {
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses "
"the all-ones index %u: both cannot be spelled at this index width, so every "
"all-ones index is drawn one vertex lower. Use "
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones value out of the "
"index data.",
applicationRestartIndex, static_cast<Uint32>(fixedMax));
indices[i] = fixedMax - 1; indices[i] = fixedMax - 1;
} }
} }
@@ -3984,14 +4045,13 @@ void main() {
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters(); const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) { if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) {
const Uint32 restartIndex = rsp.PrimitiveRestartIndex; const Uint32 restartIndex = rsp.PrimitiveRestartIndex;
Uint32 fixedMax = 0; const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
switch (vkIndexType) { // STRICTLY less, and never truncated. Equal needs no rewrite (the driver already
case VK_INDEX_TYPE_UINT8: fixedMax = 0xFFu; break; // restarts there); GREATER means the index type cannot hold the application's restart
case VK_INDEX_TYPE_UINT16: fixedMax = 0xFFFFu; break; // index, so GL 4.6 core 10.3.6 says nothing matches it and the draw restarts nowhere -
case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break; // which is exactly what ResolvePrimitiveRestartEnable told the pipeline, so rewriting
default: break; // here would put restarts into a stream the pipeline was built not to restart on.
} substituteRestart = restartIndex < fixedMax;
substituteRestart = restartIndex != fixedMax;
substituteRestartIndex = restartIndex; substituteRestartIndex = restartIndex;
} }
@@ -4707,8 +4767,9 @@ void main() {
// build in GetOrCreatePipeline - any new GL-state read there must be added here: // build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides // - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, // the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), plus the depth write mask // PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op // - patch vertices, polygon mode, cull face mode, depth func, logic op,
// min sample shading
// - front/back stencil ops + compare funcs (ref/mask are dynamic state) // - front/back stencil ops + compare funcs (ref/mask are dynamic state)
// - per draw buffer up to the render pass's colour span: indexed blend enable, // - per draw buffer up to the render pass's colour span: indexed blend enable,
// blend factors/equations, indexed colour write mask (broadcast from index 0 // blend factors/equations, indexed colour write mask (broadcast from index 0
@@ -4733,8 +4794,31 @@ void main() {
capabilityBits |= p.PrimitiveRestartEnabled ? 1ull << 6 : 0; capabilityBits |= p.PrimitiveRestartEnabled ? 1ull << 6 : 0;
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0; capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
capabilityBits |= p.DepthMask ? 1ull << 8 : 0; capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits); Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
// versions, so an unhashed float would let a pipeline built at one rate be handed back
// after glMinSampleShading moved it - the memo would see identical state.
{
Uint32 minSampleShadingBits = 0;
std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits));
}
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
// The default tessellation levels belong here for the same reason PatchVertices does:
// when a program has an evaluation stage and no control stage, both are compiled into the
// synthesized pass-through control stage, so two draws that differ only in a level need
// different pipelines. Hashed over the RAW BITS so a NaN level - which glPatchParameterfv
// accepts - keys to itself. Six extra words on a path that only recomputes when the
// pipeline-state version moved.
for (Uint32 i = 0; i < 4; ++i) {
hash = CombinePipelineStateWord(hash,
static_cast<Uint64>(std::bit_cast<Uint32>(p.PatchDefaultOuterLevel[i])));
}
for (Uint32 i = 0; i < 2; ++i) {
hash = CombinePipelineStateWord(hash,
static_cast<Uint64>(std::bit_cast<Uint32>(p.PatchDefaultInnerLevel[i])));
}
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PolygonModeFront)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PolygonModeFront));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.CullFaceModeSetting)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.CullFaceModeSetting));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.DepthFunc)); hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.DepthFunc));
@@ -4779,13 +4863,42 @@ void main() {
return program.HasLinkedShaderStage(ShaderStage::Geometry); return program.HasLinkedShaderStage(ShaderStage::Geometry);
} }
// GL primitive restart is defined on the INDEX STREAM (GL 4.6 core 10.3.6): it splits
// primitives when a fetched index matches PRIMITIVE_RESTART_INDEX. Two consequences the
// capability bits alone cannot express, both resolved here because only the caller knows them:
//
// - A non-indexed draw has no index stream, so restart is a no-op for it. Leaving the
// pipeline's primitiveRestartEnable on for a glDrawArrays is what made the list-topology
// guard below refuse those draws, so an application that enables GL_PRIMITIVE_RESTART once
// at init lost every glDrawArrays on a device without the extension.
// - The comparison is against the full 32-bit restart index with the fetched index
// zero-extended, so a restart index the type cannot hold (0x100FF against UNSIGNED_BYTE
// data) matches no index and that draw restarts NOWHERE. UploadAndBindIndexBuffer makes the
// same call for the rewrite, and the two must agree or the pipeline says "restart" over
// index data nothing rewrote.
Bool VulkanRenderer::ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
const IndexBufferView* pIndexBufferView) const {
if (!(aspects & DrawSetupAspect::IndexBuffer) || pIndexBufferView == nullptr) {
return false;
}
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
if (rsp.PrimitiveRestartFixedIndexEnabled) {
return true;
}
if (!rsp.PrimitiveRestartEnabled) {
return false;
}
return rsp.PrimitiveRestartIndex <= MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
}
VkPipeline VulkanRenderer::GetOrCreatePipeline( VkPipeline VulkanRenderer::GetOrCreatePipeline(
GLenum mode, GLenum mode,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags, ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry) { const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable) {
Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip;
if (programObj.stages.empty()) { if (programObj.stages.empty()) {
MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages");
@@ -4827,6 +4940,7 @@ void main() {
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash && entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
entry.renderPassHash == renderPassHash && entry.renderPassHash == renderPassHash &&
entry.pipelineStateHash == pipelineStateHash && entry.pipelineStateHash == pipelineStateHash &&
entry.primitiveRestartEnable == primitiveRestartEnable &&
entry.transformFlags == transformFlags) { entry.transformFlags == transformFlags) {
return entry.pipeline; return entry.pipeline;
} }
@@ -5025,22 +5139,50 @@ void main() {
: VK_POLYGON_MODE_FILL; : VK_POLYGON_MODE_FILL;
const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode); const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode);
const Bool primitiveRestartEnabled = // Resolved by the caller (ResolvePrimitiveRestartEnable), which knows whether the draw is
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || // indexed and with what index type; the capability bits alone answer neither.
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); Bool primitiveRestartEnabled = primitiveRestartEnable;
// GL applies restart to PATCHES only when PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED is true
// (GL 4.6 core 10.3.6). MobileGL supports no such thing - neither backend has a way to
// restart a patch stream - and GL_FALSE is a legal answer to that query, so a patch draw
// simply never restarts here. Doing this BEFORE the feature guard below is what keeps a
// perfectly ordinary GL_PATCHES draw from being refused on a device that lacks
// VK_EXT_primitive_topology_list_restart. (When GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED
// is eventually added to glGetIntegerv it has to report GL_FALSE to stay consistent with
// this.)
if (vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
primitiveRestartEnabled = false;
}
// Primitive restart on a *list* topology requires the primitiveTopologyListRestart feature; // Primitive restart on a *list* topology requires the primitiveTopologyListRestart feature;
// strip/fan restart works without it. Silently dropping restarts would corrupt geometry, so // strip/fan restart works without it. There is no fallback - silently dropping the restarts
// hard-fail here (at the draw) with the reason when the device lacks the feature. // would weld the primitives on either side of each one together - so the draw is declined
// here with the reason.
//
// Declined, not thrown. This used to THROW_EXCEPTION, which unwinds a C++ exception through
// the C GL ABI and takes the process down (the hazard GL_Texture.cpp and RenderState.cpp
// already name); an application that merely enabled a legal desktop feature died instead of
// getting a draw that rendered nothing. VK_NULL_HANDLE is this function's established
// "skip this draw" answer, used by the no-stages case above.
//
// Reached only when this draw's index stream really does restart. Testing the raw
// capability bits here instead - which is what it did - refused every NON-INDEXED
// list-topology draw as well, so an application that enables GL_PRIMITIVE_RESTART once at
// init and then calls glDrawArrays(GL_TRIANGLES, ...) rendered nothing at all.
const auto isListTopology = [](VkPrimitiveTopology t) { const auto isListTopology = [](VkPrimitiveTopology t) {
return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST ||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST ||
t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY ||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY || t == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
}; };
if (primitiveRestartEnabled && !m_primitiveTopologyListRestartFeatureEnabled && isListTopology(vkTopology)) { if (primitiveRestartEnabled && !m_primitiveTopologyListRestartFeatureEnabled && isListTopology(vkTopology)) {
THROW_EXCEPTION("Primitive restart on a list topology requires the primitiveTopologyListRestart device " MGLOG_E_ONCE("Draw skipped: primitive restart on a list topology (0x%x) requires the "
"feature (VK_EXT_primitive_topology_list_restart), which this device does not support; use " "primitiveTopologyListRestart device feature (VK_EXT_primitive_topology_list_restart), "
"a strip/fan topology or a device that supports it."); "which this device does not support; use a strip/fan topology, or disable primitive "
"restart for list-topology draws.",
mode);
return VK_NULL_HANDLE;
} }
PipelineFactory::PipelineCreatePayload payload { PipelineFactory::PipelineCreatePayload payload {
@@ -5050,6 +5192,13 @@ void main() {
.renderPass = renderPassEntry.renderPass, .renderPass = renderPassEntry.renderPass,
.colorAttachmentCount = renderPassEntry.colorAttachmentCount, .colorAttachmentCount = renderPassEntry.colorAttachmentCount,
.rasterizationSamples = renderPassEntry.sampleCount, .rasterizationSamples = renderPassEntry.sampleCount,
// ARB_sample_shading. Dropped on a device without sampleRateShading rather than
// hard-failing the draw: the rate is a hint, and the pipeline renders correctly at the
// driver's own rate. Both halves move the render state's PIPELINE version, so a cached
// pipeline built at the old rate cannot be handed back for the new one.
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
.subpass = 0, .subpass = 0,
.topology = vkTopology, .topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled, .primitiveRestartEnable = primitiveRestartEnabled,
@@ -5106,10 +5255,21 @@ void main() {
// program with a tessellation stage may only be drawn with GL_PATCHES), so nothing legal // program with a tessellation stage may only be drawn with GL_PATCHES), so nothing legal
// loses its pass-through here; what it does lose is the pipeline, because the refusal // loses its pass-through here; what it does lose is the pipeline, because the refusal
// below then sees an evaluation stage with no control stage and declines. // below then sees an evaluation stage with no control stage and declines.
//
// The default tessellation levels (glPatchParameterfv) are draw state for the same reason
// and are compiled into the same module, so they are read here too and their key is mixed
// into the pipeline hash - without that a pipeline memoised at one set of levels would be
// handed back after the application changed them.
if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable && if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable &&
vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) { vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
payload.passthroughTessControlStage = const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();
m_programFactory->GetOrCreatePassthroughTessControlStage(payload.patchControlPoints); const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();
payload.passthroughTessControlKey = ProgramFactory::ComputePassthroughTessControlKey(
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
programObj.passthroughPerVertexMembers);
payload.passthroughTessControlStage = m_programFactory->GetOrCreatePassthroughTessControlStage(
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
programObj.passthroughPerVertexMembers);
} }
if (!payload.stencilTestEnable) { if (!payload.stencilTestEnable) {
payload.frontStencilFailOp = VK_STENCIL_OP_KEEP; payload.frontStencilFailOp = VK_STENCIL_OP_KEEP;
@@ -5309,8 +5469,21 @@ void main() {
colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format; colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format;
} else if (colorAttachmentRenderbuffer != nullptr) { } else if (colorAttachmentRenderbuffer != nullptr) {
textureExternalIndex = static_cast<Int>(colorAttachmentRenderbuffer->GetExternalIndex()); textureExternalIndex = static_cast<Int>(colorAttachmentRenderbuffer->GetExternalIndex());
colorAttachmentFormat = MG_Util::ConvertTextureInternalFormatToVkEnum( // The SAME resolver GetOrCreateRenderbufferResource backs the image with, so the
colorAttachmentRenderbuffer->GetInternalFormat()); // probe cannot ask about a format the attachment does not have. The strict 1:1
// converter is the wrong question here and answered VK_FORMAT_UNDEFINED for
// RGBA2/RGBA12/RGB10/RGB12/RGB16 and the packed 16-bit formats for RGBA4/RGB5_A1
// - and VkFormatProperties for UNDEFINED are all zero, so blending was
// force-disabled forever on attachments that blend perfectly well. Every
// three-channel colour renderbuffer was in that set too (R8G8B8_UNORM is rarely
// supported), which is the more ordinary shape.
//
// Resolved rather than looked up: GetOrCreateRenderbufferResource creates images
// and bumps epochs, which a pipeline-state query must not do as a side effect.
// A renderbuffer has no device-fallback step after the resolver (unlike the
// texture path's D24 -> D32 substitution), so the resolver IS its live format.
colorAttachmentFormat =
ResolveTextureFormatInfo(colorAttachmentRenderbuffer->GetInternalFormat()).format;
} else { } else {
auto* texture = colorAttachmentTexture; auto* texture = colorAttachmentTexture;
MOBILEGL_ASSERT(texture != nullptr, MOBILEGL_ASSERT(texture != nullptr,
@@ -5391,6 +5564,7 @@ void main() {
entry.vertexInputHash = vertexLayoutHash; entry.vertexInputHash = vertexLayoutHash;
entry.renderPassHash = renderPassHash; entry.renderPassHash = renderPassHash;
entry.pipelineStateHash = pipelineStateHash; entry.pipelineStateHash = pipelineStateHash;
entry.primitiveRestartEnable = primitiveRestartEnable;
entry.transformFlags = transformFlags; entry.transformFlags = transformFlags;
entry.pipeline = pipeline; entry.pipeline = pipeline;
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize; m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
@@ -5775,7 +5949,11 @@ void main() {
return false; return false;
} }
SetupDrawSnapshot& snap = *snapPtr; SetupDrawSnapshot& snap = *snapPtr;
if (snap.aspects != aspects.GetRaw() || snap.mode != mode) { // Resolved once for the whole function: it guards the snapshot, keys the pipeline memo
// probe below, and is handed to GetOrCreatePipeline on a miss - all three must agree.
const Bool drawPrimitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView);
if (snap.aspects != aspects.GetRaw() || snap.mode != mode ||
snap.primitiveRestartEnable != drawPrimitiveRestartEnable) {
return false; return false;
} }
if (m_clearManager->HasAnyPendingClears()) { if (m_clearManager->HasAnyPendingClears()) {
@@ -6045,6 +6223,7 @@ void main() {
entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash && entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash &&
entry.renderPassHash == snap.renderPassHash && entry.renderPassHash == snap.renderPassHash &&
entry.pipelineStateHash == m_pipelineStateHash && entry.pipelineStateHash == m_pipelineStateHash &&
entry.primitiveRestartEnable == drawPrimitiveRestartEnable &&
entry.transformFlags == memoTransformFlags) { entry.transformFlags == memoTransformFlags) {
pipeline = entry.pipeline; pipeline = entry.pipeline;
break; break;
@@ -6055,14 +6234,17 @@ void main() {
// index, depth/stencil participation, image epochs, no pending clears) // index, depth/stencil participation, image epochs, no pending clears)
// was verified unchanged above, so this is a pure cache hit on the same // was verified unchanged above, so this is a pure cache hit on the same
// entry the snapshot's pipeline was built against. // entry the snapshot's pipeline was built against.
const RenderPassEntry& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( const RenderPassEntry* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
*drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil); *drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil);
if (!activeRenderPass->CompatibleWith(renderPassEntry)) { // A decline (nullptr) is an attachment DirectVulkan cannot represent; the builder
// has already logged it. Fall out of the fast path the same way an incompatible
// pass does - the full path re-resolves, declines again and drops the draw.
if (renderPassEntry == nullptr || !activeRenderPass->CompatibleWith(*renderPassEntry)) {
return false; return false;
} }
pipeline = GetOrCreatePipeline(mode, program, programObj, pipeline = GetOrCreatePipeline(mode, program, programObj,
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags), ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
vao, renderPassEntry); vao, *renderPassEntry, drawPrimitiveRestartEnable);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
return false; return false;
} }
@@ -6447,12 +6629,23 @@ void main() {
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) || MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest); MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
auto* renderPassEntry = auto* renderPassEntry =
&m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil); m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
// nullptr: the framebuffer has an attachment DirectVulkan cannot represent (a texture the
// texture manager declined to back, or a view it could not build). The builder logged which
// one; drop the draw here, exactly as an unresolvable sampler descriptor drops one in
// BindProgramUniformBuffers. Before this existed the same condition dereferenced a null
// resource or handed VK_NULL_HANDLE to vkCreateFramebuffer and took the process down.
if (renderPassEntry == nullptr) {
return false;
}
if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
activeRenderPass = nullptr; activeRenderPass = nullptr;
renderPassEntry = renderPassEntry =
&m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil); m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
if (renderPassEntry == nullptr) {
return false;
}
} }
if (renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { if (renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
MGLOG_D("SetupDraw skipped: drawFbo=%u resolved to an empty render pass (attachmentCount=%u extent=%dx%d)", MGLOG_D("SetupDraw skipped: drawFbo=%u resolved to an empty render pass (attachmentCount=%u extent=%dx%d)",
@@ -6499,7 +6692,8 @@ void main() {
} }
} }
auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry); auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry,
ResolvePrimitiveRestartEnable(aspects, pIndexBufferView));
// GetOrCreatePipeline documents a VK_NULL_HANDLE return (empty stages, or a driver that // GetOrCreatePipeline documents a VK_NULL_HANDLE return (empty stages, or a driver that
// rejected vkCreateGraphicsPipelines). Binding it dereferences null inside the driver - // rejected vkCreateGraphicsPipelines). Binding it dereferences null inside the driver -
// 9 of the 15 CTS process deaths were exactly this vkCmdBindPipeline. A draw that has no // 9 of the 15 CTS process deaths were exactly this vkCmdBindPipeline. A draw that has no
@@ -6562,6 +6756,7 @@ void main() {
if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) { if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) {
snap.valid = true; snap.valid = true;
snap.aspects = aspects.GetRaw(); snap.aspects = aspects.GetRaw();
snap.primitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView);
snap.mode = mode; snap.mode = mode;
snap.programLifetimeId = program.GetLifetimeId(); snap.programLifetimeId = program.GetLifetimeId();
snap.programVersion = program.GetBackendStateVersion(); snap.programVersion = program.GetBackendStateVersion();
@@ -6796,8 +6991,10 @@ void main() {
} }
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); auto* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
if (renderPassEntry->attachmentCount == 0 || // A declined render pass is the same answer as an empty one for a clear: there is nothing
// attached that can be cleared inside a pass. The builder has already logged the reason.
if (renderPassEntry == nullptr || renderPassEntry->attachmentCount == 0 ||
renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
return ScissoredClearPrep::NoOp; return ScissoredClearPrep::NoOp;
} }
@@ -6827,7 +7024,10 @@ void main() {
activeRenderPass = nullptr; activeRenderPass = nullptr;
// Re-resolve: ending the pass updates tracked attachment layouts, which feed the // Re-resolve: ending the pass updates tracked attachment layouts, which feed the
// entry's load ops and initial layouts. // entry's load ops and initial layouts.
renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired); renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
if (renderPassEntry == nullptr) {
return ScissoredClearPrep::NoOp;
}
} }
// A still-active pass is necessarily compatible here: the block above ended any // A still-active pass is necessarily compatible here: the block above ended any
// incompatible one and nothing since can change the active pass. // incompatible one and nothing since can change the active pass.
@@ -7992,8 +8192,14 @@ void main() {
// A color-only blit never touches depth/stencil: let the default-FBO pass // A color-only blit never touches depth/stencil: let the default-FBO pass
// it opens skip the depth attachment (depth-less flavor). // it opens skip the depth attachment (depth-less flavor).
auto& renderPassEntry = auto* renderPassEntryPtr =
m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired, /*drawUsesDepthStencil=*/false); m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired, /*drawUsesDepthStencil=*/false);
if (renderPassEntryPtr == nullptr) {
// Declined (the builder logged which attachment). The caller's contract for `false` is
// "this blit was not serviced here", which is the honest answer.
return false;
}
auto& renderPassEntry = *renderPassEntryPtr;
const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry); const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry);
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
@@ -8944,6 +9150,12 @@ void main() {
VkExtent2D extent = {0, 0}; VkExtent2D extent = {0, 0};
Uint32 depth = 1; Uint32 depth = 1;
Uint32 arrayLayers = 1; Uint32 arrayLayers = 1;
// Both resources carry a format; this copy used to decline to read it, which is why a
// four-row drift between the texture and renderbuffer format tables turned into
// corrupted texels with nothing in the log. vkCmdCopyImage requires size-compatible
// formats whenever they differ (VUID-vkCmdCopyImage-srcImage-01548) and there is no
// downstream check - a mismatched pair is a promise the driver takes at face value.
VkFormat format = VK_FORMAT_UNDEFINED;
}; };
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel, Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
@@ -9063,6 +9275,7 @@ void main() {
out.extent = resource->extent; out.extent = resource->extent;
out.depth = 1; out.depth = 1;
out.arrayLayers = 1; out.arrayLayers = 1;
out.format = resource->format;
return out.image != VK_NULL_HANDLE; return out.image != VK_NULL_HANDLE;
} }
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
@@ -9078,6 +9291,7 @@ void main() {
out.extent = resource->extent; out.extent = resource->extent;
out.depth = resource->depth; out.depth = resource->depth;
out.arrayLayers = resource->arrayLayers; out.arrayLayers = resource->arrayLayers;
out.format = resource->format;
return true; return true;
}; };
CopyImageVkImage srcImage{}; CopyImageVkImage srcImage{};
@@ -9118,6 +9332,42 @@ void main() {
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels); srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
return; return;
} }
// Size compatibility, the guard whose absence let a table drift two files away reach the
// driver as a promise. glCopyImageSubData is a raw texel-block move (GL 4.6 core 18.3.2), and
// Vulkan says as much: when the two formats differ they must be size-compatible - the same
// texel block size - or vkCmdCopyImage is undefined (VUID-vkCmdCopyImage-srcImage-01548).
// Nothing else on this path asks: the three checks around it cover the mip range, the region
// bounds and the slice range, and none of them ever looked at a format.
//
// A decline rather than a MOBILEGL_ASSERT, for the reason the neighbouring guards spell out:
// assertions compile out of the release build that the CTS and shipping both run, which is
// exactly where the corruption was observed.
if (srcImage.format != dstImage.format) {
// Size-compatibility is the COLOUR rule. Vulkan makes each depth/stencil format compatible
// only with ITSELF, and the texel block sizes cannot tell them apart: X8_D24_UNORM_PACK32,
// D32_SFLOAT and D24_UNORM_S8_UINT are all 4 bytes and all in different compatibility
// classes, so a raw block-size test waves through exactly the pairs Vulkan forbids. The
// frontend cannot filter them either - its own texel-block resolver is byte-size only, so
// glCopyImageSubData between a GL_DEPTH_COMPONENT24 texture and a GL_DEPTH_COMPONENT32F
// one reaches here with two different depth formats and 4 == 4.
const Bool eitherIsDepthStencil =
((srcImage.aspect | dstImage.aspect) & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
if (eitherIsDepthStencil) {
MGLOG_E_ONCE("%s: depth/stencil formats are compatible only with themselves, and source format "
"%d differs from destination format %d; declining the copy",
__func__, static_cast<Int>(srcImage.format), static_cast<Int>(dstImage.format));
return;
}
const Uint32 srcBlockSize = vkuGetFormatInfo(srcImage.format).texel_block_size;
const Uint32 dstBlockSize = vkuGetFormatInfo(dstImage.format).texel_block_size;
if (srcBlockSize == 0 || dstBlockSize == 0 || srcBlockSize != dstBlockSize) {
MGLOG_E_ONCE("%s: source format %d and destination format %d are not size-compatible "
"(%u vs %u bytes per texel block); declining the copy",
__func__, static_cast<Int>(srcImage.format), static_cast<Int>(dstImage.format),
srcBlockSize, dstBlockSize);
return;
}
}
const VkImageAspectFlags copyAspectMask = const VkImageAspectFlags copyAspectMask =
srcImage.aspect & dstImage.aspect & srcImage.aspect & dstImage.aspect &
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
@@ -10190,9 +10440,38 @@ void main() {
} }
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) { // Two shapes end up in the same place, and for the same reason: the GL level being read has
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: failed to sync textureId=%u", // no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
textureObject->GetExternalIndex()); // of its bytes - which makes the shadow both the safe answer and the correct one.
//
// (a) No VkImage at all. A mutable texture whose GL level 0 was never defined -
// glTexImage2D(GL_TEXTURE_2D, 5, ...) and nothing else, exactly what the
// clear_tex_image conformance cases build. VkTextureManager takes storage mip 0 as the
// physical image extent (CheckMipmapCompleteness), so it refuses to back the texture.
// (b) A VkImage with FEWER mip levels than the GL level count. GetUploadMipLevelCount
// breaks at the first level with a zero extent, so "level 0 defined, a gap, level 3
// defined" produces a one-mip image while GL_TEXTURE_MAX_LEVEL-style state still
// reports four levels. The same clamp also fires on a base level small enough that the
// full chain is shorter than the levels the application defined.
//
// (b) is the dangerous one and is why the level is bounded against the RESOURCE and not only
// against the GL-side count above: writing that level into imageSubresource.mipLevel is an
// out-of-range subresource, which is the promise the driver takes at face value. The
// glCopyImageSubData path two functions up carries the same guard for the same reason, added
// after it SIGSEGV'd inside the Adreno driver; the readback never had one.
const Bool hasImage = resource != nullptr && resource->image != VK_NULL_HANDLE;
const Bool levelIsBacked =
hasImage && ToStorageMipLevel(textureObject.get(), level) < resource->mipLevels;
if (!levelIsBacked) {
// Never gated on "syncing was inconvenient": a blanket shadow answer would silently
// return stale bytes for every render-to-texture result.
MGLOG_D("DirectVulkan::GetTexImage: textureId=%u level %d has no GPU storage (%s); answering "
"from the CPU shadow",
textureObject->GetExternalIndex(), level,
hasImage ? "the image has fewer mip levels" : "the texture has no VkImage");
MG_Impl::GLImpl::CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format,
type, bufSize, pixels,
"DirectVulkan::GetTextureImage");
return; return;
} }
@@ -10351,15 +10630,23 @@ void main() {
void VulkanRenderer::GenerateMipmap(GLenum target) { void VulkanRenderer::GenerateMipmap(GLenum target) {
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// The other mipmappable targets - 1D, 1D array, cube map array - are legal GL and the front // Whatever is left here is a coverage gap in this backend, not a broken invariant, so it
// end lets them through, so reaching one here is a coverage gap in this backend, not a // declines (leaving the mip chain unwritten) rather than asserting the process down. What
// broken invariant. Declining leaves the mip chain unwritten; asserting took the process // remains is the multisample targets, which GL 4.6 core 8.14.4 forbids to glGenerateMipmap
// down with it. // outright.
//
// Every ARRAY target - 1D array, 2D array, cube map array - needs no blit code of its own:
// its layers live in the VkImage's arrayLayers, so resource->extent/depth already describe
// one layer's image and the loop below already copies every layer per level via
// srcSubresource.layerCount = resource->arrayLayers. The one thing they DO need is that the
// GL-space storage allocation not shrink the layer count down the chain, which
// MipShrinkingComponentCount handles.
if (textureTarget != TextureTarget::Texture2D && textureTarget != TextureTarget::Texture2DArray && if (textureTarget != TextureTarget::Texture2D && textureTarget != TextureTarget::Texture2DArray &&
textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::TextureCubeMap && textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::TextureCubeMap &&
textureTarget != TextureTarget::TextureCubeMapArray &&
// A 1D texture needs nothing special: its storage extent is {width, 1, 1}, so the blit // A 1D texture needs nothing special: its storage extent is {width, 1, 1}, so the blit
// loop below already emits the y and z offsets of 0 and 1 that a 1D image requires. // loop below already emits the y and z offsets of 0 and 1 that a 1D image requires.
textureTarget != TextureTarget::Texture1D) { textureTarget != TextureTarget::Texture1D && textureTarget != TextureTarget::Texture1DArray) {
MGLOG_W_ONCE("GenerateMipmap: unsupported target %s", MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); MGLOG_W_ONCE("GenerateMipmap: unsupported target %s", MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
return; return;
} }
@@ -12709,6 +12996,11 @@ void main() {
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE; m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend; deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE; m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
// ARB_sample_shading. Without this feature a pipeline may not set sampleShadingEnable
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784), so the GL enable
// has to be dropped rather than forwarded - which is what the flag below records.
deviceFeatures.sampleRateShading = supportedDeviceFeatures.sampleRateShading;
m_sampleRateShadingFeatureEnabled = deviceFeatures.sampleRateShading == VK_TRUE;
// ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly // ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's // one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
// gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are // gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are
@@ -12976,6 +13268,55 @@ void main() {
} }
} }
// VK_EXT_custom_border_color: an arbitrary GL_TEXTURE_BORDER_COLOR, in float or integer form,
// instead of the four predefined VkBorderColor values. Without it a border outside
// transparent black / opaque black / opaque white has to be snapped, which is what made every
// border texel of a GL_RGBA8 texture with border (255,255,255,255) sample as 0 and what made
// an integer border of -1 come back as 0.
//
// customBorderColorWithoutFormat is required alongside customBorderColors, not merely
// preferred: a GL sampler object carries a border colour with no idea which texture it will
// be paired with, so the VkSamplerCustomBorderColorCreateInfoEXT this backend builds has to
// leave `format` VK_FORMAT_UNDEFINED.
m_customBorderColorFeatureEnabled = false;
m_maxCustomBorderColorSamplers = 0;
VkPhysicalDeviceCustomBorderColorFeaturesEXT customBorderColorFeatures{};
customBorderColorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT;
if (IsExtensionSupported(availableExtensions, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &customBorderColorFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
if (customBorderColorFeatures.customBorderColors == VK_TRUE &&
customBorderColorFeatures.customBorderColorWithoutFormat == VK_TRUE) {
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
}
customBorderColorFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &customBorderColorFeatures;
m_customBorderColorFeatureEnabled = true;
if (getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceCustomBorderColorPropertiesEXT customBorderColorProperties{};
customBorderColorProperties.sType =
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_PROPERTIES_EXT;
VkPhysicalDeviceProperties2 propertyQuery{};
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
propertyQuery.pNext = &customBorderColorProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
m_maxCustomBorderColorSamplers = customBorderColorProperties.maxCustomBorderColorSamplers;
}
MGLOG_I("Enabled optional device extension: %s (maxCustomBorderColorSamplers=%u)",
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, m_maxCustomBorderColorSamplers);
}
}
if (!m_customBorderColorFeatureEnabled) {
MGLOG_I("%s unavailable; GL_TEXTURE_BORDER_COLOR snaps to the nearest predefined VkBorderColor",
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
}
// Native subgroup topology, and VK_EXT_subgroup_size_control's // Native subgroup topology, and VK_EXT_subgroup_size_control's
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what // computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
// turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from // turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from
@@ -584,6 +584,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false; Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
// colour outside the palette has to be snapped to the nearest predefined one. Both features
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
// colour without naming the image format it will be paired with, which GL's sampler objects
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
// to be able to fall back to the snapped value once it is reached.
Bool m_customBorderColorFeatureEnabled = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
Bool m_sampleRateShadingFeatureEnabled = false;
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports // multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1 // (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline // when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
@@ -733,6 +749,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// values the memo already holds. // values the memo already holds.
Uint64 pipelineStateHash = 0; Uint64 pipelineStateHash = 0;
ProgramFactory::CompileOptionFlags transformFlags = {}; ProgramFactory::CompileOptionFlags transformFlags = {};
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
// from anything else in this key: it depends on whether the draw is indexed and on the
// index type, neither of which the mode/program/state hashes carry. Without it an
// indexed and a non-indexed draw over the same program and state collide on one entry
// and the second one gets the first one's restart setting.
Bool primitiveRestartEnable = false;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
}; };
static constexpr Uint32 kPipelineMemoSize = 8; static constexpr Uint32 kPipelineMemoSize = 8;
@@ -865,6 +887,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 bindGeneration = 0; Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0; Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0; Uint32 resolvedTransformFlags = 0;
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
// already separates indexed from non-indexed draws, but not one index TYPE from
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
// makes those two draws want different pipelines.
Bool primitiveRestartEnable = false;
Uint64 renderPassHash = 0; Uint64 renderPassHash = 0;
Uint32 imageIndex = 0; Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0; Uint64 textureEraseEpoch = 0;
@@ -1168,13 +1196,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CreateSwapchain(); void CreateSwapchain();
void CreateCommandPool(); void CreateCommandPool();
// Whether THIS draw's primitive stream restarts, and therefore what
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
// and the index TYPE (an application restart index that does not fit the type matches no
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
const IndexBufferView* pIndexBufferView) const;
VkPipeline GetOrCreatePipeline( VkPipeline GetOrCreatePipeline(
GLenum mode, GLenum mode,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags, ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); void DestroyComputePipelines();
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to // Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
+156 -25
View File
@@ -34,8 +34,81 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
static Bool ValidateCurrentProgramForExecution(const char* functionName) { // Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName); // is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
// backend samples during this draw describes the program it is drawing), so the draw funnel
// below resolves it exactly once and hands it to both users.
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const char* functionName) {
// "If there is no current program object or bound program pipeline object, the results of
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
// the compute shader stage"), which is why this lives on the draw path and not in the
// shared ValidateProgramForExecution below.
if (!currentProgram) return false;
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
// inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
// evaluation or geometry stage has an executable but no program supplies an executable
// VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
// shape - so in practice it catches a program pipeline assembled out of stage programs,
// which today draws happily and renders nothing.
//
// Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
// program is the graphics composite, whose linked-shader snapshot is built out of exactly
// the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
// stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
// deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
// nothing else here should start refusing draws GL accepts.
//
// On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
// which shares that helper and legitimately has no vertex stage - is untouched.
const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
return false;
}
return true;
}
// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
// default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
// for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
// drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
// rules out baking it at link time.
//
// Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
// decides the value and either half can move between draws. It costs a phase-A flag read for
// every program that has no shim, and a 4-byte compare for the ones that do: the write only
// bumps the UBO content version when the number actually changes, so a run of draws into one
// framebuffer re-uploads nothing.
static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
if (!program || !program->UsesReservedNumSamples()) return;
// GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
// when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
}
// The one funnel every drawing command passes through. Order is load-bearing: validate first
// (a rejected draw must leave state alone), then publish the sample count - which reads the
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
// and before the backend consumes the program's UBO content version.
static Bool PrepareCurrentProgramForDraw(const char* functionName) {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
PublishDrawFramebufferSampleCount(currentProgram);
return true;
} }
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound // A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
@@ -713,6 +786,37 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
// NO control stage tessellates at; both backends have to synthesize that control stage
// themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
// there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
// pname is the only error the spec lists: any float values are accepted, negatives and NaN
// included, and it is the tessellator that clamps them.
//
// This used to be a stub, which is why the two synthesizers hardcoded 1.0.
void PatchParameterfv(GLenum pname, const GLfloat* values) {
if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
return;
}
if (!values) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
return;
}
if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
MG_State::pGLContext->SetPatchDefaultOuterLevel(
FloatVec4(values[0], values[1], values[2], values[3]));
} else {
MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
}
}
namespace { namespace {
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the // GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with // barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
@@ -751,6 +855,27 @@ namespace MobileGL::MG_Impl::GLImpl {
memoryBarrier(barriers); memoryBarrier(barriers);
} }
void TextureBarrier() {
// GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
// framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
// read texels of a texture that is also attached to the current framebuffer.
//
// Both backends serve this through their existing memory-barrier hook rather than a new
// entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
// writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
// is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
// scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
// subset, so it cannot under-synchronize.
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
return;
}
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
}
void MemoryBarrierByRegion(GLbitfield barriers) { void MemoryBarrierByRegion(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return; if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion; auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
@@ -766,14 +891,14 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride); MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
} }
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride); MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
} }
@@ -851,7 +976,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE // NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
// they produce for a call made with no program bound. Same precedence decision, and // they produce for a call made with no program bound. Same precedence decision, and
// the same reason, as DispatchComputeIndirect above. // the same reason, as DispatchComputeIndirect above.
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount; auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
if (!multiDrawElementsIndirectCount) { if (!multiDrawElementsIndirectCount) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -872,7 +997,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// See MultiDrawElementsIndirectCount, including why this one goes last. // See MultiDrawElementsIndirectCount, including why this one goes last.
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount; auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
if (!multiDrawArraysIndirectCount) { if (!multiDrawArraysIndirectCount) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -887,7 +1012,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) { const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
@@ -897,7 +1022,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElements_Backend(mode, start, end, count, type, indices); DrawRangeElements_Backend(mode, start, end, count, type, indices);
} }
@@ -905,7 +1030,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) { GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex, DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
baseinstance); baseinstance);
@@ -914,7 +1039,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) { GLsizei instancecount, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
@@ -925,21 +1050,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) { GLsizei instancecount, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance); DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
} }
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount); DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
} }
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return; if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
@@ -949,21 +1074,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) { GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance); DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
} }
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount); DrawArraysInstanced_Backend(mode, first, count, instancecount);
} }
void DrawArraysIndirect(GLenum mode, const void* indirect) { void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return; if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
DrawArraysIndirect_Backend(mode, indirect); DrawArraysIndirect_Backend(mode, indirect);
@@ -971,7 +1096,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return; if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
@@ -981,7 +1106,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArrays(GLenum mode, GLint first, GLsizei count) { void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count); AccountTransformFeedbackPrimitives(mode, count);
DrawArrays_Backend(mode, first, count); DrawArrays_Backend(mode, first, count);
@@ -989,7 +1114,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) { if (drawcount < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1003,7 +1128,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount) { GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElements_Backend(mode, count, type, indices, drawcount); MultiDrawElements_Backend(mode, count, type, indices, drawcount);
} }
@@ -1011,7 +1136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount, const GLint* basevertex) { GLsizei drawcount, const GLint* basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return; if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
@@ -1035,7 +1160,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return; if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count); AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices); DrawElements_Backend(mode, count, type, indices);
@@ -1459,7 +1584,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// (GL 4.6 core 10.3.7). // (GL 4.6 core 10.3.7).
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream, static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
GLsizei instancecount) { GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(functionName)) return; if (!PrepareCurrentProgramForDraw(functionName)) return;
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return; if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
if (instancecount < 0) { if (instancecount < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1482,8 +1607,13 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(id) + " is not a transform feedback object name.")); std::to_string(id) + " is not a transform feedback object name."));
return; return;
} }
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists. // GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
if (stream != 0) { // answers as 1 - so stream 0 is the only one that exists and anything else is
// INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
// never drift: if vertex-stream support ever lands, this bound moves with the limit.
GLint maxVertexStreams = 1;
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -1501,6 +1631,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id); const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
if (vertices == 0) return; if (vertices == 0) return;
const auto count = static_cast<GLsizei>(vertices); const auto count = static_cast<GLsizei>(vertices);
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ); void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect); void DispatchComputeIndirect(GLintptr indirect);
void PatchParameteri(GLenum pname, GLint value); void PatchParameteri(GLenum pname, GLint value);
void PatchParameterfv(GLenum pname, const GLfloat* values);
void MemoryBarrier(GLbitfield barriers); void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers); void MemoryBarrierByRegion(GLbitfield barriers);
void TextureBarrier();
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
@@ -160,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height) DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert) DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length) DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length) DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask) DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask) DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
@@ -411,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value) DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value) DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
@@ -923,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices) DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values) DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id) DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
@@ -994,7 +994,7 @@ DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers) DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures) DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides) DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth) DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids) DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
@@ -1107,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, ) DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue) DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride) DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride) DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp) DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW) DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture) DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler) DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
@@ -1150,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face) DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue) DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
@@ -2049,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp) DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode) DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue) DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
@@ -2546,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, ) DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
@@ -474,6 +474,75 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
// one another in that section and a CTS case that walks the family must not get five
// different answers - which is exactly what happened when these lived in one helper that
// only two of the five went through.
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
const SharedPtr<MG_State::GLState::FramebufferObject>&
framebufferObject,
FramebufferAttachmentType attachmentType) {
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
// than or equal to MAX_COLOR_ATTACHMENTS."
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
// object is always there, and framebuffer 0 has to be recognised by identity instead,
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
// from what the swapchain keeps publishing.
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
if (!framebufferObject ||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"No framebuffer object is bound to the target; the default framebuffer's attachments "
"cannot be named."));
return false;
}
return true;
}
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
// texture with immutable storage it "must be smaller than the number of levels the texture
// has". Split from the attachment-point half because the caller only has a texture object
// once the detach (texture == 0) case is behind it.
Bool ValidateFramebufferTextureLevel(const char* functionName,
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level) {
if (level < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Texture level must be non-negative."));
return false;
}
if (!textureObject || !textureObject->IsImmutable()) {
// A mutable texture has no level bound here: a level it has not specified yet is
// not an error, it just leaves the framebuffer incomplete.
return true;
}
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
// two-level view onto a ten-level texture would accept level 5 and attach an image
// nothing can draw into.
const Uint levelBound = textureObject->GetAddressableLevelCount();
if (static_cast<Uint>(level) >= levelBound) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
levelBound)));
return false;
}
return true;
}
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment, void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level, GLuint texture, GLint level,
TextureUploadTarget textureUploadTarget, Bool layered = false) { TextureUploadTarget textureUploadTarget, Bool layered = false) {
@@ -482,10 +551,24 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
// which half is being recorded - and dropping it here (the parameter defaults to
// false) recorded a non-layered depth/stencil attachment beside a layered colour
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
// own rule, and downstream it means the depth/stencil attachment covers layer 0
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
// attached one layer of it beside a layered colour target, which the driver
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
// produced nothing. This is the shape
// texture_cube_map_array.stencil_attachments_*_layered and
// geometry_shader.layered_framebuffer.stencil_support are built on.
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
textureUploadTarget); textureUploadTarget, layered);
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
textureUploadTarget); textureUploadTarget, layered);
return; return;
} }
@@ -497,13 +580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject(); auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) { if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -518,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget); const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown || if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -624,16 +702,33 @@ namespace MobileGL::MG_Impl::GLImpl {
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own // GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION. // (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path resolves the limit per format the same way // The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL // (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and // pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure // frontend would accept a count it had advertised globally - but on Adreno and Mali the
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the // integer path is genuinely one sample, and accepting four only moved the failure from an
// count it actually hands the driver. // honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 0;
}
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
const SizeT formatIndex = static_cast<SizeT>(format);
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
return 0;
}
const auto& sampleCounts =
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) { Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
} }
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format); GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA; GLenum normalizedFormat = GL_RGBA;
@@ -644,13 +739,24 @@ namespace MobileGL::MG_Impl::GLImpl {
&normalizedType); &normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER || const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER; normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
// supported for internalformat (see GetInternalformativ)", and
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
// list. It was never consulted here - the TODO that deferred it was written before the
// query was backed and had gone stale - so a format whose multisample probes fail inside
// a category that allows four was accepted at four, quietly allocated at one by
// ClampSamplesToBackendSupport, and then reported as four by
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
if (probedMaxSamples > 0) {
return probedMaxSamples;
}
if (!isIntegerFormat) { if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State(); return GetMaxRenderbufferSamples_State();
} }
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised: // Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend return GetAdvertisedIntegerMaxSamples();
// clamps to it, while the frontend honours what it advertised.
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
} }
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) { Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -682,8 +788,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once // Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
// glGetInternalformativ is backed; integer formats are handled below. // to the format's category pname where nothing was probed. (This carried a TODO deferring
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
// both renderbuffers and multisample textures since, so the deferral was collected.)
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format); const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
if (samples > maxSamples) { if (samples > maxSamples) {
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports // GL 4.6 core 9.2.4 makes asking for more samples than the format supports
@@ -1048,13 +1156,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject(); auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) { if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -1069,6 +1171,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
if (layer < 0) { if (layer < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
@@ -1191,6 +1294,13 @@ namespace MobileGL::MG_Impl::GLImpl {
"Framebuffer target is bound to no framebuffer object.")); "Framebuffer target is bound to no framebuffer object."));
return; return;
} }
// glFramebufferTexture2D is by far the most-used member of the family and the only one
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
// conditions have to be asked here explicitly.
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
attachmentType)) {
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -1205,6 +1315,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget); const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown || if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -1241,6 +1352,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
// every texture name that was never generated.
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) { if (!textureObject) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1291,13 +1408,10 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (level < 0) { // The whole level condition, not just its negative half: glNamedFramebufferTexture and
MG_State::pGLContext->RecordError( // glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
ErrorCode::InvalidValue, // be rejected on both or a CTS case gets two answers for one rule.
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State", if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
"Texture level must be non-negative."));
return;
}
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown; TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
Bool layered = false; Bool layered = false;
+390 -46
View File
@@ -7,7 +7,9 @@
// End of Source File Header // End of Source File Header
#include "GL_Getter.h" #include "GL_Getter.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits>
#include <Config.h> #include <Config.h>
#include <MGGitHash.h> #include <MGGitHash.h>
#include <MG_Impl/GLImpl/Debug/GL_Debug.h> #include <MG_Impl/GLImpl/Debug/GL_Debug.h>
@@ -93,8 +95,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// limits they advertise still have to be legal. // limits they advertise still have to be legal.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
constexpr GLint kFrontendMaxDebugLoggedMessages = 1; constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
constexpr GLint kFrontendMaxVertexUniformComponents = 4096; // The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
constexpr GLint kFrontendMaxVertexUniformVectors = 128; // literal: they used to be independent (4096 components against 128 vectors, 64 varying
// components against 8 varying vectors) and could not both be describing the same
// capacity. Both are shared with BuildTBuiltInResource through Types.h, because
// gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
constexpr GLint kFrontendMaxVertexUniformComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
constexpr GLint kFrontendMaxVertexUniformVectors =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
constexpr GLint kFrontendMaxVertexUniformBlocks = 14; constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
constexpr GLint kFrontendMaxVertexOutputComponents = 64; constexpr GLint kFrontendMaxVertexOutputComponents = 64;
constexpr GLint kFrontendMaxFragmentInputComponents = 128; constexpr GLint kFrontendMaxFragmentInputComponents = 128;
@@ -106,21 +115,61 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16; constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024; constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14; constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks + // ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
kFrontendMaxGeometryUniformBlocks + // gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
kFrontendMaxFragmentUniformBlocks; // answer only; 32 is the GL 4.6 core minimum (table 23.57).
constexpr GLint kFrontendMaxVaryingComponents = 64; constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
constexpr GLint kFrontendMaxVaryingVectors = 8; constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
// The compute stage's share of the combined sum below. Compute's own per-stage answer is
// backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not
// what that query returns - it is the GL 4.3 core minimum, present here only so the
// combined total covers all SIX stages.
constexpr GLint kFrontendMaxComputeUniformBlocksShare = 14;
// GL 4.6 table 23.64 orders MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >=
// every per-stage count, and the sum has to run over SIX stages, not three and not five.
// Three (42) was the original bug. Five (70) replaced it and broke the middle term the
// other way: compute's per-stage count is backend-derived and clamps at the binding count,
// so a device reporting descriptor-indexing-scale uniform buffers (Adreno reports
// maxPerStageDescriptorUniformBuffers = 16777216) advertised 84 compute blocks against a
// combined 70. Six stages x 14 = 84, which is also exactly the binding-point count and the
// arithmetic the GL 4.5 minimum of 84 bindings is built from, so the ordering is now tight
// rather than accidental.
constexpr GLint kFrontendMaxCombinedUniformBlocks =
kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
kFrontendMaxFragmentUniformBlocks + kFrontendMaxComputeUniformBlocksShare;
constexpr GLint kFrontendMaxVaryingComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
constexpr GLint kFrontendMaxVaryingVectors =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
constexpr GLint kFrontendMaxProgramTexelOffset = 7; constexpr GLint kFrontendMaxProgramTexelOffset = 7;
constexpr GLint kFrontendMinProgramTexelOffset = -8; constexpr GLint kFrontendMinProgramTexelOffset = -8;
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64; constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4; constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4; constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
// ARB_transform_feedback3's vertex-stream count. One is what this implementation can
// actually emit to; see the GL_MAX_VERTEX_STREAMS case for why it is not four.
constexpr GLint kFrontendMaxVertexStreams = 1;
constexpr GLint kFrontendMaxGeometryOutputVertices = 256; constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024; constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
constexpr GLint kFrontendMinUniformBufferBindings = 36; // GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
// how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
// GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
// the ceiling here.
constexpr GLint kFrontendMinUniformBufferBindings = 84;
constexpr GLint kFrontendSubpixelBits = 4; constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 4; constexpr GLint kFrontendMaxSamples =
static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
// ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
// glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
// plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
// suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
// caller reading its own uninitialised stack slot and strands an error for the next
// unrelated call to trip over. The extension is deliberately NOT advertised, so the
// numbers are a table entry, not a capability claim.
constexpr GLint kFrontendMaxSubroutines = 256;
constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile // The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
// pipeline (CaptureCompileEnv floors the same driver answers at them, and // pipeline (CaptureCompileEnv floors the same driver answers at them, and
@@ -134,9 +183,19 @@ namespace MobileGL::MG_Impl::GLImpl {
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0; return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
} }
// GL 4.6 core table 23.64: components + blocks * (blockSize / 4). The product has to be
// formed in 64 bits and saturated on the way out - it overflowed a signed 32-bit int on
// every Vulkan host that reports a large maxUniformBufferRange. A Mali driver answering
// 0xFFFFFFFF saturates to INT32_MAX in the loader, and 14 * (2147483647 / 4) + 4096 wraps
// to -1073737742, which the conformance suite read back as a limit "smaller than 58368".
// Saturating instead of wrapping is also the only honest answer: an implementation that
// can serve more components than a GLint holds still has to report a GLint.
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks, GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
GLint maxUniformBlockSizeBytes) { GLint maxUniformBlockSizeBytes) {
return maxDefaultUniformComponents + maxUniformBlocks * (maxUniformBlockSizeBytes / 4); const Int64 blocks = std::max<Int64>(static_cast<Int64>(maxUniformBlocks), 0);
const Int64 componentsPerBlock = std::max<Int64>(static_cast<Int64>(maxUniformBlockSizeBytes), 0) / 4;
const Int64 total = static_cast<Int64>(maxDefaultUniformComponents) + blocks * componentsPerBlock;
return static_cast<GLint>(std::min<Int64>(total, std::numeric_limits<GLint>::max()));
} }
bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) { bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) {
@@ -304,24 +363,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
GLint ResolveDrawFramebufferSampleCount() {
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (!drawFbo) return 0;
GLint maxSamples = 0;
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
} else if (attachment.IsTexture() && attachment.GetTexture()) {
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
// report 1 for any multisampled draw framebuffer).
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
}
}
return maxSamples;
}
void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) { void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
@@ -473,10 +514,18 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace } // namespace
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be // before it is advertised. gl_MaxSamples expands from the same floored number
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES // (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it //
// just told the application to use. The backends clamp the realised count instead. // THE FLOOR STOPS HERE, and that is the point. It used to be applied to
// GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
// on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
// every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
// reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
// single sample, so flooring the query at 4 did not make four samples exist - it made the
// backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
// per-sample data it could never read back. Reporting what was probed turns that into an
// honest "unsupported" the application can branch on.
GLint GetAdvertisedMaxSamples() { GLint GetAdvertisedMaxSamples() {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return kFrontendMaxSamples; return kFrontendMaxSamples;
@@ -484,6 +533,50 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples); return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
} }
// GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
// see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
// floored rather than trusted.
namespace {
GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 1;
}
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
}
} // namespace
GLint GetAdvertisedColorTextureMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
}
GLint GetAdvertisedDepthTextureMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
}
GLint GetAdvertisedIntegerMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
}
// Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
// gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
GLint ResolveDrawFramebufferSampleCount() {
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (!drawFbo) return 0;
GLint maxSamples = 0;
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
} else if (attachment.IsTexture() && attachment.GetTexture()) {
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
// report 1 for any multisampled draw framebuffer).
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
}
}
return maxSamples;
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
const GLubyte* GetString(GLenum name) { const GLubyte* GetString(GLenum name) {
static String vendorString; static String vendorString;
@@ -680,12 +773,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: { case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
// Same reason as the three above: the integer fallback would round the fraction to 0
// or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE.
case GL_MIN_SAMPLE_SHADING_VALUE: {
GLfloat value = 0.0f; GLfloat value = 0.0f;
GetFloatv(pname, &value); GetFloatv(pname, &value);
*params = value != 0.0f ? GL_TRUE : GL_FALSE; *params = value != 0.0f ? GL_TRUE : GL_FALSE;
return; return;
} }
// Float-native state, so GL 4.6 core 2.2.2's "zero becomes FALSE, every other value
// becomes TRUE" has to be applied to the VALUE. Answering these through the integer getter
// below instead - which rounds - reported GL_FALSE for a perfectly non-zero level of 0.25,
// and every other float state in this function already reads through GetFloatv for exactly
// that reason.
case GL_PATCH_DEFAULT_OUTER_LEVEL:
case GL_PATCH_DEFAULT_INNER_LEVEL: {
const GLsizei componentCount = pname == GL_PATCH_DEFAULT_OUTER_LEVEL ? 4 : 2;
GLfloat levels[4] = {};
GetFloatv(pname, levels);
for (GLsizei i = 0; i < componentCount; ++i) {
params[i] = levels[i] != 0.0f ? GL_TRUE : GL_FALSE;
}
return;
}
default: default:
break; break;
} }
@@ -735,6 +846,22 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = depthRange.y(); params[1] = depthRange.y();
return; return;
} }
// glPatchParameterfv's two states. Float-native, so they are answered here rather than
// through the integer fallback below - which rounds, and would report 0 for a level of 0.5.
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
params[0] = outer.x();
params[1] = outer.y();
params[2] = outer.z();
params[3] = outer.w();
return;
}
case GL_PATCH_DEFAULT_INNER_LEVEL: {
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
params[0] = inner.x();
params[1] = inner.y();
return;
}
case GL_VIEWPORT_BOUNDS_RANGE: { case GL_VIEWPORT_BOUNDS_RANGE: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.ViewportBoundsRangeMin; params[0] = dynamicParameters.ViewportBoundsRangeMin;
@@ -800,6 +927,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POLYGON_OFFSET_UNITS: case GL_POLYGON_OFFSET_UNITS:
params[0] = MG_State::pGLContext->GetPolygonOffsetUnits(); params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
return; return;
case GL_POLYGON_OFFSET_CLAMP:
// Float-native state, so it is answered here rather than through the integer
// fallback: glPolygonOffsetClamp(1, 1, 0.5) must read back as 0.5, not as 0.
params[0] = MG_State::pGLContext->GetPolygonOffsetClamp();
return;
case GL_SMOOTH_LINE_WIDTH_RANGE: { case GL_SMOOTH_LINE_WIDTH_RANGE: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.SmoothLineWidthRangeMin; params[0] = dynamicParameters.SmoothLineWidthRangeMin;
@@ -815,6 +947,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SAMPLE_COVERAGE_VALUE: case GL_SAMPLE_COVERAGE_VALUE:
params[0] = MG_State::pGLContext->GetSampleCoverageValue(); params[0] = MG_State::pGLContext->GetSampleCoverageValue();
return; return;
case GL_MIN_SAMPLE_SHADING_VALUE:
// Float state, so it has to be answered here rather than through the integer
// fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0.
params[0] = MG_State::pGLContext->GetMinSampleShadingValue();
return;
case GL_POINT_FADE_THRESHOLD_SIZE: case GL_POINT_FADE_THRESHOLD_SIZE:
// Float state: read it directly so the fractional part is not lost to the integer path. // Float state: read it directly so the fractional part is not lost to the integer path.
params[0] = MG_State::pGLContext->GetPointFadeThresholdSize(); params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
@@ -1186,6 +1323,13 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
switch (pname) { switch (pname) {
case GL_MAX_ELEMENT_INDEX:
// The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
// not left to the 32-bit fallback below: the conformance suite reads it with
// glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
// spec requires 2^32-1.
params[0] = 0xFFFFFFFFLL;
return;
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE: case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
if (MG_Backend::pActiveBackendObject) { if (MG_Backend::pActiveBackendObject) {
params[0] = static_cast<GLint64>( params[0] = static_cast<GLint64>(
@@ -1222,12 +1366,17 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint ints[4] = {}; GLint ints[4] = {};
GetIntegerv(pname, ints); GetIntegerv(pname, ints);
// GL 4.6 core 22.1 gives glGetInteger64v the same accepted-pname set as glGetIntegerv, so
// every pname the integer getter answers with several components owes them all here too.
// A pname that reaches the `default:` arm writes params[0] and leaves the caller's other
// components holding whatever they held, with no error to say so.
switch (pname) { switch (pname) {
case GL_BLEND_COLOR: case GL_BLEND_COLOR:
case GL_COLOR_CLEAR_VALUE: case GL_COLOR_CLEAR_VALUE:
case GL_COLOR_WRITEMASK: case GL_COLOR_WRITEMASK:
case GL_SCISSOR_BOX: case GL_SCISSOR_BOX:
case GL_VIEWPORT: case GL_VIEWPORT:
case GL_PATCH_DEFAULT_OUTER_LEVEL:
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
params[i] = static_cast<GLint64>(ints[i]); params[i] = static_cast<GLint64>(ints[i]);
} }
@@ -1237,6 +1386,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_VIEWPORT_DIMS: case GL_MAX_VIEWPORT_DIMS:
case GL_POINT_SIZE_RANGE: case GL_POINT_SIZE_RANGE:
case GL_VIEWPORT_BOUNDS_RANGE: case GL_VIEWPORT_BOUNDS_RANGE:
case GL_PATCH_DEFAULT_INNER_LEVEL:
params[0] = static_cast<GLint64>(ints[0]); params[0] = static_cast<GLint64>(ints[0]);
params[1] = static_cast<GLint64>(ints[1]); params[1] = static_cast<GLint64>(ints[1]);
return; return;
@@ -1268,6 +1418,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POINT_SIZE_RANGE: case GL_POINT_SIZE_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE: case GL_SMOOTH_LINE_WIDTH_RANGE:
case GL_MAX_VIEWPORT_DIMS: case GL_MAX_VIEWPORT_DIMS:
case GL_PATCH_DEFAULT_INNER_LEVEL:
count = 2; count = 2;
break; break;
case GL_BLEND_COLOR: case GL_BLEND_COLOR:
@@ -1275,6 +1426,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VIEWPORT: case GL_VIEWPORT:
case GL_SCISSOR_BOX: case GL_SCISSOR_BOX:
case GL_COLOR_WRITEMASK: case GL_COLOR_WRITEMASK:
case GL_PATCH_DEFAULT_OUTER_LEVEL:
count = 4; count = 4;
break; break;
default: default:
@@ -1314,6 +1466,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0; *params = 0;
return; return;
} }
// GL_TEXTURE_BUFFER_BINDING and GL_TEXTURE_BUFFER are the same token (0x8C2A): as a
// glGetIntegerv pname it asks which BUFFER object is bound to the buffer-texture target,
// not which texture is (that one is GL_TEXTURE_BINDING_BUFFER, handled by the texture-unit
// decoder above).
case GL_TEXTURE_BUFFER_BINDING: {
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Texture).GetBoundObject();
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_BLEND: case GL_BLEND:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE; *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
return; return;
@@ -1369,6 +1530,16 @@ namespace MobileGL::MG_Impl::GLImpl {
// this single case serves every getter flavor. // this single case serves every getter flavor.
*params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor()); *params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
return; return;
// glClipControl's two state variables (GL 4.5 core table 23.7). They answer from the
// state the entry point records, which is what the conformance suite's initial-value and
// set-then-get cases read - the RASTERIZATION half of clip control is a separate,
// backend-side question and does not gate the query.
case GL_CLIP_ORIGIN:
*params = static_cast<GLint>(MG_State::pGLContext->GetClipOrigin());
return;
case GL_CLIP_DEPTH_MODE:
*params = static_cast<GLint>(MG_State::pGLContext->GetClipDepthMode());
return;
case GL_COLOR_CLEAR_VALUE: { case GL_COLOR_CLEAR_VALUE: {
const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor(); const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
params[0] = static_cast<GLint>(clearColor.x()); params[0] = static_cast<GLint>(clearColor.x());
@@ -1657,6 +1828,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS: case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
*params = kFrontendMaxGeometryUniformComponents; *params = kFrontendMaxGeometryUniformComponents;
return; return;
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
*params = kFrontendMaxGeometryShaderInvocations;
return;
case GL_MAX_IMAGE_SAMPLES: case GL_MAX_IMAGE_SAMPLES:
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend *params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
return; return;
@@ -1710,6 +1884,59 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = *params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks); StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
return; return;
// The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
// constant that BuildTBuiltInResource expands, and the two must report the same number
// (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
// the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
// They were the whole per-stage tess family: the table had been filled in only where the
// honest answer was zero (the atomic counters, the image uniforms) or where a driver
// query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
// answer is a real resource count fell through to GL_INVALID_ENUM.
case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
return;
case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
return;
case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
return;
case GL_MAX_TESS_PATCH_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
return;
// Routed through the same clamp as every other per-stage block count so the
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
// GL 4.6 table 23.64 cannot be broken by the two families moving independently.
case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
return;
case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
return;
case GL_MAX_SUBROUTINES:
*params = kFrontendMaxSubroutines;
return;
case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
*params = kFrontendMaxSubroutineUniformLocations;
return;
case GL_MAX_TEXTURE_LOD_BIAS: case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO *params = 15; // TODO
return; return;
@@ -1755,8 +1982,21 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_NUM_PROGRAM_BINARY_FORMATS: case GL_NUM_PROGRAM_BINARY_FORMATS:
*params = 0; *params = 0;
return; return;
// GL_ARB_spirv_extensions / GL 4.6 core 22.2. An implementation that advertises no
// SPIR-V extension answers zero here, and glGetStringi(GL_SPIR_V_EXTENSIONS, i) is then
// never legally called - MobileGL runs the module through its own translation pipeline
// and relies on no SPIR-V extension to do it, so zero is the true answer rather than a
// placeholder.
case GL_NUM_SPIR_V_EXTENSIONS:
*params = 0;
return;
// GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
// agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
case GL_NUM_SHADER_BINARY_FORMATS: case GL_NUM_SHADER_BINARY_FORMATS:
*params = 0; // ShaderBinary entrypoints are stubbed *params = 1;
return;
case GL_SHADER_BINARY_FORMATS:
*params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
return; return;
case GL_PACK_ALIGNMENT: case GL_PACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment); *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
@@ -1815,6 +2055,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PRIMITIVE_RESTART_INDEX: case GL_PRIMITIVE_RESTART_INDEX:
*params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex()); *params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
return; return;
case GL_POLYGON_OFFSET_CLAMP:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 4.6
// core 22.1's float-to-integer rule.
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPolygonOffsetClamp()));
return;
case GL_PROGRAM_BINARY_FORMATS: case GL_PROGRAM_BINARY_FORMATS:
*params = 0; // program-binary entrypoints are stubbed *params = 0; // program-binary entrypoints are stubbed
return; return;
@@ -1900,6 +2145,13 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SAMPLE_MASK: case GL_SAMPLE_MASK:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE; *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
return; return;
case GL_SAMPLE_SHADING:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE;
return;
case GL_MIN_SAMPLE_SHADING_VALUE:
// GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest.
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
return;
case GL_SAMPLE_MASK_VALUE: case GL_SAMPLE_MASK_VALUE:
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue()); *params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
return; return;
@@ -2118,7 +2370,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
case GL_MAX_ELEMENT_INDEX: case GL_MAX_ELEMENT_INDEX:
*params = 1024 * 1024; // TODO // 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
// state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
// uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
// indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
// that no draw path ever consulted.
*params = INT32_MAX;
return; return;
case GL_CONTEXT_PROFILE_MASK: case GL_CONTEXT_PROFILE_MASK:
// Reports the requested context profile (EGL defaults 3.x contexts to core); // Reports the requested context profile (EGL defaults 3.x contexts to core);
@@ -2174,8 +2431,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeTextureImageUnits; *params = dynamicParameters.MaxComputeTextureImageUnits;
break; break;
case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS: case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS:
// The CLAMPED block count, i.e. exactly what GL_MAX_COMPUTE_UNIFORM_BLOCKS answers.
// GL 4.6 table 23.64 defines this as the components reachable through the blocks a
// stage may declare, so deriving it from the raw backend number described 256 blocks
// an application is only ever allowed 84 of.
*params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents, *params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents,
dynamicParameters.MaxComputeUniformBlocks, ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
dynamicParameters.MaxUniformBlockSize); dynamicParameters.MaxUniformBlockSize);
break; break;
case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
@@ -2219,16 +2480,16 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex); *params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
break; break;
case GL_MAX_COLOR_TEXTURE_SAMPLES: case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples()); *params = GetAdvertisedColorTextureMaxSamples();
break; break;
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS: case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents, *params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
kFrontendMaxFragmentUniformBlocks, ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
dynamicParameters.MaxUniformBlockSize); dynamicParameters.MaxUniformBlockSize);
break; break;
case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS: case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents, *params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
kFrontendMaxGeometryUniformBlocks, ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
dynamicParameters.MaxUniformBlockSize); dynamicParameters.MaxUniformBlockSize);
break; break;
case GL_MAX_GEOMETRY_OUTPUT_VERTICES: case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
@@ -2242,14 +2503,14 @@ namespace MobileGL::MG_Impl::GLImpl {
break; break;
case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS: case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents, *params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
kFrontendMaxVertexUniformBlocks, ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
dynamicParameters.MaxUniformBlockSize); dynamicParameters.MaxUniformBlockSize);
break; break;
case GL_MAX_CUBE_MAP_TEXTURE_SIZE: case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
*params = dynamicParameters.MaxCubeMapTextureSize; *params = dynamicParameters.MaxCubeMapTextureSize;
break; break;
case GL_MAX_DEPTH_TEXTURE_SAMPLES: case GL_MAX_DEPTH_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples()); *params = GetAdvertisedDepthTextureMaxSamples();
break; break;
case GL_MAX_FRAMEBUFFER_WIDTH: case GL_MAX_FRAMEBUFFER_WIDTH:
*params = dynamicParameters.MaxFramebufferWidth; *params = dynamicParameters.MaxFramebufferWidth;
@@ -2276,7 +2537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeImageUniforms; *params = dynamicParameters.MaxComputeImageUniforms;
break; break;
case GL_MAX_INTEGER_SAMPLES: case GL_MAX_INTEGER_SAMPLES:
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples()); *params = GetAdvertisedIntegerMaxSamples();
break; break;
case GL_MAX_RENDERBUFFER_SIZE: case GL_MAX_RENDERBUFFER_SIZE:
*params = dynamicParameters.MaxRenderbufferSize; *params = dynamicParameters.MaxRenderbufferSize;
@@ -2287,12 +2548,56 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PATCH_VERTICES: case GL_PATCH_VERTICES:
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices()); *params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
break; break;
// Float state, so glGetIntegerv rounds it (GL 4.6 core 2.2.2) - the exact values come back
// through glGetFloatv. Answered here so glGetBooleanv, which delegates to this getter for
// everything its own switch does not handle, does not report INVALID_ENUM for them.
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
for (Uint i = 0; i < 4; ++i) params[i] = static_cast<GLint>(std::lround(outer[i]));
break;
}
case GL_PATCH_DEFAULT_INNER_LEVEL: {
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
break;
}
// GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
// GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
// the DirectVulkan draw path relies on this staying false (it resolves primitive restart
// to "never" for a PATCH_LIST topology on the strength of it).
case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
*params = GL_FALSE;
break;
case GL_MAX_PATCH_VERTICES: case GL_MAX_PATCH_VERTICES:
*params = dynamicParameters.MaxPatchVertices; *params = dynamicParameters.MaxPatchVertices;
break; break;
case GL_MAX_TESS_GEN_LEVEL: case GL_MAX_TESS_GEN_LEVEL:
*params = dynamicParameters.MaxTessGenLevel; *params = dynamicParameters.MaxTessGenLevel;
break; break;
// Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
// COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
// conformance suite's own formula exactly, so the two cannot drift.
case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks), dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks), dynamicParameters.MaxUniformBlockSize);
break;
// ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
// for a stronger reason: a cull distance discards the whole primitive, so advertising
// eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
// the honest answer on a host with no cull-distance route, and the conformance suite then
// skips the functional cases instead of failing them deep inside a pixel comparison.
case GL_MAX_CULL_DISTANCES:
*params = dynamicParameters.MaxCullDistances;
break;
case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
*params = dynamicParameters.MaxCombinedClipAndCullDistances;
break;
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET: case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
*params = dynamicParameters.MinProgramTextureGatherOffset; *params = dynamicParameters.MinProgramTextureGatherOffset;
break; break;
@@ -2343,7 +2648,25 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxTransformFeedbackSeparateAttribs; *params = kFrontendMaxTransformFeedbackSeparateAttribs;
break; break;
case GL_MAX_VERTEX_STREAMS: case GL_MAX_VERTEX_STREAMS:
*params = 1; // ONE, which is under the GL 4.5 core table 23.62 minimum of four and is a known,
// deliberate non-conformance. It was briefly raised to 4 on the theory that streams
// 1..3 could exist and be permanently empty; measuring that decision refuted it.
// Raising the limit un-gates two CTS cases per package across KHR-GL40..GL46 -
// transform_feedback.draw_xfb_stream_test (which stops being skipped) and
// transform_feedback3.multiple_streams (which stops reporting NotSupported) - and
// both then fail, because nothing in the shader pipeline supports layout(stream = N),
// EmitStreamVertex or EndStreamPrimitive, and because the query state machine tracks
// one active query per TARGET rather than per (target, stream). That is 14 new
// failures against 2 gained limits passes, and a 4 nothing can back is the
// advertised-caps lie with the sign flipped.
//
// The real fix is the feature, not the number: per-stream capture needs
// layout(stream = N) through the transpiler plus per-(target, stream) query slots,
// which DirectVulkan could back with VK_EXT_transform_feedback's geometryStreams and
// DirectGLES cannot back at all (ES has no vertex streams). Until that lands, one is
// the honest count and every stream-addressing entry point bounds itself by THIS
// query, so raising it later moves them all together.
*params = kFrontendMaxVertexStreams;
break; break;
case GL_TRANSFORM_FEEDBACK_ACTIVE: case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0; *params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
@@ -2360,15 +2683,36 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TEXTURE_SIZE: case GL_MAX_TEXTURE_SIZE:
*params = dynamicParameters.MaxTextureSize; *params = dynamicParameters.MaxTextureSize;
break; break;
case GL_MAX_UNIFORM_BUFFER_BINDINGS: case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
// Never advertise more bindings than the state layer's indexed-binding array can track // Never advertise more bindings than the state layer's indexed-binding array can track
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that // (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every // capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
// advertised index and expects no error. The floor equals the GL 3.3 core minimum // advertised index and expects no error. The floor is the GL 4.5 core minimum, and
// (36), so the clamp never under-advertises. // the array was widened to exactly it, so the two coincide by construction.
//
// WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
// family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
// not a driver binding point. Neither backend forwards it as one on the draw path:
// DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
// (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
// resolves each block to a descriptor. So what the host driver's count bounds is how
// many blocks ONE PROGRAM may use, not how many points an application may bind.
//
// That per-program number is NOT GL_MAX_COMBINED_UNIFORM_BLOCKS (84, the six-stage
// sum): no single program can reach it. A graphics program is bounded by the five
// graphics stages' per-stage counts, 14 each, so 70 blocks plus the global UBO at ES
// point 0 = 71 - inside the ES 3.2 minimum of 72. A compute program is bounded by
// GL_MAX_COMPUTE_UNIFORM_BLOCKS, which on DirectGLES is the ES driver's own count
// (GL-scale, ~14) and on DirectVulkan is served from descriptors with no ES binding
// points involved. Raising any per-stage graphics count past 14 is what would break
// this, so that is the edit to check against the ES ceiling - not this one.
static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
kFrontendMinUniformBufferBindings,
"the indexed-binding array must be able to hold every advertised uniform binding point");
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings, *params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount)); static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break; break;
}
case GL_MAX_UNIFORM_BLOCK_SIZE: case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize; *params = dynamicParameters.MaxUniformBlockSize;
break; break;
+19 -2
View File
@@ -25,7 +25,24 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum GetError(); GLenum GetError();
GLenum GetGraphicsResetStatus(); GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL // The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum. Frontend multisample validators have to honour this ceiling for every // core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
// format, otherwise MobileGL rejects a sample count it advertised itself. // ceilings below have a minimum of one and are reported as probed.
GLint GetAdvertisedMaxSamples(); GLint GetAdvertisedMaxSamples();
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
// the query promised - it used to floor both at 4 and then let the backend quietly
// under-allocate whatever the driver could not actually provide.
GLint GetAdvertisedColorTextureMaxSamples();
GLint GetAdvertisedDepthTextureMaxSamples();
GLint GetAdvertisedIntegerMaxSamples();
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
//
// Shared rather than duplicated because two callers need the identical number and disagreeing
// would be a silent bug: the query itself, and the draw path's write of the reserved
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
// is exactly what the sample_variables CTS does.
GLint ResolveDrawFramebufferSampleCount();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+351 -25
View File
@@ -11,6 +11,8 @@
#include "Config.h" #include "Config.h"
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <set>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h> #include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -30,10 +32,22 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckShaderNameValidity(Uint shader) { static bool CheckShaderNameValidity(Uint shader) {
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) { if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) {
// The mirror of CheckProgramNameValidity below, and for the same reason: programs and
// shaders are drawn from ONE name space (ProgramState hands both out of a single
// generator), so a name that exists but belongs to a PROGRAM is the wrong kind of
// object - GL 3.3 core 2.11.x makes that INVALID_OPERATION - while a name GL never
// handed out is INVALID_VALUE. This half of the split was missing, so every shader
// entry point handed a program name reported INVALID_VALUE; the conformance suite
// reads exactly that code back from glSpecializeShader.
const ErrorCode error = (shader != 0 && MG_State::pGLContext->ValidateProgramName(shader))
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(shader) + " is not a valid name.")); std::to_string(shader) +
(error == ErrorCode::InvalidOperation ? " is not a shader object."
: " is not a valid name.")));
return false; return false;
} }
return true; return true;
@@ -245,6 +259,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
// GL 4.6 core 7.6.3: INVALID_VALUE when uniformBlockBinding >= MAX_UNIFORM_BUFFER_BINDINGS.
// The storage-block twin below has always had this check; the uniform one never did, and the
// value it stores is used as a RAW SUBSCRIPT into the state layer's fixed indexed-binding
// array on every draw and dispatch (DirectGLES's per-program UBO rebind, DirectVulkan's
// descriptor resolve, whose only guard is a MOBILEGL_ASSERT that compiles away in release).
// An out-of-range binding therefore did not merely go unreported - it read past the array and
// dereferenced whatever SharedPtr it found there.
bool ValidateUniformBlockBinding(GLuint binding) {
// Exactly what glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS) advertises: the state
// layer's array width, which the getter clamps to as well.
const SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform);
if (binding < maxBindingCount) {
return true;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("Uniform block binding {} is not less than GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).", binding,
maxBindingCount)));
return false;
}
bool ValidateShaderStorageBlockBinding(GLuint binding) { bool ValidateShaderStorageBlockBinding(GLuint binding) {
SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage); SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
if (MG_Backend::pActiveBackendObject) { if (MG_Backend::pActiveBackendObject) {
@@ -307,9 +345,195 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompileShader_State(GLuint shader) { void CompileShader_State(GLuint shader) {
auto& shaderObject = TryToGetShaderObject(shader); auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return; if (!shaderObject) return;
// ARB_gl_spirv: "INVALID_OPERATION is generated by CompileShader if shader has been
// associated with a SPIR-V binary". Such an object has no GLSL source to compile - it is
// waiting for glSpecializeShader, which is the operation that compiles it.
if (shaderObject->HasSpirvBinary()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" holds a SPIR-V binary; use glSpecializeShader instead of glCompileShader."));
return;
}
shaderObject->Compile(); shaderObject->Compile();
} }
// ---------------------------------------------------------------------------------------
// GL_ARB_gl_spirv
// ---------------------------------------------------------------------------------------
void ShaderBinary_State(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary,
GLsizei length) {
if (count < 0 || length < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count and length must be non-negative."));
return;
}
// GL_NUM_SHADER_BINARY_FORMATS advertises exactly one format, so every other value is
// INVALID_ENUM (GL 4.6 core 7.2). This is the check that used to be missing entirely -
// the entry point was a silent stub, so an application handed a format nothing supports
// and was told nothing.
if (binaryformat != GL_SHADER_BINARY_FORMAT_SPIR_V) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"binaryformat must be GL_SHADER_BINARY_FORMAT_SPIR_V."));
return;
}
if (count == 0) return;
if (shaders == nullptr || (length > 0 && binary == nullptr)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "shaders and binary must not be null."));
return;
}
// A SPIR-V module is a sequence of 32-bit words, so a length that is not a multiple of
// four cannot be one (ARB_gl_spirv makes this INVALID_VALUE).
if ((length % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"length must be a multiple of four for a SPIR-V module."));
return;
}
// EVERY name is validated before ANY of them is written: the entry point is all-or-
// nothing, and half-applying it would leave some objects holding a module the call was
// rejected for. The duplicate check is the extension's own ("INVALID_VALUE ... if the
// same shader object is specified more than once").
std::set<GLuint> seen;
for (GLsizei i = 0; i < count; ++i) {
if (!seen.insert(shaders[i]).second) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shaders[i]) +
" appears more than once in `shaders`."));
return;
}
if (!MG_State::pGLContext->ValidateShaderName(shaders[i])) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(shaders[i]) + " is not the name of a shader object."));
return;
}
}
const SizeT wordCount = static_cast<SizeT>(length) / 4;
Vector<Uint32> module(wordCount);
if (wordCount != 0) {
Memcpy(module.data(), binary, static_cast<SizeT>(length));
}
// spirv-val here, not at glSpecializeShader: this is where the words arrive, and past it
// they reach SPIRV-Cross, which parses rather than validates. ARB_gl_spirv lets an
// implementation reject an invalid module at either call; rejecting at the earlier one
// means the application's error is reported next to the data that caused it.
if (const auto validated = MG_Util::ShaderTranspiler::ShaderCompiler::ValidateSpirvModule(module);
!validated) {
MGLOG_D("%s: rejected SPIR-V module: %s", __func__, validated.error().log.c_str());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, validated.error().log));
return;
}
for (GLsizei i = 0; i < count; ++i) {
auto& shaderObject = TryToGetShaderObject(shaders[i]);
if (!shaderObject) continue;
// A copy per object, not a shared buffer: each shader object may be specialized with
// different constants, and each specialization re-reads its own original words.
Vector<Uint32> perObject = module;
shaderObject->SetSpirvBinary(Move(perObject));
}
}
void SpecializeShader_State(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return;
if (!shaderObject->HasSpirvBinary()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" has no SPIR-V binary; call glShaderBinary first."));
return;
}
// ARB_gl_spirv: a shader that has already been specialized may not be specialized again
// until glShaderBinary re-associates a module with it.
if (shaderObject->HasBeenSpecialized()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" has already been specialized; re-associate its module with "
"glShaderBinary before specializing it again."));
return;
}
// pEntryPoint names the entry point to specialize; there is no default. A null pointer
// cannot name one, and neither can the empty string.
if (pEntryPoint == nullptr || *pEntryPoint == '\0') {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pEntryPoint must name an entry point."));
return;
}
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pConstantIndex and pConstantValue must not be null."));
return;
}
// "INVALID_VALUE is generated if any value in pConstantIndex is repeated" - checked before
// anything is applied, for the same all-or-nothing reason glShaderBinary checks its names
// up front.
Vector<Uint32> constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants);
Vector<Uint32> constantValues(pConstantValue, pConstantValue + numSpecializationConstants);
{
std::set<Uint32> seen;
for (const Uint32 id : constantIds) {
if (seen.insert(id).second) continue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"constant index " + std::to_string(id) + " is repeated."));
return;
}
}
const String entryPoint(pEntryPoint);
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
using SpecializationFailure = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializationFailure;
SpecializationFailure failure = SpecializationFailure::None;
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
if (!specialized) {
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
specialized.error().log.c_str());
// The two conditions ARB_gl_spirv ENUMERATES are GL errors, and an erroring GL command
// must have no other effect - so the shader object is left exactly as it was rather
// than being pushed into a failed-compile state. Anything else is a genuine compile
// failure of a well-formed request, which the extension routes through COMPILE_STATUS
// and the info log exactly as glCompileShader does.
if (failure == SpecializationFailure::UnknownEntryPoint ||
failure == SpecializationFailure::UnknownConstantId) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, specialized.error().log));
return;
}
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
return;
}
shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
specialized.value().xfbBufferMode);
}
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation, // glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the // because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings. // same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
@@ -744,12 +968,77 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE; *params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
break; break;
case GL_PROGRAM_SEPARABLE: case GL_PROGRAM_SEPARABLE:
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE; // The LATCHED flag, not the live one: glProgramParameteri's write takes effect at the
// next link (GL 4.6 core 7.3), so a program told to be separable and then never
// linked still reports GL_FALSE.
*params = programObject->GetLinkedSeparable() ? GL_TRUE : GL_FALSE;
break; break;
// The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as
// GL_COMPUTE_WORK_GROUP_SIZE above, and for the same reason: "a linked program object
// with a geometry shader" is one whose EXECUTABLE has the stage, so an
// attached-but-not-yet-linked shader must give INVALID_OPERATION rather than the previous
// link's value. The geometry three used to be listed here only to fall through into the
// INVALID_ENUM default, and the tessellation five were not listed at all.
case GL_GEOMETRY_VERTICES_OUT: case GL_GEOMETRY_VERTICES_OUT:
case GL_GEOMETRY_INPUT_TYPE: case GL_GEOMETRY_INPUT_TYPE:
case GL_GEOMETRY_OUTPUT_TYPE: case GL_GEOMETRY_OUTPUT_TYPE:
case GL_GEOMETRY_SHADER_INVOCATIONS: {
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Geometry)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) +
" is not a linked program object with a geometry shader."));
return;
}
switch (pname) {
case GL_GEOMETRY_VERTICES_OUT: *params = programObject->GetGeometryVerticesOut(); break;
case GL_GEOMETRY_INPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryInputType()); break;
case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryOutputType()); break;
default: *params = programObject->GetGeometryShaderInvocations(); break;
}
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
}
case GL_TESS_CONTROL_OUTPUT_VERTICES: {
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessControl)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::to_string(program) +
" is not a linked program object with a tessellation control shader."));
return;
}
*params = programObject->GetTessControlOutputVertices();
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
}
case GL_TESS_GEN_MODE:
case GL_TESS_GEN_SPACING:
case GL_TESS_GEN_VERTEX_ORDER:
case GL_TESS_GEN_POINT_MODE: {
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessEval)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::to_string(program) +
" is not a linked program object with a tessellation evaluation shader."));
return;
}
switch (pname) {
case GL_TESS_GEN_MODE: *params = static_cast<GLint>(programObject->GetTessGenMode()); break;
case GL_TESS_GEN_SPACING: *params = static_cast<GLint>(programObject->GetTessGenSpacing()); break;
case GL_TESS_GEN_VERTEX_ORDER:
*params = static_cast<GLint>(programObject->GetTessGenVertexOrder());
break;
default: *params = programObject->GetTessGenPointMode() ? GL_TRUE : GL_FALSE; break;
}
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
}
default: default:
MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str()); MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -811,8 +1100,19 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1; *params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
break; break;
case GL_SHADER_SOURCE_LENGTH: case GL_SHADER_SOURCE_LENGTH: {
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1; // The APPLICATION's source, which is empty for a shader that came from glShaderBinary -
// see ShaderObject::GetApplicationShaderSource.
const auto& source = shaderObject->GetApplicationShaderSource();
*params = source.empty() ? 0 : (GLint)source.length() + 1;
break;
}
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
// while the object stands for an application-supplied module. It is the FIRST thing the
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
// default arm below and take the whole test with it.
case GL_SPIR_V_BINARY:
*params = shaderObject->HasSpirvBinary() ? GL_TRUE : GL_FALSE;
break; break;
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in // GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has // GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
@@ -858,13 +1158,23 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& shaderObject = TryToGetShaderObject(shader); auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return; if (!shaderObject) return;
auto& src = shaderObject->GetShaderSource(); auto& src = shaderObject->GetApplicationShaderSource();
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length()); CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
} }
GLint GetUniformLocation_State(GLuint program, const GLchar* name) { GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return -1; if (!programObject) return -1;
// GL 4.6 core 7.6: "INVALID_OPERATION is generated if program has not been successfully
// linked". Answering -1 silently is not the same thing - the conformance suite reads the
// error, not the location.
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return -1;
}
auto loc = programObject->GetUniformLocation(name); auto loc = programObject->GetUniformLocation(name);
MGLOG_D("%s: loc %02d = %s", __func__, loc, name); MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
return loc; return loc;
@@ -1277,11 +1587,13 @@ namespace MobileGL::MG_Impl::GLImpl {
template <GLsizei ItemCount, typename T> template <GLsizei ItemCount, typename T>
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) { void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
// The link check comes BEFORE the location == -1 early-out, not after. GL 4.6 core 7.6
// makes an unlinked program INVALID_OPERATION regardless of the location, and -1 is
// exactly the location an application holds after glGetUniformLocation on such a program -
// so checking -1 first swallowed the very case the rule exists for.
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
@@ -1289,6 +1601,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
// "If location is equal to -1, the data passed in will be silently ignored and the
// specified uniform variable will not be changed" - after the program itself has been
// found acceptable.
if (location == -1) return;
for (GLint offset = 0; offset < count; offset++) { for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) { if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
@@ -1699,8 +2015,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -1712,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2, UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
"program " + std::to_string(program)); "program " + std::to_string(program));
} }
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -1731,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored. // Values for elements beyond the end of the uniform array are ignored.
@@ -1756,8 +2072,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -1769,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored. // Values for elements beyond the end of the uniform array are ignored.
@@ -1790,8 +2106,6 @@ 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) { GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -1803,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows, UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
"program " + std::to_string(program)); "program " + std::to_string(program));
} }
@@ -1836,6 +2152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Program object" + std::to_string(program) + " that has been linked.")); "Program object" + std::to_string(program) + " that has been linked."));
return; return;
} }
if (!ValidateUniformBlockBinding(uniformBlockBinding)) return;
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) { if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
@@ -2083,6 +2400,15 @@ namespace MobileGL::MG_Impl::GLImpl {
BindAttribLocation_State(program, index, name); BindAttribLocation_State(program, index, name);
} }
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) {
ShaderBinary_State(count, shaders, binaryformat, binary, length);
}
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
SpecializeShader_State(shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue);
}
void CompileShader(GLuint shader) { void CompileShader(GLuint shader) {
CompileShader_State(shader); CompileShader_State(shader);
} }
@@ -2342,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2352,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
} }
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2368,7 +2694,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2378,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
} }
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2394,7 +2720,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2404,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
} }
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2420,7 +2746,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2430,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
} }
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2446,7 +2772,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2456,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
} }
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2472,7 +2798,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2482,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
} }
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2498,7 +2824,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2508,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
} }
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2524,7 +2850,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2534,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
} }
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2550,7 +2876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2560,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
} }
void GetUniformdv(GLuint program, GLint location, GLdouble* params) { void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader); void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name); void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader); void CompileShader(GLuint shader);
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
// the module to one or more shader objects, glSpecializeShader names its entry point and
// supplies its specialization constants and is what actually compiles them.
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue);
GLuint CreateProgram(void); GLuint CreateProgram(void);
GLuint CreateShader(GLenum type); GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program); void DeleteProgram(GLuint program);
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Program {} has not been linked successfully.", program)); std::format("Program {} has not been linked successfully.", program));
return; return;
} }
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
// whose live flag was cleared after a separable link is still a legal stage, and a
// program whose live flag was set after a non-separable link is not.
if (!programObject->GetLinkedSeparable()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} was not linked as a separable program.", program));
return;
}
} }
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages; const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
+98 -17
View File
@@ -59,6 +59,37 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_activePrimitivesGeneratedQueryId = 0; GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none). // Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0; GLuint g_activeSamplesPassedQueryId = 0;
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
// target (0 = none). A map rather than a field per target: the eleven behave identically
// and none of them has any state beyond "which object is counting".
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core
// since 4.6 and there is no query by which an application could learn otherwise before
// calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1
// provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the
// spec's own signal that the counter is unsupported and its results indeterminate. That
// is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a
// core entry point, is both non-conformant AND less informative.
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) {
case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_SHADER_INVOCATIONS:
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
case GL_FRAGMENT_SHADER_INVOCATIONS:
case GL_COMPUTE_SHADER_INVOCATIONS:
case GL_CLIPPING_INPUT_PRIMITIVES:
case GL_CLIPPING_OUTPUT_PRIMITIVES:
return true;
default:
return false;
}
}
Bool TimerQueryDisabled() { Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery; return MG_Config::Features.DisableTimerQuery;
@@ -370,6 +401,9 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
queryObject->active = false; queryObject->active = false;
g_activeSamplesPassedQueryId = 0; g_activeSamplesPassedQueryId = 0;
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
queryObject->active = false;
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || } else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) { queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false; queryObject->active = false;
@@ -410,7 +444,9 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets // GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support. // need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
@@ -426,10 +462,12 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist."); RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return; return;
} }
GLuint& activeQueryId = isTransformFeedbackQuery GLuint& activeQueryId = isPipelineStatisticsQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId ? g_activePipelineStatisticsQueryIds[target]
: g_activePrimitivesGeneratedQueryId) : (isTransformFeedbackQuery
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId != 0) { if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on this target."); "A query is already active on this target.");
@@ -448,7 +486,11 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target; queryObject->target = target;
queryObject->active = true; queryObject->active = true;
if (isTransformFeedbackQuery) { if (isPipelineStatisticsQuery) {
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
// The object still becomes a real, target-latched query so every other rule about it
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
} else if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders); // Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them. // the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery; const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
@@ -476,15 +518,19 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return; return;
} }
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex); const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
GLuint& activeQueryId = isTransformFeedbackQuery GLuint& activeQueryId = isPipelineStatisticsQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId ? g_activePipelineStatisticsQueryIds[target]
: g_activePrimitivesGeneratedQueryId) : (isTransformFeedbackQuery
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId == 0) { if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target."); RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return; return;
@@ -494,6 +540,17 @@ namespace MobileGL::MG_Impl::GLImpl {
activeQueryId = 0; // should not happen; keep state consistent activeQueryId = 0; // should not happen; keep state consistent
return; return;
} }
if (isPipelineStatisticsQuery) {
// The result is a definite zero rather than an unread backend handle, so a later
// GetQueryObject* answers immediately and never waits on something that was never
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
queryObject->cachedResult = 0;
queryObject->resultCached = true;
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isTransformFeedbackQuery) { if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
@@ -657,7 +714,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId); *params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break; break;
default: default:
*params = 0; if (IsPipelineStatisticsQueryTarget(target)) {
const auto it = g_activePipelineStatisticsQueryIds.find(target);
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
} else {
*params = 0;
}
break; break;
} }
return; return;
@@ -668,6 +730,14 @@ namespace MobileGL::MG_Impl::GLImpl {
// entry points / timestamp valid bits at call time, not at table // entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always // init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. // wins.
if (IsPipelineStatisticsQueryTarget(target)) {
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
// results are indeterminate. The conformance suite reads exactly this and skips
// the functional half of each such target, which is the outcome an uninstrumented
// counter should produce.
*params = 0;
return;
}
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
@@ -741,14 +811,24 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
namespace { namespace {
Bool IsPerVertexStreamQueryTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
}
// The indexed query entry points differ from the plain ones only in the vertex // The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS // stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. With a // for the two transform feedback targets and zero for every other target. MobileGL
// single vertex stream both bounds are 1, so a valid call is always index 0 and // implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
// forwards to the unindexed implementation. // which is what makes the three forwards below equivalent to the unindexed entry points.
//
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
// per-TARGET globals and would end - or report - a query begun on a different stream.
// Raising that limit therefore means giving each active query a stream index and
// comparing it here, not just changing the number.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) { Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget = const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
GLint maxVertexStreams = 1; GLint maxVertexStreams = 1;
if (perStreamTarget) { if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
@@ -761,6 +841,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: "index must be zero for this query target."); : "index must be zero for this query target.");
return false; return false;
} }
} // namespace } // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) { void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
@@ -328,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE); MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
} }
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
void MinSampleShading_State(GLfloat value) {
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
}
void PolygonOffset_State(GLfloat factor, GLfloat units) { void PolygonOffset_State(GLfloat factor, GLfloat units) {
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units)); MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
} }
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
static_cast<Float>(clamp));
}
void ClipControl_State(GLenum origin, GLenum depth) {
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
// GL_INVALID_ENUM with the state left untouched.
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
MG_Util::ConvertGLEnumToString(origin) + "."));
return;
}
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
MG_Util::ConvertGLEnumToString(depth) + "."));
return;
}
MG_State::pGLContext->SetClipControl(origin, depth);
}
void PolygonMode_State(GLenum face, GLenum mode) { void PolygonMode_State(GLenum face, GLenum mode) {
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal // GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers // face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
@@ -1013,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
SampleCoverage_State(value, invert); SampleCoverage_State(value, invert);
} }
void MinSampleShading(GLfloat value) {
MinSampleShading_State(value);
}
void PolygonOffset(GLfloat factor, GLfloat units) { void PolygonOffset(GLfloat factor, GLfloat units) {
PolygonOffset_State(factor, units); PolygonOffset_State(factor, units);
} }
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
PolygonOffsetClamp_State(factor, units, clamp);
}
void ClipControl(GLenum origin, GLenum depth) {
ClipControl_State(origin, depth);
}
void PolygonMode(GLenum face, GLenum mode) { void PolygonMode(GLenum face, GLenum mode) {
PolygonMode_State(face, mode); PolygonMode_State(face, mode);
} }
@@ -38,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void StencilFunc(GLenum func, GLint ref, GLuint mask); void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height); void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert); void SampleCoverage(GLfloat value, GLboolean invert);
void MinSampleShading(GLfloat value);
void PolygonOffset(GLfloat factor, GLfloat units); void PolygonOffset(GLfloat factor, GLfloat units);
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
void ClipControl(GLenum origin, GLenum depth);
void PolygonMode(GLenum face, GLenum mode); void PolygonMode(GLenum face, GLenum mode);
void PointSize(GLfloat size); void PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param); void PointParameterf(GLenum pname, GLfloat param);
+131 -39
View File
@@ -13,6 +13,7 @@
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Math/FixedPointConversion.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
return static_cast<Float>(*(const GLint*)param); return static_cast<Float>(*(const GLint*)param);
} }
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
// reads exactly one component and does not care.
Bool IsVectorOnlySamplerPname(GLenum pname) {
return pname == GL_TEXTURE_BORDER_COLOR;
}
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
// sampler path funnels all three spellings through one void* function, which is precisely how
// it came to write a fixed type regardless of the caller.
//
// Truncation rather than rounding for the float -> integer direction, matching the texture
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
// sampler queried the same way must answer the same number.
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
if (isFloat) {
*(GLfloat*)params = value;
return;
}
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
// conversion is the well-defined modular one, and it is what the texture-side
// GetTexParameterIuiv fallback does.
const GLint asInt = static_cast<GLint>(value);
if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(asInt);
} else {
*(GLint*)params = asInt;
}
}
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
if (isFloat) {
*(GLfloat*)params = static_cast<GLfloat>(value);
} else if (isUnsignedInteger) {
*(GLuint*)params = value;
} else {
*(GLint*)params = static_cast<GLint>(value);
}
}
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) { Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (param == nullptr) return false; if (param == nullptr) return false;
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} // namespace } // namespace
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
bool isUnsignedInteger) { bool isUnsignedInteger, bool isIntegerCommand) {
if (param == nullptr) return; if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isFloat) { if (isFloat) {
const auto* values = (const GLfloat*)param; const auto* values = (const GLfloat*)param;
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3])); samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
} else if (!isIntegerCommand) {
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
const auto* values = (const GLint*)param;
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
} else if (isUnsignedInteger) { } else if (isUnsignedInteger) {
const auto* values = (const GLuint*)param; const auto* values = (const GLuint*)param;
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3])); samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
@@ -128,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
bool isUnsignedInteger) { bool isUnsignedInteger, bool isIntegerCommand) {
if (params == nullptr) return; if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!SamplerImpl::ValidateSamplerObject(sampler)) return; if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
using namespace MG_Util; using namespace MG_Util;
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
// decides the destination type. Writing a fixed type regardless - which is what these case
// labels used to do - hands back the other type's bit pattern rather than a converted value:
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
// how the same bug class was already found and fixed once for a single pname.
switch (pname) { switch (pname) {
case GL_TEXTURE_WRAP_S: case GL_TEXTURE_WRAP_S:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
break; break;
case GL_TEXTURE_WRAP_T: case GL_TEXTURE_WRAP_T:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
break; break;
case GL_TEXTURE_WRAP_R: case GL_TEXTURE_WRAP_R:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
break; break;
case GL_TEXTURE_MIN_FILTER: case GL_TEXTURE_MIN_FILTER:
*(GLuint*)params = StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode()); MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
samplerObj->GetMipmapMode()));
break; break;
case GL_TEXTURE_MAG_FILTER: case GL_TEXTURE_MAG_FILTER:
*(GLuint*)params = StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None); MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
SamplerMipmapMode::None));
break; break;
case GL_TEXTURE_MIN_LOD: case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
break; break;
case GL_TEXTURE_MAX_LOD: case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
break; break;
case GL_TEXTURE_LOD_BIAS: case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
break; break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT: case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (isFloat) { StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
} else if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
} else {
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
break; break;
case GL_TEXTURE_COMPARE_MODE: case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
break; break;
case GL_TEXTURE_COMPARE_FUNC: case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
break; break;
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
if (isFloat) { if (isFloat) {
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
out[1] = color.y(); out[1] = color.y();
out[2] = color.z(); out[2] = color.z();
out[3] = color.w(); out[3] = color.w();
} else if (!isIntegerCommand) {
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
const auto& color = samplerObj->GetBorderColor();
auto* out = (GLint*)params;
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
} else if (isUnsignedInteger) { } else if (isUnsignedInteger) {
const auto& color = samplerObj->GetBorderColorUI(); const auto& color = samplerObj->GetBorderColorUI();
auto* out = (GLuint*)params; auto* out = (GLuint*)params;
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) { if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr); textureUnit.SetSamplerObject(nullptr);
} else { } else {
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already // GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same // deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
// name, which is why this cannot go through the shared SamplerImpl validator. // name, so the shared validator answers for all of them.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { if (!SamplerImpl::ValidateSamplerName(sampler)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler); Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) { if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler); MG_State::pGLContext->CreateSamplerObject(sampler);
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) { void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false); GetSamplerParam_State(sampler, pname, params, false, false, false);
} }
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) { void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
SetSamplerParam_State(sampler, pname, param, false, true); SetSamplerParam_State(sampler, pname, param, false, true, true);
} }
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) { void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false); SetSamplerParam_State(sampler, pname, param, false, false, true);
} }
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) { void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false); SetSamplerParam_State(sampler, pname, param, false, false, false);
} }
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) { void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
SetSamplerParam_State(sampler, pname, param, true, false); SetSamplerParam_State(sampler, pname, param, true, false, false);
} }
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
// past the caller's single stack scalar - taking the address of a by-value argument and handing
// it to a four-component reader is what these used to do. The texture-side twins already answer
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) { void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameteriv(sampler, pname, &param); SamplerParameteriv(sampler, pname, &param);
} }
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) { void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameterfv(sampler, pname, &param); SamplerParameterfv(sampler, pname, &param);
} }
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) { void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
GetSamplerParam_State(sampler, pname, params, false, true); GetSamplerParam_State(sampler, pname, params, false, true, true);
} }
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) { void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false); GetSamplerParam_State(sampler, pname, params, false, false, true);
} }
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) { void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
GetSamplerParam_State(sampler, pname, params, true, false); GetSamplerParam_State(sampler, pname, params, true, false, false);
} }
void GenSamplers(GLsizei count, GLuint* samplers) { void GenSamplers(GLsizei count, GLuint* samplers) {
@@ -12,11 +12,17 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
// sampler object previously returned from a call to GenSamplers." That class is shared by every
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
Bool ValidateSamplerName(GLuint sampler) { Bool ValidateSamplerName(GLuint sampler) {
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName", ErrorCode::InvalidOperation,
std::format("Invalid sampler name {}", sampler))); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
std::format("Invalid sampler name {}", sampler)));
return false; return false;
} }
return true; return true;
+530 -93
View File
@@ -27,6 +27,8 @@
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h> #include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Impl/GLImpl/Framebuffer/Validators.h> #include <MG_Impl/GLImpl/Framebuffer/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Sampler/Validators.h>
#include <MG_Util/Math/FixedPointConversion.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h> #include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
@@ -41,13 +43,15 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetBorderColor(FloatVec4(params[0], params[1], params[2], params[3])); textureObject->SetBorderColor(FloatVec4(params[0], params[1], params[2], params[3]));
} }
// glTexParameteriv(GL_TEXTURE_BORDER_COLOR): GL 4.6 core 8.10 sends the components through
// equation 2.2 into the floating-point border colour. glGetTexParameteriv reverses it with
// equation 2.3; the two live in one header so they cannot drift apart.
void SetTextureBorderColorFromInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, void SetTextureBorderColorFromInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const GLint* params) { const GLint* params) {
constexpr Float kSignedIntToFloat = 1.0f / 2147483647.0f; textureObject->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(params[0]),
textureObject->SetBorderColor(FloatVec4(static_cast<Float>(params[0]) * kSignedIntToFloat, MG_Util::SignedNormalizedInt32ToFloat(params[1]),
static_cast<Float>(params[1]) * kSignedIntToFloat, MG_Util::SignedNormalizedInt32ToFloat(params[2]),
static_cast<Float>(params[2]) * kSignedIntToFloat, MG_Util::SignedNormalizedInt32ToFloat(params[3])));
static_cast<Float>(params[3]) * kSignedIntToFloat));
} }
void SetTextureBorderColorFromIntegerInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, void SetTextureBorderColorFromIntegerInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
@@ -339,6 +343,11 @@ namespace MobileGL::MG_Impl::GLImpl {
return GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false); return GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false);
case GL_TEXTURE_DEPTH_TYPE: case GL_TEXTURE_DEPTH_TYPE:
return GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false); return GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false);
case GL_TEXTURE_SHARED_SIZE:
// GL 4.6 core table 8.24: the size in bits of the SHARED EXPONENT, which only the
// one shared-exponent format has. Everything else answers zero, and the
// conformance suite compares "at least", not "equal".
return textureInternalFormat == TextureInternalFormat::RGB9E5 ? 5 : 0;
default: default:
MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname); MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname);
return 0; return 0;
@@ -398,10 +407,46 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
} }
// Array targets store their layer count in z; layers never participate in mip // How many components of a GL-space texel size actually halve down the mip chain.
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level. //
// An array texture's LAYER COUNT is not a dimension of the image (GL 4.6 core 8.14.3): it
// stays put all the way down, and it is stored in whichever component sits after the
// image's own dimensions - z for a 2D array or a cube array, and HEIGHT for a 1D array,
// whose level is recorded as {width, layers, 1}.
//
// THE one statement of that rule on the frontend side, because three readers have to agree
// on it or a chain is allocated under one and judged under another: this allocator,
// ComputeMipmapCompleteForFilter (MG_State/GLState/TextureState/TextureObject.cpp, which
// uses the identical 1/2/3 split) and DirectVulkan's MipShrinkingComponentCount. It used to
// be a two-way `depthMips` flag, which had no way to say "height is not a dimension" - so
// glGenerateMipmap on a GL_TEXTURE_1D_ARRAY allocated a chain whose LAYER COUNT halved,
// and the completeness rule then rejected the texture the generate was supposed to make
// complete. The backend allocator could not repair it either: it only ever GROWS a chain,
// and the frontend's (wrong) count is always the longer of the two.
Int MipShrinkingAxisCount(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
// {width, 1, 1} - the other two are already 1, but say so rather than rely on it.
return 1;
case TextureTarget::Texture1DArray:
// {width, layers, 1}: height IS the layer count.
return 1;
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
// {width, height, layers}: depth IS the layer count.
return 2;
case TextureTarget::Texture3D:
return 3;
default:
// 2D, cube faces, rectangle, multisample: a plain two-dimensional image.
return 2;
}
}
// Only true 3D textures halve their depth per level; every array target keeps its layer
// count. Expressed through the rule above so the two cannot drift.
Bool DepthParticipatesInMipmapping(TextureTarget target) { Bool DepthParticipatesInMipmapping(TextureTarget target) {
return target == TextureTarget::Texture3D; return MipShrinkingAxisCount(target) == 3;
} }
// Which targets each glTextureStorage*D accepts (GL 4.6 core 8.19). A texture whose target // Which targets each glTextureStorage*D accepts (GL 4.6 core 8.19). A texture whose target
@@ -422,20 +467,20 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// The longest mip chain the level-0 size admits. A 1D array keeps its layer count in // The longest mip chain the level-0 size admits, over the axes that actually reduce.
// height, so unlike a 2D texture its height takes no part in the reduction. Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips);
Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) { Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
const Int mipHeight = (target == TextureTarget::Texture1DArray) ? 1 : std::max<Int>(height, 1); return ComputeFullMipmapLevelCount(
return ComputeFullMipmapLevelCount({std::max<Int>(width, 1), mipHeight, std::max<Int>(depth, 1)}, {std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
DepthParticipatesInMipmapping(target)); MipShrinkingAxisCount(target));
} }
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) { Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
Int maxDimension = std::max<Int>( Int maxDimension = 1;
baseTexelSize.x(), for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1)); maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
}
Uint mipLevelCount = 1; Uint mipLevelCount = 1;
while (maxDimension > 1) { while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1); maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -444,13 +489,13 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount; return mipLevelCount;
} }
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
return { IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1), std::max<Int>(baseTexelSize.z(), 1)};
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1), for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1) size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
: std::max<Int>(baseTexelSize.z(), 1), }
}; return size;
} }
Bool EnsureGeneratedMipmapStorageAllocated( Bool EnsureGeneratedMipmapStorageAllocated(
@@ -472,10 +517,10 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const SizeT bytesPerTexel = baseByteSize / baseTexelCount; const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips); const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
for (Uint level = 1; level < requiredLevelCount; ++level) { for (Uint level = 1; level < requiredLevelCount; ++level) {
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips); const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, shrinkingAxes);
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) * const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) * static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z()); static_cast<SizeT>(levelTexelSize.z());
@@ -522,44 +567,55 @@ namespace MobileGL::MG_Impl::GLImpl {
return sampleCounts.empty() ? 0 : sampleCounts.front(); return sampleCounts.empty() ? 0 : sampleCounts.front();
} }
// The ceiling the frontend enforces, which must never be lower than the one MobileGL // The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that // this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or
// exact count to glTexImage*Multisample for every format. Answering 4 there and then // GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a // reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand // that an application reads GL_MAX_SAMPLES once and hands that count to every
// the driver; the shadow state keeps reporting what the application asked for. // glTexStorage*Multisample. That reasoning had it backwards: on Adreno and on Mali an
// integer multisample texture is backed by ONE sample, so accepting four here did not
// make four samples exist - ClampSamplesToBackendSupport quietly allocated one and the
// application wrote per-sample data it could never read back. Raising INVALID_OPERATION
// is what a real driver does, and it is what makes that silent squeeze unreachable for
// application-visible storage.
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget, Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
TextureInternalFormat textureInternalFormat) { TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
} }
const Int advertisedMaxSamples = GetAdvertisedMaxSamples(); const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format Bool isIntegerFormat = false;
// maximum - validating against anything else is how the two answers drifted apart. if (!isDepthOrStencil) {
GLenum normalizedInternalFormat =
MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
&normalizedFormat, &normalizedType);
isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
}
const Int categoryMaxSamples = isDepthOrStencil ? GetAdvertisedDepthTextureMaxSamples()
: isIntegerFormat ? GetAdvertisedIntegerMaxSamples()
: GetAdvertisedColorTextureMaxSamples();
// glGetInternalformativ(GL_SAMPLES) is answered from this very list
// (GetInternalformativ below), and GL 4.6 core 8.8 makes that query the definition of
// the per-format maximum - so when the probe has an answer it IS the ceiling, and the
// category limit only stands in where nothing was probed.
//
// This used to be max(probed, category), which made the probe dead: the walk starts
// AT the category limit (BackendObject_DirectGLES's ProbeTextureSampleCounts) so its
// head can never exceed it, and max() therefore always collapsed to the category
// value. A format whose 4- and 2-sample probes fail inside a 4-sample category - a
// float colour format under EXT_color_buffer_float is the natural instance - was
// still accepted at 4, silently squeezed to 1 by ClampSamplesToBackendSupport, and
// then reported as 4 by GL_TEXTURE_SAMPLES while glGetInternalformativ said 1.
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat); const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
if (probedMaxSamples > 0) { return probedMaxSamples > 0 ? probedMaxSamples : categoryMaxSamples;
return std::max(probedMaxSamples, advertisedMaxSamples);
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
}
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
&normalizedFormat, &normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
: dynamicParameters.MaxColorTextureSamples,
advertisedMaxSamples);
} }
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width, Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
@@ -1130,13 +1186,40 @@ namespace MobileGL::MG_Impl::GLImpl {
pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE || pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE ||
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR || pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) { pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) {
// GL 4.6 core 8.10: a multisample target simply does not ACCEPT these pnames, which is
// an INVALID_ENUM - not the INVALID_OPERATION the two BASE_LEVEL gates above report.
// Those really are operation errors (the pname is accepted, the value is not), which is
// presumably how the wrong class got copied down here.
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Sampler state is invalid for multisample textures.")); "Multisample textures do not accept sampler-state pnames."));
return false; return false;
} }
// The six pnames a texture object shares with a sampler object carry an enum VALUE, and an
// unrecognised one is INVALID_ENUM. The texture path used to hand the value straight to
// ConvertGLEnumToSamplerWrapMode / ...FilterMode and throw the Unknown away, so
// glTexParameteri(GL_TEXTURE_WRAP_S, GL_RED) was silently accepted. Sampler objects have had
// exactly this validator all along; calling it here rather than writing a second one is also
// what keeps the two spellings of the same state from drifting.
//
// Called selectively: ValidateSamplerParam's default arm reports InvalidEnum for anything it
// does not know, and the texture-only pnames (BASE_LEVEL, SWIZZLE_*, ...) are not in its list.
switch (pname) {
case GL_TEXTURE_WRAP_S:
case GL_TEXTURE_WRAP_T:
case GL_TEXTURE_WRAP_R:
case GL_TEXTURE_MIN_FILTER:
case GL_TEXTURE_MAG_FILTER:
case GL_TEXTURE_COMPARE_MODE:
case GL_TEXTURE_COMPARE_FUNC:
if (!SamplerImpl::ValidateSamplerParam(pname, static_cast<GLenum>(param))) return false;
break;
default:
break;
}
if (target == TextureTarget::TextureRectangle) { if (target == TextureTarget::TextureRectangle) {
if ((pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T) && if ((pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T) &&
(param == GL_MIRROR_CLAMP_TO_EDGE || param == GL_MIRRORED_REPEAT || param == GL_REPEAT)) { (param == GL_MIRROR_CLAMP_TO_EDGE || param == GL_MIRRORED_REPEAT || param == GL_REPEAT)) {
@@ -1446,6 +1529,78 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// The targets glTexParameter* / glGetTexParameter* accept (GL 4.6 core 8.10 and 8.11). This is a
// SHORTER list than the one ConvertGLEnumToTextureTarget knows, and deliberately so: that
// converter folds the six cube-map FACE targets onto TextureCubeMap because glTexImage2D and
// glCopyTexImage2D need exactly that folding, and it maps GL_TEXTURE_BUFFER to a real target
// because glTexBuffer needs it. Neither is a legal parameter target, so without a separate
// predicate glTexParameteri(GL_TEXTURE_CUBE_MAP_POSITIVE_X, ...) quietly applied the parameter
// to the bound cube map and glGetTexParameterIiv(GL_TEXTURE_BUFFER, ...) quietly answered from
// the default texture - both GL_NO_ERROR where the spec says GL_INVALID_ENUM.
//
// An enum the converter does not know at all was equally silent: it produced TextureTarget::
// Unknown, GetTextureObjectByTargetForParameter handed back the null object and every caller
// returned without recording anything. Rejecting here closes that too, at the entry point rather
// than at the lookup, so exactly one error is recorded.
//
// EXACTLY the ten targets 8.10 and 8.11 enumerate - no proxies. The spec's own asymmetry is the
// proof: GetTexLevelParameter needs an explicit clause extending its list with PROXY_TEXTURE_1D,
// PROXY_TEXTURE_2D and the rest, and neither TexParameter nor GetTexParameter carries one. That
// clause is why GetTexLevelParameteriv_State/GetTexLevelParameterfv_State are deliberately NOT
// gated by this predicate.
//
// Routing was not a reason to accept them: GetTextureObjectByTargetForParameter resolves a proxy
// object only after a proxy glTexImage has run, so before that the parameter call was a silent
// no-op and after it the parameter was applied for real - both GL_NO_ERROR, and both the same
// silent-acceptance shape this predicate exists to close for cube faces and GL_TEXTURE_BUFFER.
static Bool IsLegalTextureParameterTarget(GLenum target) {
switch (target) {
case GL_TEXTURE_1D:
case GL_TEXTURE_2D:
case GL_TEXTURE_3D:
case GL_TEXTURE_1D_ARRAY:
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_RECTANGLE:
case GL_TEXTURE_CUBE_MAP:
case GL_TEXTURE_CUBE_MAP_ARRAY:
case GL_TEXTURE_2D_MULTISAMPLE:
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
return true;
default:
return false;
}
}
// The by-NAME spelling of the same rule. glTextureParameter* has no target token, so GL 4.6 core
// 8.10 applies the list to the texture's EFFECTIVE target instead. The four vector DSA forms
// reach the gate above for free because they re-enter through WithTemporarilyBoundNamedTexture,
// which synthesizes the target from the object; the two scalar forms call the per-object setter
// directly and reached no gate at all, so glTextureParameteri on a buffer texture applied state
// with GL_NO_ERROR while glTextureParameteriv on the same texture answered GL_INVALID_ENUM.
static Bool ValidateNamedTextureParameterTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (!textureObject) return false;
const GLenum effectiveTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
if (IsLegalTextureParameterTarget(effectiveTarget)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("The effective target {} does not accept texture parameters.",
MG_Util::ConvertGLEnumToString(effectiveTarget))));
return false;
}
static Bool ValidateTextureParameterTarget(GLenum target, const char* caller) {
if (IsLegalTextureParameterTarget(target)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("target {} does not accept texture parameters.", MG_Util::ConvertGLEnumToString(target))));
return false;
}
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture // Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset // (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay // sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
@@ -1852,6 +2007,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// TexParameteriv/TexParameterfv are introduced in OpenGL 4.0, so do not support them for now. // TexParameteriv/TexParameterfv are introduced in OpenGL 4.0, so do not support them for now.
void TexParameterf_State(GLenum target, GLenum pname, GLfloat param) { void TexParameterf_State(GLenum target, GLenum pname, GLfloat param) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================ // ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -1949,6 +2105,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void TexParameteri_State(GLenum target, GLenum pname, GLint param) { void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================ // ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -1963,6 +2121,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// Quick and dirty TexParameter*v implementation to make NeoForge happy. // Quick and dirty TexParameter*v implementation to make NeoForge happy.
// TODO: implement the missing part // TODO: implement the missing part
void TexParameterfv_State(GLenum target, GLenum pname, const GLfloat* params) { void TexParameterfv_State(GLenum target, GLenum pname, const GLfloat* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) { switch (pname) {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
// ======================= Converting ================================ // ======================= Converting ================================
@@ -1972,6 +2131,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
// The vector setters reach the border colour without passing through the per-object
// validator the scalar ones use, so the multisample gate has to be asked for explicitly -
// otherwise glTexParameterfv(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_BORDER_COLOR, ...)
// is accepted while the scalar spelling of the same call is not.
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromFloats(textureObject, params); SetTextureBorderColorFromFloats(textureObject, params);
break; break;
} }
@@ -1994,6 +2158,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void TexParameteriv_State(GLenum target, GLenum pname, const GLint* params) { void TexParameteriv_State(GLenum target, GLenum pname, const GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) { switch (pname) {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
// ======================= Converting ================================ // ======================= Converting ================================
@@ -2003,6 +2168,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromInts(textureObject, params); SetTextureBorderColorFromInts(textureObject, params);
break; break;
} }
@@ -2023,12 +2189,14 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void TexParameterIiv_State(GLenum target, GLenum pname, const GLint* params) { void TexParameterIiv_State(GLenum target, GLenum pname, const GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) { switch (pname) {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromIntegerInts(textureObject, params); SetTextureBorderColorFromIntegerInts(textureObject, params);
break; break;
} }
@@ -2051,12 +2219,14 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void TexParameterIuiv_State(GLenum target, GLenum pname, const GLuint* params) { void TexParameterIuiv_State(GLenum target, GLenum pname, const GLuint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) { switch (pname) {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromUnsignedInts(textureObject, params); SetTextureBorderColorFromUnsignedInts(textureObject, params);
break; break;
} }
@@ -2203,6 +2373,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return; if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return; if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return; if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
if (!TextureImpl::ValidateTextureBorderNumber(border)) return; if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
@@ -2632,6 +2803,22 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// GL 4.6 core 8.9 / GL_EXT_texture_buffer: the two TARGET-taking forms (glTexBuffer,
// glTexBufferRange) accept exactly GL_TEXTURE_BUFFER, and anything else is GL_INVALID_ENUM.
// Checked up front rather than left to fall out of "the bound object is not a buffer texture"
// deeper in, because that path's error code depends on which entry point took it - the
// name-taking DSA forms owe GL_INVALID_OPERATION for the same shape - and because for some
// targets it did not reach that check at all. esextcTextureBufferErrors walks every other
// texture target through both entry points and reads the code back each time.
static Bool ValidateBufferTextureTarget(GLenum target, const char* caller) {
if (target == GL_TEXTURE_BUFFER) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
std::format("target 0x{:X} is not GL_TEXTURE_BUFFER.", target)));
return false;
}
static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size, GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
const char* caller) { const char* caller) {
@@ -2714,9 +2901,21 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat); TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
if (!ValidateBufferTextureTarget(target, __func__)) return;
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
// TODO: make sure `internalformat` is in one of supported format for TexBuffer // The sized-format table a buffer texture accepts (GL 4.6 core table 8.15). The DSA and
// range forms have always run this through AttachBufferToTexture; this one carried a TODO
// instead, so glTexBuffer(GL_TEXTURE_BUFFER, GL_DEPTH_COMPONENT32F, ...) succeeded.
if (!IsBufferTextureInternalFormat(internalformat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
internalformat)));
return;
}
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a // GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on // nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER), // the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
@@ -2740,6 +2939,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// silent no-op; the slot is never empty now that every unit/target holds its default. // silent no-op; the slot is never empty now that every unit/target holds its default.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) { if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
// Defensive: the target gate above already rejected every target but GL_TEXTURE_BUFFER,
// whose binding slot only ever holds buffer textures.
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -2767,6 +2968,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexParameterIuiv_State(GLenum target, GLenum pname, GLuint* params) { void GetTexParameterIuiv_State(GLenum target, GLenum pname, GLuint* params) {
if (params == nullptr) return; if (params == nullptr) return;
if (!ValidateTextureParameterTarget(target, __func__)) return;
if (pname == GL_TEXTURE_BORDER_COLOR) { if (pname == GL_TEXTURE_BORDER_COLOR) {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -2798,6 +3000,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexParameterIiv_State(GLenum target, GLenum pname, GLint* params) { void GetTexParameterIiv_State(GLenum target, GLenum pname, GLint* params) {
if (params == nullptr) return; if (params == nullptr) return;
if (!ValidateTextureParameterTarget(target, __func__)) return;
if (pname == GL_TEXTURE_BORDER_COLOR) { if (pname == GL_TEXTURE_BORDER_COLOR) {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -2823,6 +3026,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params) { Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return false;
// ======================= Converting ================================ // ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -2905,11 +3110,16 @@ namespace MobileGL::MG_Impl::GLImpl {
break; break;
case GL_TEXTURE_BORDER_COLOR: case GL_TEXTURE_BORDER_COLOR:
if (params) { if (params) {
// glGetTexParameteriv is the exact inverse of glTexParameteriv: GL 4.6 core
// equation 2.3 against equation 2.2 on the write side (SetTextureBorderColorFromInts).
// A bare truncating cast turned the ~4.7e-10 that equation 2.2 makes of a small
// integer back into 0, so the legal {0,1,2,4} round trip answered {0,0,0,0}. The raw
// integer border colour is what glGetTexParameterIiv returns, not this.
const auto& borderColor = textureObject->GetBorderColor(); const auto& borderColor = textureObject->GetBorderColor();
params[0] = static_cast<GLint>(borderColor.x()); params[0] = MG_Util::FloatToSignedNormalizedInt32(borderColor.x());
params[1] = static_cast<GLint>(borderColor.y()); params[1] = MG_Util::FloatToSignedNormalizedInt32(borderColor.y());
params[2] = static_cast<GLint>(borderColor.z()); params[2] = MG_Util::FloatToSignedNormalizedInt32(borderColor.z());
params[3] = static_cast<GLint>(borderColor.w()); params[3] = MG_Util::FloatToSignedNormalizedInt32(borderColor.w());
} }
break; break;
case GL_TEXTURE_SWIZZLE_RGBA: case GL_TEXTURE_SWIZZLE_RGBA:
@@ -3003,6 +3213,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetTexParameterfv_State(GLenum target, GLenum pname, GLfloat* params) { void GetTexParameterfv_State(GLenum target, GLenum pname, GLfloat* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================ // ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -3296,6 +3508,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_ALPHA_SIZE: case GL_TEXTURE_ALPHA_SIZE:
case GL_TEXTURE_DEPTH_SIZE: case GL_TEXTURE_DEPTH_SIZE:
case GL_TEXTURE_STENCIL_SIZE: case GL_TEXTURE_STENCIL_SIZE:
case GL_TEXTURE_SHARED_SIZE:
if (params) { if (params) {
*params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname); *params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname);
} }
@@ -3468,6 +3681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_ALPHA_SIZE: case GL_TEXTURE_ALPHA_SIZE:
case GL_TEXTURE_DEPTH_SIZE: case GL_TEXTURE_DEPTH_SIZE:
case GL_TEXTURE_STENCIL_SIZE: case GL_TEXTURE_STENCIL_SIZE:
case GL_TEXTURE_SHARED_SIZE:
if (params) { if (params) {
*params = static_cast<GLfloat>(GetTextureLevelComponentParameter(textureObject->GetFormat(), pname)); *params = static_cast<GLfloat>(GetTextureLevelComponentParameter(textureObject->GetFormat(), pname));
} }
@@ -3633,9 +3847,159 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
Bool ValidateCopyTextureSubImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, const char* caller);
// The destination box of a copy has to lie inside the storage the copy actually WRITES, which
// is the requested (uploadTarget, level) pair's - not level 0's.
//
// This exists because the general-purpose ValidateTextureSubImageOffsets bounds everything by
// ITextureObject::GetBaseSize(), which is hardcoded to level 0 (TextureObject::GetBaseSize ->
// GetTexelSize(0, 0)). CopyReadFramebufferIntoMipmapRegion, meanwhile, sizes its rows and
// slices from GetMipmapTexelSize(uploadTarget, level) and memcpys into the exact-sized
// std::vector MipmapStorage allocated for that level, with no clamp of its own. A box that is
// legal at level 0 and out of range at level N therefore passed validation and wrote past the
// end of the heap allocation - e.g. a 4x4 copy at offset (4,4) into level 2 of an 8x8x4
// GL_RGBA8 array texture ran 24 bytes past a 64-byte buffer. Every level > 0 of every
// mipmapped texture was reachable that way, and both entry points had been no-ops before, so
// the whole exposure arrived with their implementation.
static Bool ValidateCopySubImageRegionAtLevel(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLint xoffset,
GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, const char* caller) {
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "The destination texture has no mipmap storage."));
return false;
}
const IntVec3 levelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
// A level that was never defined reports a degenerate extent. GL 4.6 core 8.6 makes
// copying into an undefined texture image INVALID_OPERATION, and it is also what keeps the
// writer below from indexing an empty allocation.
if (levelSize.x() <= 0 || levelSize.y() <= 0 || levelSize.z() <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"The requested texture level has no storage."));
return false;
}
// Signed 64-bit sums: xoffset and width are both GLint and an application may pass values
// whose sum overflows a GLint, which would otherwise compare as negative and pass.
const Int64 lastX = static_cast<Int64>(xoffset) + static_cast<Int64>(width);
const Int64 lastY = static_cast<Int64>(yoffset) + static_cast<Int64>(height);
const Int64 lastZ = static_cast<Int64>(zoffset) + static_cast<Int64>(depth);
if (xoffset < 0 || yoffset < 0 || zoffset < 0 || lastX > levelSize.x() || lastY > levelSize.y() ||
lastZ > levelSize.z()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("The destination region does not lie inside level {} ({}x{}x{}).", level,
levelSize.x(), levelSize.y(), levelSize.z())));
return false;
}
return true;
}
// The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has
// resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between
// the two forms: the DSA form takes a cube map and selects the face with zoffset (GL 4.6 core
// 8.6), while the target-taking form cannot even name a cube map here - GL_TEXTURE_CUBE_MAP is
// not in glCopyTexSubImage3D's accepted-target list, its faces go through
// glCopyTexSubImage2D - so for it zoffset is always a layer index.
static void CopyTextureSubImage3DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset,
const char* caller) {
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
return;
}
// THE FACE MAPPING HAS TO HAPPEN BEFORE THE BOUNDS CHECK, not after it. A cube map stores
// its six faces as six upload targets of ONE z-slice each, so its GetBaseSize().z() is 1 -
// and the generic offset validator, whose z bound always comes from that, rejected every
// zoffset in 1..5 with GL_INVALID_VALUE before the mapping below could run. Five of six
// faces were unreachable through glCopyTextureSubImage3D even though the entry point
// documents zoffset as the face selector (GL 4.6 core 8.6). The cube bound is the FACE
// COUNT, which the generic validator has no way to express because its `depth` parameter
// is the copy extent; glClearTexSubImage already special-cases the same shape.
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
GLint sliceOffset = zoffset;
if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
const SizeT faceCount = textureObject->GetUploadTargets().size();
if (zoffset < 0 || static_cast<SizeT>(zoffset) >= faceCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"zoffset selects the cube map face and must be in [0, " + std::to_string(faceCount) + ")."));
return;
}
uploadTarget = static_cast<TextureUploadTarget>(
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
sliceOffset = 0;
}
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset,
width, height, /*depth=*/1, caller)) {
return;
}
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
width, height, caller);
}
// The same for the one-dimensional pair. A 1D level is {width, 1, 1}, so the y and z arms of
// the check above are trivially satisfied and the x arm is the whole rule - which is exactly
// the one that overflowed: level 2 of an 8-texel GL_RGBA8 1D texture is 8 bytes, and a 4-texel
// copy at xoffset 4 wrote 16 bytes starting 16 bytes in, entirely outside the allocation.
static void CopyTextureSubImage1DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level, GLint xoffset, GLint x, GLint y, GLsizei width,
const char* caller) {
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (width < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
return;
}
const TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
/*zoffset=*/0, width, /*height=*/1, /*depth=*/1, caller)) {
return;
}
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
/*zoffset=*/0, x, y, width, /*height=*/1, caller);
}
void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height) { GLint y, GLsizei width, GLsizei height) {
// TODO: implement // GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts
// exactly TEXTURE_3D, TEXTURE_2D_ARRAY and TEXTURE_CUBE_MAP_ARRAY. A cube map's faces are
// two-dimensional targets of their own and go through glCopyTexSubImage2D.
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::Texture2DArray &&
textureTarget != TextureTarget::TextureCubeMapArray) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glCopyTexSubImage3D requires GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY or "
"GL_TEXTURE_CUBE_MAP_ARRAY."));
return;
}
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return;
CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
/*allowCubeFaceFromZOffset=*/false, __func__);
} }
// What the three CopyTextureSubImage forms check in common (GL 4.6 core 8.6), once the caller // What the three CopyTextureSubImage forms check in common (GL 4.6 core 8.6), once the caller
@@ -4001,7 +4365,20 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void CopyTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { void CopyTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
// TODO: implement // The bound-texture form of glCopyTextureSubImage1D. GL 4.6 core 8.6 accepts only
// GL_TEXTURE_1D here.
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (textureTarget != TextureTarget::Texture1D) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glCopyTexSubImage1D requires GL_TEXTURE_1D."));
return;
}
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return;
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
} }
Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
@@ -4457,6 +4834,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return; if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return; if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return; if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
if (!TextureImpl::ValidateTextureBorderNumber(border)) return; if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
@@ -4901,6 +5279,72 @@ namespace MobileGL::MG_Impl::GLImpl {
"GetTexImage_State"); "GetTexImage_State");
} }
// What this helper can and cannot answer.
//
// ProcessTexturePixelsDataPack performs NO format or type conversion: it sizes every texel with
// GetInternalBytesPerPixel(the TEXTURE's internal format) and memcpys the shadow rows verbatim,
// and it carries a standing TODO for the pixel-store parameters, so it honours only SwapBytes and
// the bitmap LSBFirst path. Both facts are invisible from the outside, and both are dangerous:
//
// * a (format, type) narrower than the shadow's own texel makes the copy write MORE bytes than
// the caller's buffer holds. glGetTexImage passes bufSize = -1 (it has no bufSize argument),
// so the size guard below is skipped and the Memcpy runs off the end of the application's
// allocation - reading an 8x8 GL_RGBA8 level as (GL_RED, GL_UNSIGNED_BYTE) writes 256 bytes
// into the 64 that GL 4.6 core 8.11 says are required. A wider (format, type) is not an
// overflow but is still wrong data.
// * a pack state that puts padding, a row-length override or a skip offset between rows is
// ignored outright, so the rows land at the wrong destination strides - while the GPU
// readback path (DirectGLES StoreClientRows, and DirectVulkan through it) honours all of it.
// Same glGetTexImage call, two different destination layouts, decided by whether the texture
// happens to have a GPU image.
//
// So the copy is only correct when the client layout IS the shadow layout and the destination
// walk is tight. That is checked here rather than assumed, and a request outside it is refused
// with an error instead of being answered wrongly. Refusing is a real narrowing of what GL
// promises - the spec wants the conversion performed - but the alternative on this path is a
// heap overflow, and the conversion belongs in the pack processor rather than in another
// open-coded copy here.
static Bool ValidateShadowReadbackLayout(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureInputFormat textureInputFormat,
TexturePixelDataType texturePixelDataType, GLsizei width,
const char* caller) {
const SizeT shadowTexelSize =
MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
const SizeT clientTexelSize = MG_Util::GetInputBytesPerPixel(textureInputFormat, texturePixelDataType);
if (shadowTexelSize == 0 || clientTexelSize == 0 || shadowTexelSize != clientTexelSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("Reading this texture back needs a format/type conversion that the CPU-shadow "
"path cannot perform: the shadow texel is {} bytes and the requested one is {}.",
shadowTexelSize, clientTexelSize)));
return false;
}
// A tight destination walk is the only one the pack processor produces. GL_PACK_ALIGNMENT
// defaults to 4, so a row whose byte count is not already a multiple of it needs padding that
// would never be written - no glPixelStorei call from the application is required to reach
// this.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT alignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
const SizeT rowBytes = static_cast<SizeT>(std::max<GLsizei>(width, 0)) * clientTexelSize;
const Bool tightRows = (rowBytes % alignment) == 0;
const Bool noOverrides = packParams.RowLength == 0 && packParams.ImageHeight == 0 &&
packParams.SkipPixels == 0 && packParams.SkipRows == 0 &&
packParams.SkipImages == 0;
if (!tightRows || !noOverrides) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"The CPU-shadow readback path packs rows tightly and cannot honour a pixel-store state that "
"adds row padding, a row-length override or a skip offset."));
return false;
}
return true;
}
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget textureUploadTarget, GLint level, GLenum format, TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller) { GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
@@ -4927,6 +5371,11 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level); const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);
if (!ValidateShadowReadbackLayout(textureObject, textureInputFormat, texturePixelDataType, texelSize.x(),
caller)) {
return;
}
const void* src = textureMipmapObject->MapMipmapData(textureUploadTarget, level); const void* src = textureMipmapObject->MapMipmapData(textureUploadTarget, level);
if (!src) return; if (!src) return;
@@ -5195,17 +5644,11 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
if (textureObject->GetTarget() == TextureTarget::TextureCubeMapArray &&
(width != height || depth % 6 != 0)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"Cube map array immutable storage must be square with depth multiple of 6."));
return;
}
const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject); const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
// The cube-array shape rules, shared with glTexImage3D / glCompressedTexImage3D so the
// three cannot drift (they had: this check used to exist here and nowhere else).
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
@@ -5785,11 +6228,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void TextureParameteri(GLuint texture, GLenum pname, GLint param) { void TextureParameteri(GLuint texture, GLenum pname, GLint param) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
TextureParameterObject_State(textureObject, pname, param, __func__); TextureParameterObject_State(textureObject, pname, param, __func__);
} }
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param) { void TextureParameterf(GLuint texture, GLenum pname, GLfloat param) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
TextureParameterObjectf_State(textureObject, pname, param, __func__); TextureParameterObjectf_State(textureObject, pname, param, __func__);
} }
@@ -6519,6 +6964,10 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) { void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
// The TARGET-taking form owes GL_INVALID_ENUM for a target that is not GL_TEXTURE_BUFFER,
// where the name-taking DSA forms below owe GL_INVALID_OPERATION for the corresponding
// "that texture is not a buffer texture". Same shared body, different gate.
if (!ValidateBufferTextureTarget(target, __func__)) return;
AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset, AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
static_cast<SizeT>(size < 0 ? 0 : size), __func__); static_cast<SizeT>(size < 0 ? 0 : size), __func__);
} }
@@ -6617,9 +7066,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"CopyTextureSubImage1D requires a 1D texture.")); "CopyTextureSubImage1D requires a 1D texture."));
return; return;
} }
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return; CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
} }
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
@@ -6638,20 +7085,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"cube map array texture.")); "cube map array texture."));
return; return;
} }
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, __func__)) {
return;
}
// A cube map addresses its faces as separate upload targets, so zoffset selects the target // A cube map addresses its faces as separate upload targets, so zoffset selects the target
// rather than a slice within one; every other layered target keeps zoffset as the slice. // rather than a slice within one; every other layered target keeps zoffset as the slice.
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject); CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
GLint sliceOffset = zoffset; /*allowCubeFaceFromZOffset=*/true, __func__);
if (target == TextureTarget::TextureCubeMap) {
uploadTarget = static_cast<TextureUploadTarget>(
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
sliceOffset = 0;
}
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
width, height, __func__);
} }
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
@@ -8,9 +8,24 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
//
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
// that adds row padding / a row-length override / a skip offset, is rejected with
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer // The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
// clears take the same list, so it is shared rather than written out twice. // clears take the same list, so it is shared rather than written out twice.
Bool IsBufferTextureInternalFormat(GLenum internalformat); Bool IsBufferTextureInternalFormat(GLenum internalformat);
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller) {
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
return true;
}
if (width != height) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array levels must be square (width == height)"));
return false;
}
if (depth % 6 != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array depth must be a multiple of six"));
return false;
}
return true;
}
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) { Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX || if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY || target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType); Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
Bool ValidateTextureLevelNumber(Int level); Bool ValidateTextureLevelNumber(Int level);
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height); Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
// non-cube-array upload target answers true untouched.
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller);
Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border); Bool ValidateTextureBorderNumber(Int border);
@@ -12,15 +12,15 @@
#include <MG_State/GLState/ErrorState/Error.h> #include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h> #include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h> #include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl { namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
Uint GetMaxVertexAttribs() { Uint GetMaxVertexAttribs() {
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); // Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
if (!MG_Backend::pActiveBackendObject) return capacity; const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
const Int backendLimit =
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs; hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
if (backendLimit <= 0) return capacity; return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
return std::min(static_cast<Uint>(backendLimit), capacity);
} }
Uint GetMaxVertexAttribBindings() { Uint GetMaxVertexAttribBindings() {
@@ -63,8 +63,10 @@ add_executable(MobileGLIntegrationTest
Scenarios/PipelineFailureScenario.cpp Scenarios/PipelineFailureScenario.cpp
Scenarios/AdvertisedLimitsScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp
Scenarios/PixelStoreSweepScenario.cpp Scenarios/PixelStoreSweepScenario.cpp
Scenarios/PrimitiveRestartScenario.cpp
Scenarios/FragCoordOriginScenario.cpp Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp Scenarios/DepthStencilReadbackMatrixScenario.cpp
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
@@ -101,13 +103,18 @@ add_executable(MobileGLIntegrationTest
Scenarios/TextureViewScenario.cpp Scenarios/TextureViewScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredAttachmentShapeScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp Scenarios/AtomicCounterScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/SpirvShaderBinaryScenario.cpp
Scenarios/RelinkStageSetScenario.cpp Scenarios/RelinkStageSetScenario.cpp
Scenarios/GuiBatchScenario.cpp Scenarios/GuiBatchScenario.cpp
Scenarios/UnboundImageDescriptorScenario.cpp Scenarios/UnboundImageDescriptorScenario.cpp
Scenarios/IntegerBorderColorScenario.cpp
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -56,7 +56,13 @@ namespace MGITest {
const std::vector<LimitBound>& BufferLimitTable() { const std::vector<LimitBound>& BufferLimitTable() {
static const std::vector<LimitBound> table = { static const std::vector<LimitBound> table = {
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256}, // 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
// layer's indexed-binding array - the two were made to coincide when the array
// was widened from 36, which had made the clamp in GL_Getter degenerate.
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
// three, and the two tessellation stages were simply missing from it.
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256}, {GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256}, {GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256}, {GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
@@ -133,6 +139,49 @@ namespace MGITest {
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = " << relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to"; << bindings << "; a shader may declare more blocks than there are binding points to bind them to";
} }
// THE MIDDLE TERM, which the relation quoted above always had and this case never
// checked. It is the one that actually broke: widening the binding-point array to 84
// raised what every PER-STAGE count clamps to, while the combined value was a
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
struct StageAgainstCombined {
GLenum stage;
const char* stageName;
GLenum combined;
const char* combinedName;
};
const StageAgainstCombined stageRelations[] = {
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
};
for (const StageAgainstCombined& relation : stageRelations) {
GLint stage = -1;
GLint combined = -1;
glGetIntegerv(relation.stage, &stage);
glGetIntegerv(relation.combined, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
EXPECT_LE(stage, combined)
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
}
} }
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's // KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
@@ -199,6 +248,80 @@ namespace MGITest {
"derived component limits are computed in"; "derived component limits are computed in";
} }
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
// too-small value against a context advertising 4.6, and each is the FIRST call its
// conformance case makes - so the case died before it could measure anything.
//
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
// cull-distance route MUST report it), so it is checked for answerability only, below.
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
const std::vector<LimitBound> table = {
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
// the number un-gates two transform-feedback CTS cases per package across
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
// a row here would pin a number the implementation cannot back.
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
1 << 20},
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
1 << 20},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
58368, 1 << 30},
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
};
for (const LimitBound& bound : table) {
GLint value = -424242;
glGetIntegerv(bound.pname, &value);
const unsigned int error = FirstGLError();
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
if (error != GL_NO_ERROR) continue;
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
<< bound.minimum;
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
<< bound.ceiling;
}
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
// so only answerability and the combined-limit ordering are checked here.
GLint cull = -1;
GLint clip = -1;
GLint combined = -1;
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
EXPECT_GE(cull, 0);
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
// the wide query must answer it and the narrow one must saturate rather than wrap.
GLint64 elementIndex = -1;
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
}
// ARB_viewport_array's own limits. They are advertised from three different places - // ARB_viewport_array's own limits. They are advertised from three different places -
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the // GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
// subpixel bits from the backend caps table - and each backend fills that table from a // subpixel bits from the backend caps table - and each backend fills that table from a
@@ -0,0 +1,250 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.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
//
// Scenario - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED.
//
// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE
// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level
// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with
// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no
// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body
// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and
// all three entry points.
//
// The frontend understands this shape - the clear is a pure CPU-shadow write, and
// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The
// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a
// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and
// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly
// as it found it. The conformance failures carried no <Text> at all, because nothing raised a GL
// error: the destination was simply never written, so the test compared its own zero-initialized
// buffer against the clear value.
//
// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have
// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on
// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would
// return stale bytes for every render-to-texture result instead.
//
// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible
// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a
// separate change (it moves every GL-level-to-subresource translation in the backend); this
// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able
// to define a lone level N - is the built-in control for what the answer should be.
#include <array>
#include <cstdint>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// The conformance family's own shape: a mid-chain level of a texture that has nothing else.
constexpr GLint kDefinedLevel = 3;
constexpr GLsizei kLevelExtent = 8;
struct Texel8 {
GLubyte r = 0, g = 0, b = 0, a = 0;
bool operator==(const Texel8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::ostream& operator<<(std::ostream& os, const Texel8& c) {
return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
}
// The conformance test's clear value is a single repeated component; 5 is what it uses, and
// it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one).
constexpr Texel8 kClearValue{5, 5, 5, 5};
constexpr Texel8 kInitialValue{200, 100, 50, 255};
class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
if (m_texture != 0) {
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &m_texture);
m_texture = 0;
}
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D,
// which would define the whole chain and could not express "level 0 does not exist".
void MakeTextureWithOnlyLevel(GLint level) {
if (m_texture != 0) glDeleteTextures(1, &m_texture);
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
const std::vector<Texel8> initial(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
initial.data());
// What the conformance case does: MAX_LEVEL names the one level that exists, and
// BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND
// nominally the base level, the shape the backend could not express.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level;
}
std::vector<Texel8> ReadLevel(GLint level) {
std::vector<Texel8> pixels(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0});
glBindTexture(GL_TEXTURE_2D, m_texture);
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind";
return pixels;
}
void ExpectAllTexels(const char* what, const std::vector<Texel8>& pixels, Texel8 expected) {
std::size_t offenders = 0;
Texel8 firstBad{};
for (const Texel8& pixel : pixels) {
if (pixel == expected) continue;
if (offenders == 0) firstBad = pixel;
++offenders;
}
EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " ("
<< offenders << " of " << pixels.size() << " texels wrong)";
}
// Level 0 defined, a GAP, then `level` defined. GL keeps the intervening levels at a zero
// extent, so the backend's mip walk stops at the gap and the VkImage ends up with FEWER
// mip levels than the GL level count - which is a different shape from "no image at all"
// and is why the readback has to bound the level against the IMAGE.
void MakeTextureWithAGapBefore(GLint level) {
if (m_texture != 0) glDeleteTextures(1, &m_texture);
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
const std::vector<Texel8> base(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
base.data());
const std::vector<Texel8> gapped(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA,
GL_UNSIGNED_BYTE, gapped.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with a gap before level " << level;
}
GLuint m_texture = 0;
};
} // namespace
// The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the
// caller's buffer kept whatever it already held - which is why the conformance failures showed
// the test's own zero-initialized memory and carried no GL error.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(kDefinedLevel);
// Pre-flight: the level reads back as what was uploaded. This is what makes the assertion
// after the clear falsifiable - without it, a readback that silently wrote nothing could not
// be told from one that wrote the right answer.
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
Gl().EndFrame();
}
// The same shape through glClearTexSubImage, which is a separate entry point in the conformance
// family and failed on exactly the same bodies.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(kDefinedLevel);
glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA,
GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue);
Gl().EndFrame();
}
// The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from
// the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally
// would pass the two tests above and this one too - but it would also hand back stale bytes for
// anything the GPU had written, which is why the partial-clear check below matters: the readback
// has to see a region the backend cleared and a region it did not, in one image.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(0);
ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue);
// Clear only the left half, so the answer is neither "all initial" nor "all cleared".
glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
&kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
const std::vector<Texel8> pixels = ReadLevel(0);
ASSERT_EQ(pixels.size(), static_cast<std::size_t>(kLevelExtent) * kLevelExtent);
for (int y = 0; y < kLevelExtent; ++y) {
for (int x = 0; x < kLevelExtent; ++x) {
const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue;
const Texel8 actual = pixels[static_cast<std::size_t>(y) * kLevelExtent + x];
ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")";
}
}
Gl().EndFrame();
}
// The adjacent shape the first fix did NOT cover: level 0 defined, a gap, then the level being
// read. This one DOES get a VkImage - just one with fewer mip levels than GL thinks the texture
// has - so the "no VkImage" test passes and the GL level was written straight into
// imageSubresource.mipLevel and into a VkImageMemoryBarrier's baseMipLevel. An out-of-range
// subresource is a promise the driver takes at face value; the glCopyImageSubData path two
// functions away grew the same guard after it SIGSEGV'd inside the Adreno driver.
//
// The level being read really does hold its own data (the shadow is its only copy, since nothing
// ever uploaded it), so the correct answer is the uploaded bytes - not a decline.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadBackALevelSeparatedFromLevelZeroByAGap) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithAGapBefore(kDefinedLevel);
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
// Level 0 is backed by the real image and must still read back from it, so the level bound is
// about the level and not about the texture.
ExpectAllTexels("level 0 after clearing level 3", ReadLevel(0), kInitialValue);
Gl().EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,391 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.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
//
// Scenario - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER.
//
// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border
// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and
// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754
// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on
// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK.
//
// Two independent halves, and this scenario covers both because it goes through the frontend:
//
// * the STATE had no record of which entry point wrote the border colour. All three
// representations are kept numerically in step, so the value alone cannot say whether the
// application called glTexParameterfv or glTexParameterIiv.
// * each backend then had exactly one border-colour call site: glTexParameterfv /
// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on
// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all.
//
// The border value is deliberately outside every predefined VkBorderColor and outside anything a
// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black,
// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color.
// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit.
//
// Both an integer image view and an integer border colour are involved, which is the other half of
// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour
// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK.
// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kOutputWidth = 8;
constexpr int kOutputHeight = 8;
// The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border
// is deliberately not derivable from the texel.
constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44};
constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3};
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
// One channel per draw, so a failure names the component that is wrong. The coordinate is a
// uniform rather than a literal so the same program serves the border sample and the inside
// sample and nothing can be constant-folded differently between them.
std::string FragmentSource(int channel) {
static const char* kChannels[4] = {"x", "y", "z", "w"};
return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n"
"out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") +
kChannels[channel] + ";\n}\n";
}
class IntegerBorderColorScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory.
const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}};
glGenTextures(1, &m_sourceTexture);
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
// 8x8 R32I render target: an integer readback, so nothing is normalized on the way
// out and a wrong value is reported as the number it actually was.
glGenTextures(1, &m_outputTexture);
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glGenVertexArrays(1, &m_vao);
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
}
void TearDown() override {
if (!Ready()) return;
if (m_sampler != 0) {
glBindSampler(0, 0);
glDeleteSamplers(1, &m_sampler);
m_sampler = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
if (m_narrowTexture != 0) glDeleteTextures(1, &m_narrowTexture);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// Samples `coord` through the integer sampler and returns every texel the draw wrote.
std::vector<std::int32_t> RenderChannel(int channel, float coordX, float coordY) {
const std::string fragment = FragmentSource(channel);
std::string error;
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
if (program == 0) {
ADD_FAILURE() << "channel " << channel << ": program did not build: " << error;
return {};
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glViewport(0, 0, kOutputWidth, kOutputHeight);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// A clear value nothing under test can produce, so an undrawn target is not mistaken
// for a correct one.
const GLint clearValue[4] = {-559038737, 0, 0, 0};
glClearBufferiv(GL_COLOR, 0, clearValue);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glUniform1i(glGetUniformLocation(program, "smp"), 0);
glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
glUseProgram(0);
glDeleteProgram(program);
return texels;
}
void ExpectAllTexels(const char* what, int channel, std::int32_t expected,
const std::vector<std::int32_t>& texels) {
if (texels.empty()) return;
std::size_t offenders = 0;
std::int32_t firstBad = 0;
for (const std::int32_t texel : texels) {
if (texel == expected) continue;
if (offenders == 0) firstBad = texel;
++offenders;
}
EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad
<< " instead of " << expected << " (" << offenders << " of " << texels.size()
<< " texels wrong)";
}
// Every component of the border, in one place, so both the texture-object and the
// sampler-object case assert exactly the same thing.
void ExpectBorderIsDelivered(const char* what) {
for (int channel = 0; channel < 4; ++channel) {
// (-0.5, -0.5) is a full texture width outside the image on both axes, so
// CLAMP_TO_BORDER can only answer with the border colour.
const std::vector<std::int32_t> texels = RenderChannel(channel, -0.5f, -0.5f);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind";
ExpectAllTexels(what, channel, kBorderColor[channel], texels);
}
}
// A narrow-format source built on demand, for the clamp cases. Returns the texture, which
// the caller owns until TearDown deletes it through m_narrowTexture.
void MakeNarrowSource(GLenum internalFormat, GLenum clientFormat, const void* texels,
const GLint* border, bool borderIsUnsigned) {
glGenTextures(1, &m_narrowTexture);
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 2, 2);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, clientFormat,
internalFormat == GL_R8UI ? GL_UNSIGNED_BYTE : GL_BYTE, texels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
if (borderIsUnsigned) {
const GLuint asUnsigned[4] = {static_cast<GLuint>(border[0]), static_cast<GLuint>(border[1]),
static_cast<GLuint>(border[2]), static_cast<GLuint>(border[3])};
glTexParameterIuiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, asUnsigned);
} else {
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
}
ASSERT_EQ(FirstGLError(), 0u) << "narrow source setup left a GL error behind";
}
// The narrow sources are single-channel, so only component 0 carries anything, and the
// sampler declaration has to match the format's signedness.
std::vector<std::int32_t> RenderNarrowBorder(bool isUnsignedSampler) {
const std::string fragment =
std::string("#version 330 core\n\nuniform ") + (isUnsignedSampler ? "usampler2D" : "isampler2D") +
" smp;\nuniform vec2 uCoord;\n\nout int out_color;\n\nvoid main()\n{\n"
" out_color = int(texture(smp, uCoord).x);\n}\n";
std::string error;
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
if (program == 0) {
ADD_FAILURE() << "narrow-border program did not build: " << error;
return {};
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glViewport(0, 0, kOutputWidth, kOutputHeight);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
const GLint clearValue[4] = {-559038737, 0, 0, 0};
glClearBufferiv(GL_COLOR, 0, clearValue);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
glUniform1i(glGetUniformLocation(program, "smp"), 0);
glUniform2f(glGetUniformLocation(program, "uCoord"), -0.5f, -0.5f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
glUseProgram(0);
glDeleteProgram(program);
return texels;
}
GLuint m_sourceTexture = 0;
GLuint m_outputTexture = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
GLuint m_sampler = 0;
GLuint m_narrowTexture = 0;
};
} // namespace
// The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE
// sample has to fetch the texture's own texel. If this fails the sampler, the shader or the
// integer readback is broken and nothing about the border colour has been measured.
TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) {
if (!Ready()) GTEST_SKIP();
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
for (int channel = 0; channel < 4; ++channel) {
const std::vector<std::int32_t> texels = RenderChannel(channel, 0.5f, 0.5f);
EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind";
ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels);
}
Gl().EndFrame();
}
// The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend
// already accepted and then flattened into the same FloatVec4 every other spelling wrote.
TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) {
if (!Ready()) GTEST_SKIP();
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
ExpectBorderIsDelivered("glTexParameterIiv");
Gl().EndFrame();
}
// The regression, sampler-object spelling - which is the one the conformance cases actually use,
// and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the
// sampler cache key on DirectVulkan, where a border colour that is not part of the key would
// alias two samplers that differ only in it).
TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) {
if (!Ready()) GTEST_SKIP();
glGenSamplers(1, &m_sampler);
ASSERT_NE(m_sampler, 0u);
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected";
// The texture object carries a DIFFERENT border colour, so a pass here cannot come from the
// texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state
// wins over the texture's for every sampling parameter.
const std::int32_t decoyBorder[4] = {0, 0, 0, 0};
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder);
glBindSampler(0, m_sampler);
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected";
ExpectBorderIsDelivered("glSamplerParameterIiv");
glBindSampler(0, 0);
Gl().EndFrame();
}
// GL 4.6 core 8.14.2: "For floating-point and integer formats, border values are clamped to the
// representable range of the format." A border of 300 on a GL_R8I texture is 127, not 300 - and
// VK_BORDER_COLOR_INT_CUSTOM_EXT delivers whatever it is handed, with format VK_FORMAT_UNDEFINED
// there is nothing for the driver to clamp against, so the clamp has to happen before the value
// leaves MobileGL. DirectGLES gets it right for free (the ES driver knows the texture format),
// which is what makes this a cross-backend divergence and not only a spec one.
TEST_F(IntegerBorderColorScenario, ASignedIntegerBorderIsClampedToTheFormatsRepresentableRange) {
if (!Ready()) GTEST_SKIP();
const std::int8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {300, 0, 0, 1};
MakeNarrowSource(GL_R8I, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/false);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8I border 300", 0, 127, sampled);
Gl().EndFrame();
}
// The reciprocal half, and the one that decides how the two integer forms relate: -1 written
// through glTexParameterIiv against an UNSIGNED format. GL 4.6 core 8.10 stores an "I"-form
// border unmodified with an integer internal data type and defines no sign conversion between
// the two integer forms, so the stored bits are reinterpreted in the sampled format's own
// signedness: 0xFFFFFFFF, clamped to the format's maximum of 255.
//
// That is the DRIVER's answer, established by running this case rather than by reading the spec:
// clamping to 0 is an equally defensible reading of the same paragraph, and DirectVulkan can be
// made to produce either - but DirectGLES forwards the value to the ES driver verbatim and cannot
// deviate, so choosing 0 would mean the same program sampling 0 on Magma and 255 on Espryt. The
// whole point of carrying the border colour's form is to stop that class of divergence, so the
// backends agree on the driver's answer.
//
// The clamp itself is still doing the work: without it the value reaches the driver as
// 0xFFFFFFFF against a format whose maximum is 255, with format VK_FORMAT_UNDEFINED and so
// nothing for the driver to clamp against.
TEST_F(IntegerBorderColorScenario, ANegativeBorderOnAnUnsignedFormatClampsToTheFormatsMaximum) {
if (!Ready()) GTEST_SKIP();
const std::uint8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {-1, 0, 0, 1};
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8UI border -1", 0, 255, sampled);
Gl().EndFrame();
}
// The same clamp from the unambiguous side: a value written through the UNSIGNED form that is
// simply too large for the format. No sign reinterpretation is involved, so both backends and
// the spec agree that 5000 on a GL_R8UI texture is 255.
TEST_F(IntegerBorderColorScenario, AnOversizedUnsignedBorderIsClampedToTheFormatsMaximum) {
if (!Ready()) GTEST_SKIP();
const std::uint8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {5000, 0, 0, 1};
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/true);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8UI border 5000", 0, 255, sampled);
Gl().EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,962 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.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
//
// Scenario - THE ATTACHMENT SHAPES A LAYERED FRAMEBUFFER CAN TAKE, AND THE ONE VIEW TYPE
// VULKAN ACCEPTS FOR ALL OF THEM.
//
// glFramebufferTexture on a GL_TEXTURE_3D or a GL_TEXTURE_CUBE_MAP_ARRAY makes a LAYERED
// framebuffer: one attachment that covers every slice / layer-face, addressed by a geometry
// shader writing gl_Layer. Vulkan has exactly one legal spelling for that
// (VUID-VkFramebufferCreateInfo-flags-04113: an attachment view must be VK_IMAGE_VIEW_TYPE_2D
// or _2D_ARRAY), and DirectVulkan used to hand vkCreateFramebuffer the IMAGE's own view type
// instead:
//
// * GL_TEXTURE_3D -> VK_IMAGE_VIEW_TYPE_3D. A 3D image has arrayLayers == 1 and keeps its
// layers on z, so the layer-span guard measured [0, depth) against 1, refused, and returned
// VK_NULL_HANDLE - which then went into pAttachments as a null handle.
// * GL_TEXTURE_CUBE_MAP_ARRAY -> VK_IMAGE_VIEW_TYPE_CUBE_ARRAY. A perfectly valid view, of a
// type no framebuffer may take. The driver dereferenced or rejected it inside
// vkCreateFramebuffer.
//
// Both exits were guarded only by MOBILEGL_ASSERT, which an INFO build (the production and CTS
// default) compiles to nothing - so both were process kills, not wrong pixels: 51 lost QPA
// records over 7 conformance bodies, one runner restart each.
//
// The same function is what routes a NON-layered slice of a 3D texture
// (glFramebufferTextureLayer), and it had the mirror-image hole: it asked for a 3D view there
// too, so the per-slice branch that exists for exactly this case was unreachable and every
// slice above z = 0 came back VK_NULL_HANDLE.
//
// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
// not merely survival: what a layer receives is a function of its own index, so an attachment that
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
// never landed here" reads differently from "the wrong layer landed here".
//
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
// sat beside a non-layered depth/stencil one and BOTH backends silently lost the draw. That is
// the shape texture_cube_map_array.stencil_attachments_*_layered and
// geometry_shader.layered_framebuffer.stencil_support are built on, and it is why they fail on
// Espryt as well as crashing on Magma. Case (5) is what found it.
//
// DirectGLES is the control: it hands the same GL calls to the driver, so a red on both backends
// means the scenario is wrong - or the defect is in the shared front end, as it was above - and a
// red on DirectVulkan alone means Magma is.
#include <cstddef>
#include <cstdlib>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
// Four z slices: enough that "only slice 0 was written" and "the whole thing was written"
// are different answers, and small enough that the geometry shader stays well inside
// GL_MAX_GEOMETRY_OUTPUT_VERTICES.
constexpr int k3DSlices = 4;
// Two cubes. One cube would let "attached a single cube" pass; twelve layer-faces would
// not.
constexpr int kCubeLayerFaces = 12;
// The slice a non-layered 3D attachment names. Not 0: slice 0 is the one address that is
// right whether or not the slice is resolved at all.
constexpr int kSubjectSlice = 2;
// Layers of the 1D array whose clear the last case checks. Its layer count lives in the
// state-side HEIGHT, not in z, which is the whole reason it is here.
constexpr int kOneDArrayLayers = 4;
// A colour no pass paints, uploaded before every draw. A layer that reads it back was
// never rendered to.
constexpr GLubyte kPoison = 0xAB;
// The glClear colour the two materialise cases use. Chosen as exact 8-bit values and fed
// to glClearColor as n/255, so the round trip through a UNORM8 target is lossless and a
// mismatch means a real miss rather than rounding.
constexpr Rgba8 kClearColor{17, 68, 187, 255};
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
// says which layer it came from) and b names the PASS (so "the second draw was not
// rejected" is distinguishable from "the first draw never happened").
Rgba8 ExpectedColor(int layer, int pass) {
return {static_cast<GLubyte>(10 + layer * 20), static_cast<GLubyte>(200 - layer * 10),
static_cast<GLubyte>(3 + pass * 60), 255};
}
std::string Describe(const Rgba8& color) {
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " +
std::to_string(color.b) + ", " + std::to_string(color.a) + ")";
}
// A full-viewport triangle built from gl_VertexID, so nothing here needs a vertex buffer
// and the draw cannot fail for a reason that has nothing to do with the attachment.
// u_depth is the NDC z the whole primitive sits at - the depth/stencil case needs two
// different ones.
const char* const kVertexSource = R"(#version 420 core
uniform float u_depth;
void main()
{
vec2 corner = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);
gl_Position = vec4(corner, u_depth, 1.0);
}
)";
// The layer count is baked in as a literal rather than passed as a uniform: a
// non-constant loop bound in a geometry shader is legal but is one more thing the
// ESSL transpile could get wrong, and this scenario is not about that.
std::string MakeGeometrySource(int layerCount) {
return "#version 420 core\n"
"layout(triangles) in;\n"
"layout(triangle_strip, max_vertices = " +
std::to_string(layerCount * 3) +
") out;\n"
"flat out int v_layer;\n"
"void main()\n"
"{\n"
" for (int layer = 0; layer < " +
std::to_string(layerCount) +
"; ++layer) {\n"
" for (int i = 0; i < 3; ++i) {\n"
" gl_Layer = layer;\n"
" v_layer = layer;\n"
" gl_Position = gl_in[i].gl_Position;\n"
" EmitVertex();\n"
" }\n"
" EndPrimitive();\n"
" }\n"
"}\n";
}
const char* const kLayeredFragmentSource = R"(#version 420 core
flat in int v_layer;
uniform int u_pass;
out vec4 o_color;
void main()
{
o_color = vec4(float(10 + v_layer * 20) / 255.0,
float(200 - v_layer * 10) / 255.0,
float(3 + u_pass * 60) / 255.0,
1.0);
}
)";
// The two clear cases do not draw into the layered attachment at all - they SAMPLE it, so
// the queued clear is consumed by MaterializePendingClearForTexture rather than by a render
// pass's LOAD_OP_CLEAR. What the sample returns is irrelevant; being sampled is the point.
const char* const kCubeSampleFragmentSource = R"(#version 420 core
uniform samplerCube u_source;
out vec4 o_color;
void main() { o_color = texture(u_source, vec3(1.0, 0.0, 0.0)); }
)";
const char* const kOneDArraySampleFragmentSource = R"(#version 420 core
uniform sampler1DArray u_source;
out vec4 o_color;
void main() { o_color = texture(u_source, vec2(0.5, 0.0)); }
)";
// The non-layered case has no geometry stage at all - the slice comes from the
// attachment, not from gl_Layer - so it names its layer through a uniform.
const char* const kFlatFragmentSource = R"(#version 420 core
uniform int u_layer;
uniform int u_pass;
out vec4 o_color;
void main()
{
o_color = vec4(float(10 + u_layer * 20) / 255.0,
float(200 - u_layer * 10) / 255.0,
float(3 + u_pass * 60) / 255.0,
1.0);
}
)";
class LayeredAttachmentShapeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
for (const GLuint fbo : m_fbos) glDeleteFramebuffers(1, &fbo);
m_fbos.clear();
for (const GLuint texture : m_textures) glDeleteTextures(1, &texture);
m_textures.clear();
for (const GLuint program : m_programs) glDeleteProgram(program);
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// 0 on a DirectGLES driver without GL_EXT_geometry_shader and on a DirectVulkan
// device without the geometryShader feature. The same probe GeometryDrawModeScenario
// and IoBlockNameCollisionScenario use.
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= kCubeLayerFaces * 3;
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
// geometrySource may be null, which builds the no-geometry-stage program the
// non-layered case uses.
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
std::vector<GLuint> shaders;
const auto compile = [&](GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
ADD_FAILURE() << "stage 0x" << std::hex << stage << std::dec
<< " did not compile: " << InfoLog(shader, true);
return false;
}
return true;
};
bool ok = compile(GL_VERTEX_SHADER, kVertexSource);
if (ok && geometrySource != nullptr) ok = compile(GL_GEOMETRY_SHADER, geometrySource);
if (ok) ok = compile(GL_FRAGMENT_SHADER, fragmentSource);
if (!ok) {
for (const GLuint shader : shaders) glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) glAttachShader(program, shader);
glLinkProgram(program);
for (const GLuint shader : shaders) glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
ADD_FAILURE() << "the program did not link: " << InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
GLuint TrackTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
GLuint TrackFramebuffer() {
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
m_fbos.push_back(fbo);
return fbo;
}
// An RGBA8 3D texture, every texel poisoned.
GLuint MakePoisoned3DColor() {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_3D, texture);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kExtent, kExtent, k3DSlices);
const std::vector<GLubyte> seed(
static_cast<std::size_t>(kExtent) * kExtent * k3DSlices * 4, kPoison);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kExtent, kExtent, k3DSlices, GL_RGBA,
GL_UNSIGNED_BYTE, seed.data());
glBindTexture(GL_TEXTURE_3D, 0);
return texture;
}
// An RGBA8 cube-map array of kCubeLayerFaces layer-faces, every texel poisoned.
GLuint MakePoisonedCubeArrayColor() {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, kExtent, kExtent, kCubeLayerFaces);
const std::vector<GLubyte> seed(
static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces * 4, kPoison);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent, kCubeLayerFaces,
GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
return texture;
}
// A plain RGBA8 CUBE MAP (not an array), every face poisoned. This is the shape whose
// layered attachment records the +X face as its representative upload target, so its
// level size reads z = 1 - the reason a shared layer-count helper is needed at all.
GLuint MakePoisonedCubeMap() {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kExtent * 4, kPoison);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (int face = 0; face < 6; ++face) {
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return texture;
}
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
// gives 1 however many layers there are.
GLuint MakePoisoned1DArray() {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kOneDArrayLayers * 4, kPoison);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kExtent, kOneDArrayLayers, 0, GL_RGBA,
GL_UNSIGNED_BYTE, seed.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
return texture;
}
// A scratch 2D colour target for the sampling draw. It exists only so the draw has
// somewhere to go that is NOT the layered attachment under test - a draw into that
// would open a render pass and consume the pending clear through LOAD_OP_CLEAR, which
// is the other consumer and the one that was already right.
GLuint MakeScratchColorFbo() {
const GLuint scratch = TrackTexture();
glBindTexture(GL_TEXTURE_2D, scratch);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kExtent, kExtent);
glBindTexture(GL_TEXTURE_2D, 0);
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scratch, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
return fbo;
}
// One draw that SAMPLES `texture`, into `intoFbo`. This is what drags the queued clear
// through MaterializePendingClearForTexture (VulkanRenderer's sampled-texture
// pre-pass), which is the consumer that used to write the clear key's layerCount
// straight into a VkImageSubresourceRange.
void DrawSampling(GLuint program, GLuint intoFbo, GLenum textureTarget, GLuint texture) {
glBindFramebuffer(GL_FRAMEBUFFER, intoFbo);
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(textureTarget, texture);
glUseProgram(program);
const GLint sourceLocation = glGetUniformLocation(program, "u_source");
ASSERT_GE(sourceLocation, 0) << "u_source was not reflected";
glUniform1i(sourceLocation, 0);
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
glUniform1f(depthLocation, 0.0f);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindTexture(textureTarget, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// Every texel of `texels` is the clear colour. +/-1 per channel, which no rounding can
// exceed and which cannot be confused with the poison (0xAB) it replaced.
void ExpectAllCleared(const std::vector<Rgba8>& texels, int perTexelStride, const char* what) {
for (std::size_t i = 0; i < texels.size(); ++i) {
const Rgba8& actual = texels[i];
const bool ok = std::abs(static_cast<int>(actual.r) - kClearColor.r) <= 1 &&
std::abs(static_cast<int>(actual.g) - kClearColor.g) <= 1 &&
std::abs(static_cast<int>(actual.b) - kClearColor.b) <= 1;
if (ok) continue;
ADD_FAILURE() << what << ": unit " << (static_cast<int>(i) / perTexelStride) << " texel "
<< (static_cast<int>(i) % perTexelStride) << " is " << Describe(actual)
<< ", expected " << Describe(kClearColor)
<< (actual.r == kPoison && actual.g == kPoison
? " - the poison, so the clear never reached this one"
: "");
// One message per unit is enough to say what happened.
i = (static_cast<std::size_t>(i) / perTexelStride + 1) * perTexelStride - 1;
}
}
// A depth (or packed depth-stencil) cube-map array of the same shape. No upload: a
// depth array is filled by clearing through an attachment, which is the state the
// gating cases start from anyway.
GLuint MakeCubeArrayDepth(GLenum internalFormat) {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent, kCubeLayerFaces);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
return texture;
}
// glGetTexImage rather than a per-layer glReadPixels: a cube-map array has no
// per-layer attachment on every backend, and glGetTexImage is the readback both of
// them answer for whole-level layered targets (LayeredTextureReadbackScenario pins
// that contract). It is a real GPU readback on DirectVulkan - the texture manager
// copies the image into a staging buffer - so a stale CPU shadow cannot pass it.
std::vector<Rgba8> ReadLevel(GLenum target, GLuint texture, int layers) {
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent * layers, Rgba8{});
glBindTexture(target, texture);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glGetTexImage(target, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(target, 0);
return texels;
}
// Every texel of every layer must be that layer's expected colour. Reported per layer
// so a failure names which one, and the poison is called out by name.
void ExpectEveryLayer(const std::vector<Rgba8>& texels, int layers, int pass, const char* what) {
for (int layer = 0; layer < layers; ++layer) {
const Rgba8 expected = ExpectedColor(layer, pass);
for (int y = 0; y < kExtent; ++y) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index =
(static_cast<std::size_t>(layer) * kExtent + y) * kExtent + x;
const Rgba8 actual = texels[index];
if (actual == expected) continue;
ADD_FAILURE()
<< what << ": layer " << layer << " texel (" << x << ", " << y << ") is "
<< Describe(actual) << ", expected " << Describe(expected)
<< (actual.r == kPoison && actual.g == kPoison
? " - the poison, so nothing was ever rendered into this layer"
: "");
// One message per layer is enough to say what happened.
y = kExtent;
break;
}
}
}
}
::testing::AssertionResult FramebufferIsComplete() {
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
}
// One layered pass over the whole attachment.
void DrawLayered(GLuint program, int pass, float depth) {
glUseProgram(program);
const GLint passLocation = glGetUniformLocation(program, "u_pass");
ASSERT_GE(passLocation, 0) << "u_pass was not reflected";
glUniform1i(passLocation, pass);
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
glUniform1f(depthLocation, depth);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
GLuint m_vao = 0;
std::vector<GLuint> m_textures;
std::vector<GLuint> m_fbos;
std::vector<GLuint> m_programs;
};
// (1) A LAYERED GL_TEXTURE_3D colour attachment. Pre-fix this is the null VkImageView:
// the attachment asked for a 3D view, whose [0, 4) layer span was measured against the
// image's arrayLayers == 1 and refused, and VK_NULL_HANDLE went to vkCreateFramebuffer.
TEST_F(LayeredAttachmentShapeScenario, LayeredThreeDColorAttachmentReachesEverySlice) {
if (!Ready()) return;
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
const std::string geometrySource = MakeGeometrySource(k3DSlices);
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
if (program == 0) return;
const GLuint color = MakePoisoned3DColor();
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 3D texture layered failed";
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
ExpectEveryLayer(texels, k3DSlices, /*pass=*/0, "layered GL_TEXTURE_3D colour attachment");
Gl().EndFrame();
}
// (2) The same texture attached ONE SLICE at a time, which is the other half of the same
// view-type decision. Pre-fix a non-layered 3D attachment also asked for a 3D view, so
// the per-slice branch never ran and slice 2 resolved to VK_NULL_HANDLE. Needs no
// geometry stage - the slice comes from the attachment.
TEST_F(LayeredAttachmentShapeScenario, NonLayeredThreeDSliceAttachmentWritesOnlyThatSlice) {
if (!Ready()) return;
const GLuint program = BuildProgram(nullptr, kFlatFragmentSource);
if (program == 0) return;
const GLuint color = MakePoisoned3DColor();
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0, kSubjectSlice);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching slice " << kSubjectSlice << " failed";
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glUseProgram(program);
const GLint layerLocation = glGetUniformLocation(program, "u_layer");
const GLint passLocation = glGetUniformLocation(program, "u_pass");
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
ASSERT_GE(layerLocation, 0);
ASSERT_GE(passLocation, 0);
ASSERT_GE(depthLocation, 0);
glUniform1i(layerLocation, kSubjectSlice);
glUniform1i(passLocation, 0);
glUniform1f(depthLocation, 0.0f);
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(FirstGLError(), 0u) << "the per-slice draw errored";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
const Rgba8 expected = ExpectedColor(kSubjectSlice, 0);
const Rgba8 poison{kPoison, kPoison, kPoison, kPoison};
for (int slice = 0; slice < k3DSlices; ++slice) {
const Rgba8& target = (slice == kSubjectSlice) ? expected : poison;
for (int y = 0; y < kExtent; ++y) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index =
(static_cast<std::size_t>(slice) * kExtent + y) * kExtent + x;
const Rgba8 actual = texels[index];
if (actual == target) continue;
ADD_FAILURE() << "slice " << slice << " texel (" << x << ", " << y << ") is "
<< Describe(actual) << ", expected " << Describe(target)
<< (slice == kSubjectSlice
? " - the attached slice was not the one written"
: " - a slice the attachment did not name was written");
y = kExtent;
break;
}
}
}
Gl().EndFrame();
}
// (3) A LAYERED GL_TEXTURE_CUBE_MAP_ARRAY colour attachment. Pre-fix this is the other
// exit: a valid CUBE_ARRAY view of a type no framebuffer may take, handed straight to
// vkCreateFramebuffer.
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayColorAttachmentReachesEveryLayerFace) {
if (!Ready()) return;
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
const GLuint color = MakePoisonedCubeArrayColor();
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
}
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
if (program == 0) return;
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube-map array layered failed";
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
"layered GL_TEXTURE_CUBE_MAP_ARRAY colour attachment");
Gl().EndFrame();
}
// (4) A layered cube-map-array DEPTH attachment, proved to have covered every layer-face:
//
// pass 0 paints at z = 0 against a depth buffer cleared to 1;
// pass 1 paints at z = +0.5, which GL_LESS must reject.
//
// A layer that reads back pass 1's colour is a layer the depth attachment never covered -
// which is exactly what attaching one layer-face of it, or none, looks like. Runs on both
// backends: this is the cross-backend control for the packed case below.
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthAttachmentGatesEveryLayerFace) {
if (!Ready()) return;
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
const GLuint color = MakePoisonedCubeArrayColor();
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
}
const GLuint depth = MakeCubeArrayDepth(GL_DEPTH_COMPONENT24);
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no depth GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
}
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
if (program == 0) return;
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth pair failed";
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDepthMask(GL_TRUE);
glClearDepth(1.0);
glClear(GL_DEPTH_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth attachment errored";
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f); // farther: GL_LESS must reject it
EXPECT_EQ(FirstGLError(), 0u) << "the two layered draws errored";
glDisable(GL_DEPTH_TEST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
"layered cube-map-array depth attachment (pass 1's colour on a layer means the "
"depth test did not cover it)");
Gl().EndFrame();
}
// (5) The PACKED depth-stencil shape the conformance suite crashes on:
// texture_cube_map_array.stencil_attachments_*_layered attaches a cube-map array as COLOR0
// AND the same-shaped GL_DEPTH24_STENCIL8 array as GL_DEPTH_STENCIL_ATTACHMENT, both
// layered. Both aspects are proved to have covered every layer-face:
//
// pass 0 paints at z = 0 with the stencil op writing 1;
// pass 1 paints at z = +0.5, which the depth test must reject;
// pass 2 paints with the depth test off but a stencil func of EQUAL 0, which the
// stencil written by pass 0 must reject.
//
// The probe in front of the gating is where this scenario earned its keep. The attachment
// point ITSELF was broken: glFramebufferTexture(GL_DEPTH_STENCIL_ATTACHMENT) is a
// shorthand that the front end splits into a depth and a stencil attachment, and the split
// dropped the call's `layered` flag (GL_Framebuffer.cpp,
// AttachFramebufferTextureWithUploadTarget). A layered colour attachment therefore sat
// beside a NON-layered depth/stencil one, and both backends lost the draw entirely - with
// no GL error and glCheckFramebufferStatus answering COMPLETE. DirectVulkan built the
// depth/stencil view with layerCount 1 under a framebuffer declaring 12 layers
// (VUID-VkFramebufferCreateInfo-flags-04535, which the validation layers report on this
// exact case); DirectGLES attached one layer of it beside a layered colour target, which
// the driver answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS. Case (4) above is what
// isolates it to the attachment point: the same cube-map array on GL_DEPTH_ATTACHMENT
// rendered and gated correctly throughout.
//
// So the probe stays, as an assertion rather than as scaffolding: it turns that regression
// back into ONE message about the shape instead of twelve about individual layers.
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthStencilAttachmentGatesEveryLayerFace) {
if (!Ready()) return;
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
const GLuint color = MakePoisonedCubeArrayColor();
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
}
const GLuint depthStencil = MakeCubeArrayDepth(GL_DEPTH24_STENCIL8);
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no depth-stencil GL_TEXTURE_CUBE_MAP_ARRAY on this backend: "
<< GLErrorName(error);
}
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
if (program == 0) return;
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
// The probe: its own colour attachment (so the subject texture keeps its poison), the
// same depth-stencil attachment, and both tests off - so every layer-face must come
// back painted, whatever the gating below then decides.
{
const GLuint probeColor = MakePoisonedCubeArrayColor();
const GLuint probeFbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, probeFbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, probeColor, 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
ASSERT_TRUE(FramebufferIsComplete());
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
DrawLayered(program, /*pass=*/3, /*depth=*/0.0f);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> probeTexels =
ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, probeColor, kCubeLayerFaces);
EXPECT_EQ(FirstGLError(), 0u) << "the probe draw or readback errored";
ExpectEveryLayer(probeTexels, kCubeLayerFaces, /*pass=*/3,
"a layered draw with the depth and stencil tests DISABLED, into a colour + "
"GL_DEPTH_STENCIL_ATTACHMENT cube-map-array pair (all poison means the "
"attachment pair lost the draw outright, which is what a non-layered "
"depth/stencil attachment beside a layered colour one looks like)");
// The gating assertions below can only add noise once the shape itself is broken.
if (::testing::Test::HasNonfatalFailure()) return;
}
const GLuint fbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
ASSERT_TRUE(FramebufferIsComplete());
glDepthMask(GL_TRUE);
glStencilMask(0xFFu);
glClearDepth(1.0);
glClearStencil(0);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth-stencil attachment errored";
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
// Farther than pass 0, so GL_LESS must reject it on every layer.
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f);
// Depth out of the way; only the stencil pass 0 wrote can reject this one.
glDepthFunc(GL_ALWAYS);
glStencilFunc(GL_EQUAL, 0, 0xFFu);
DrawLayered(program, /*pass=*/2, /*depth=*/-0.5f);
EXPECT_EQ(FirstGLError(), 0u) << "the three layered draws errored";
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
"layered cube-map-array depth-stencil attachment (a later pass's colour means "
"the depth or stencil test did not cover that layer)");
Gl().EndFrame();
}
// (6) and (7) leave the render pass alone entirely and pin the OTHER consumer of a layered
// attachment's layer count.
//
// A glClear on a texture-backed FBO with the scissor test off is not executed on the spot:
// it is queued (VkClearManager), and then exactly one of two things consumes it - the next
// render pass's LOAD_OP_CLEAR over the attachment view, or MaterializePendingClearForTexture
// if the texture is used outside a pass first (sampled, blitted, copied, read back). The
// second path writes the queued key's layerCount straight into a VkImageSubresourceRange
// and then POPS the entry, so whatever it misses is lost for good - the render pass never
// gets a second chance at it.
//
// Both consumers must therefore agree about how many layers a layered attachment spans, and
// they are now literally the same function (ResolveAttachmentLayerCount, VkTextureManager.h).
// These two cases are the shapes where a raw `size.z()` and the real answer differ, and
// neither is reachable through the cases above: a cube MAP records the +X face as its
// representative upload target (z = 1, six real faces) and a 1D ARRAY keeps its layer count
// in the state-side height (z = 1, N real layers). The cube-map-ARRAY and 3D shapes the
// earlier cases use both carry their count in z, so they agree either way and cannot see it.
//
// The draw goes into a scratch 2D target, never into the layered attachment, so the
// materialise path is the only consumer that can fire.
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapClearMaterialisedBySamplingReachesEveryFace) {
if (!Ready()) return;
const GLuint program = BuildProgram(nullptr, kCubeSampleFragmentSource);
if (program == 0) return;
const GLuint cube = MakePoisonedCubeMap();
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
const GLuint layeredFbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cube, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube map layered failed";
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered cube-map attachment errored";
// Consume the queued clear through the sampled-texture path, with no draw into the
// layered FBO in between.
const GLuint scratchFbo = MakeScratchColorFbo();
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
DrawSampling(program, scratchFbo, GL_TEXTURE_CUBE_MAP, cube);
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
// Every face, read back through an FBO that names THAT face.
//
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
// where only +X had been cleared, that spelling returned the cleared colour for all
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
// else, and the pending clear is long gone by now (materialised and popped above), so
// this readback cannot alter what it is measuring.
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
for (int face = 0; face < 6; ++face) {
const GLuint faceFbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
glReadBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
EXPECT_EQ(FirstGLError(), 0u) << "reading cube face " << kFaceNames[face] << " back errored";
ExpectAllCleared(texels, kExtent * kExtent,
(std::string("layered GL_TEXTURE_CUBE_MAP glClear materialised by sampling, "
"face ") +
kFaceNames[face])
.c_str());
}
Gl().EndFrame();
}
// The 1D-array half of the same divergence. Pre-existing rather than introduced by this
// branch (the clear copy never had ToVulkanLevelExtent), and fixed by the same hoist.
TEST_F(LayeredAttachmentShapeScenario, LayeredOneDArrayClearMaterialisedBySamplingReachesEveryLayer) {
if (!Ready()) return;
const GLuint program = BuildProgram(nullptr, kOneDArraySampleFragmentSource);
if (program == 0) return;
const GLuint array = MakePoisoned1DArray();
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "no usable GL_TEXTURE_1D_ARRAY on this backend: " << GLErrorName(error);
}
const GLuint layeredFbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, array, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 1D array layered failed";
ASSERT_TRUE(FramebufferIsComplete());
// The viewport is the LEVEL's shape: a 1D array level is `kExtent` wide and one row
// tall, whatever its layer count.
glViewport(0, 0, kExtent, 1);
glDisable(GL_SCISSOR_TEST);
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered 1D-array attachment errored";
const GLuint scratchFbo = MakeScratchColorFbo();
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
DrawSampling(program, scratchFbo, GL_TEXTURE_1D_ARRAY, array);
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
// GL hands a 1D array back as a two-dimensional image whose ROWS are the layers.
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kOneDArrayLayers, Rgba8{});
glBindTexture(GL_TEXTURE_1D_ARRAY, array);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
EXPECT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
ExpectAllCleared(texels, kExtent,
"layered GL_TEXTURE_1D_ARRAY glClear materialised by sampling (unit = layer)");
Gl().EndFrame();
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,404 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitiveRestartScenario.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
//
// Scenario - DESKTOP GL_PRIMITIVE_RESTART WITH AN APPLICATION-CHOSEN INDEX.
//
// Desktop GL restarts on whatever glPrimitiveRestartIndex named; GLES and Vulkan both restart
// only on the all-ones value of the index type. DirectGLES used to THROW_EXCEPTION on the
// mismatch, and a throw out of a GL entry point unwinds a C++ exception through the C ABI and
// kills the process - which is how KHR-GL4x.geometry_shader.primitive_counter.*_rp took the whole
// conformance runner down, nine bodies at a time, losing every result in the chunk with it.
//
// So the first thing this asserts is simply that the process is still here. The second is that
// the restart actually happened: the substitution rewrites the index data so the driver restarts
// where the application asked, and the difference between "restart honoured" and "restart
// silently dropped" is a triangle strip that welds its two halves together across the gap.
//
// Needs a real context on purpose. The GPU-free suite cannot reach a backend at all, and this is
// entirely about what the backend does with the index buffer.
#include <cstddef>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr GLsizei kSurface = 64;
const char* const kVertexSource = R"(#version 420 core
layout(location = 0) in vec2 a_position;
void main()
{
gl_Position = vec4(a_position, 0.0, 1.0);
}
)";
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
// Two triangles with a gap down the middle, plus two spare vertices parked at the origin.
//
// The spares exist so the restart index is a LEGAL vertex index: if the restart were
// dropped the driver would still fetch a real vertex rather than read out of bounds, so
// the negative case is defined behaviour and the test measures the restart rather than
// whatever robust-buffer-access does.
constexpr GLfloat kVertices[] = {
-0.9f, -0.9f, // 0 - left triangle
-0.1f, -0.9f, // 1
-0.9f, 0.9f, // 2
0.1f, -0.9f, // 3 - right triangle
0.9f, -0.9f, // 4
0.9f, 0.9f, // 5
0.0f, 0.0f, // 6 - spare
0.0f, 0.0f, // 7 - spare, and the application's restart index
};
constexpr GLuint kRestartIndex = 7;
// A triangle STRIP, restarted in the middle: honoured, it is exactly the two triangles
// above. Dropped, the strip welds vertices 2, 7 and 3 into extra triangles that spill
// across the gap - which is what the middle probe below catches.
constexpr GLuint kIndices[] = {0, 1, 2, kRestartIndex, 3, 4, 5};
struct Pixel {
GLubyte r = 0, g = 0, b = 0, a = 0;
};
class PrimitiveRestartScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
glEnableVertexAttribArray(0);
glGenBuffers(1, &m_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
glGenTextures(1, &m_colorTexture);
glBindTexture(GL_TEXTURE_2D, m_colorTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSurface, kSurface);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTexture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSurface, kSurface);
m_program = BuildProgram();
ASSERT_NE(m_program, 0u) << "the flat-colour program did not build: " << m_buildLog;
glUseProgram(m_program);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glDisable(GL_PRIMITIVE_RESTART);
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
glPrimitiveRestartIndex(0);
glUseProgram(0);
if (m_program != 0) glDeleteProgram(m_program);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_colorTexture != 0) glDeleteTextures(1, &m_colorTexture);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
GLuint BuildProgram() {
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &kVertexSource, nullptr);
glCompileShader(vs);
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &kFragmentSource, nullptr);
glCompileShader(fs);
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(vs);
glDeleteShader(fs);
if (!linked) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
m_buildLog = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
// The whole surface, so a failure can report the three probes together rather than
// three separate readbacks that might disagree about which draw they saw.
std::vector<Pixel> DrawAndRead() {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(kIndices)), GL_UNSIGNED_INT,
nullptr);
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
return pixels;
}
static const Pixel& At(const std::vector<Pixel>& pixels, int x, int y) {
return pixels[static_cast<std::size_t>(y) * kSurface + x];
}
static bool IsGreen(const Pixel& p) { return p.g > 128 && p.r < 128; }
// NDC (-0.5, -0.5): well inside the left triangle whichever way the restart went.
static constexpr int kLeftX = 16, kLeftY = 16;
// NDC (0.6, -0.5): well inside the right triangle, and outside every welded one.
static constexpr int kRightX = 51, kRightY = 16;
// NDC (0.2, -0.5): in the gap between the two triangles, and INSIDE the triangle the
// strip welds out of vertices 7, 3 and 4 when the restart is dropped. This is the
// probe that distinguishes a working restart from a silently ignored one.
static constexpr int kGapX = 38, kGapY = 16;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_ebo = 0;
GLuint m_fbo = 0;
GLuint m_colorTexture = 0;
GLuint m_program = 0;
std::string m_buildLog;
};
// THE crash regression. Before the fix this call never returned: DirectGLES threw
// std::runtime_error out of glDrawElements and the process died on the spot. Reaching the
// assertion at all is most of the point.
TEST_F(PrimitiveRestartScenario, AnArbitraryRestartIndexDrawsInsteadOfKillingTheProcess) {
if (!Ready()) GTEST_SKIP();
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const std::vector<Pixel> pixels = DrawAndRead();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "an arbitrary restart index is legal desktop GL and must raise no error";
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
<< "the gap between the two halves is covered, so the restart was dropped and the "
"strip welded across it";
}
// The other half of the state: an application that sets the restart index TO the fixed
// all-ones value needs no rewriting at all, and the cap must map straight onto the
// driver's own fixed-index restart. Same picture, different path through the backend.
TEST_F(PrimitiveRestartScenario, TheFixedIndexValueTakesTheForwardingPath) {
if (!Ready()) GTEST_SKIP();
// Index 0xFFFFFFFF is not a vertex this draw uses, so the strip is the same shape.
const GLuint fixedIndices[] = {0, 1, 2, 0xFFFFFFFFu, 3, 4, 5};
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(fixedIndices), fixedIndices);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(0xFFFFFFFFu);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const std::vector<Pixel> pixels = DrawAndRead();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY)));
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)));
// Put the buffer back for whatever runs next in this fixture.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(kIndices), kIndices);
DrainErrors();
}
// With the cap off, the same index data is just data - nothing restarts, and the strip
// welds across the gap. The negative control for the probe above: without it, a backend
// that lost the whole draw would pass the test by rendering nothing in the gap.
TEST_F(PrimitiveRestartScenario, WithoutTheCapTheStripWeldsAcrossTheGap) {
if (!Ready()) GTEST_SKIP();
glDisable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const std::vector<Pixel> pixels = DrawAndRead();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the draw itself must still happen";
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
<< "with restart disabled the strip is continuous, so the gap must be covered - if "
"it is not, the probe above proves nothing";
}
// A second draw with a DIFFERENT restart index has to be rewritten again. The substitution
// stages through one scratch buffer, so a cached or half-restored element-array binding
// would show up here as the second draw reusing the first one's data.
TEST_F(PrimitiveRestartScenario, ChangingTheRestartIndexBetweenDrawsIsHonoured) {
if (!Ready()) GTEST_SKIP();
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
const std::vector<Pixel> restarted = DrawAndRead();
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_FALSE(IsGreen(At(restarted, kGapX, kGapY)));
// 6 is the other spare vertex, and it appears nowhere in the index data - so nothing
// restarts and the strip is continuous again, from the very same buffer.
glPrimitiveRestartIndex(6);
const std::vector<Pixel> notRestarted = DrawAndRead();
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_TRUE(IsGreen(At(notRestarted, kLeftX, kLeftY)));
EXPECT_TRUE(IsGreen(At(notRestarted, kGapX, kGapY)))
<< "the second draw restarted on an index that is not in its data";
}
// A NON-indexed draw has no index stream, so GL primitive restart cannot affect it - and a
// list topology is the shape DirectVulkan has to refuse when the device lacks
// VK_EXT_primitive_topology_list_restart. Deriving the pipeline's primitiveRestartEnable
// from the capability bits alone conflated the two: an application that enables
// GL_PRIMITIVE_RESTART once at init and then draws its UI with glDrawArrays(GL_TRIANGLES)
// had every one of those draws silently dropped on such a device.
TEST_F(PrimitiveRestartScenario, ANonIndexedListTopologyDrawIsUnaffectedByTheCap) {
if (!Ready()) GTEST_SKIP();
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Vertices 0,1,2 are the left triangle; GL_TRIANGLES is a list topology.
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)))
<< "primitive restart has no meaning for glDrawArrays, so the draw must render "
"normally whatever the device supports";
DrainErrors();
}
// GL 4.6 core 10.3.6 compares the fetched index, zero-extended, against the full 32-bit
// PRIMITIVE_RESTART_INDEX. A restart index the index type cannot hold therefore matches
// nothing and the draw restarts NOWHERE - it does not restart on the truncated value, and
// it does not restart on the type's all-ones value either, which is what the driver's own
// fixed-index restart would have done if it had been left enabled.
TEST_F(PrimitiveRestartScenario, ARestartIndexTooLargeForTheIndexTypeRestartsNowhere) {
if (!Ready()) GTEST_SKIP();
// 16-bit indices with a restart index of 0x10007: the low half (7) IS a real index in
// the data, so a truncating comparison would split the strip exactly where a correct
// one leaves it whole.
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5};
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(0x10000u + kRestartIndex);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(shortIndices)), GL_UNSIGNED_SHORT,
nullptr);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
<< "no 16-bit index can equal 0x10007, so nothing restarts and the strip is "
"continuous - truncating the restart index to 7 would split it here";
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
DrainErrors();
}
// The all-ones value of an index type is an ordinary vertex index whenever the array uses
// the type's full range, which is exactly why an application picks an arbitrary restart
// index in the first place. Substituting the sentinel in place would either steal that
// vertex or spuriously restart on it, so the copy widens instead - and the draw has to be
// issued with the widened type, which is the part that is easy to forget.
TEST_F(PrimitiveRestartScenario, AnAllOnesVertexIndexSurvivesTheSubstitution) {
if (!Ready()) GTEST_SKIP();
// The buffer carries the 16-bit all-ones value as an ordinary element. It sits past
// the seven indices this draw reads, because the vertex array has only eight entries
// and fetching index 65535 would be out of range - what is under test is that its
// mere PRESENCE forces the widened copy, and that the draw still finds its own
// indices at the right offsets in a copy whose element width has changed underneath
// it. Narrowly substituting in place instead would rewrite this element to 0xFFFE.
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5, 0xFFFFu};
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Only the first seven indices are drawn, so the 0xFFFF element is never fetched - what
// is under test is that its PRESENCE does not break the substitution or the offsets.
glDrawElements(GL_TRIANGLE_STRIP, 7, GL_UNSIGNED_SHORT, nullptr);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
<< "the restart still has to happen once the copy has been widened";
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
DrainErrors();
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,227 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RenderbufferBlendFormatScenario.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
//
// Scenario - BLENDING WORKS ON A RENDERBUFFER WHOSE GL FORMAT HAS NO EXACT VkFormat.
//
// DirectVulkan force-disables blending on an attachment whose VkFormat lacks
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT, which is the right thing to do - blending on such a
// format is invalid pipeline state. The probe has to ask about the format the attachment ACTUALLY
// has, and for renderbuffers it asked a different question from the one that created the image: the
// image comes from ResolveTextureFormatInfo (which widens GL formats with no Vulkan twin onto a real
// one) while the probe used the strict 1:1 converter, which answers VK_FORMAT_UNDEFINED for RGBA2,
// RGBA12, RGB10, RGB12, RGB16 and the three-channel formats, and the 16-bit packed formats for RGBA4
// and RGB5_A1.
//
// VkFormatProperties for VK_FORMAT_UNDEFINED are all zero, so the probe concluded "not blendable"
// and every pipeline for that attachment was built with blendEnable = VK_FALSE - permanently, and
// silently apart from one log line. The source colour then overwrites the destination instead of
// blending with it, which is a wrong PICTURE, not a wrong error code.
//
// GL_RGB8 is the ordinary shape and is what this scenario leads with: it is a required
// colour-renderable format, its image has been R8G8B8A8_UNORM all along, and the probe asked about
// the 24-bit R8G8B8_UNORM that most drivers do not support at all. GL_RGBA4 covers the other half -
// a format whose probe answered a real-but-different VkFormat.
//
// DirectGLES is the control: it forwards the renderbuffer to the ES driver and blends whatever the
// driver blends, so a disagreement between the two backends is the defect.
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 16;
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
constexpr const char* kFragmentSource = R"(#version 330 core
uniform vec4 uColor;
out vec4 fragColor;
void main()
{
fragColor = uColor;
}
)";
class RenderbufferBlendFormatScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
std::string error;
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
ASSERT_NE(m_program, 0u) << "program did not build: " << error;
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
Destroy();
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Destroy() {
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
if (m_renderbuffer != 0) {
glDeleteRenderbuffers(1, &m_renderbuffer);
m_renderbuffer = 0;
}
}
// Returns false (having skipped, not failed) when the driver will not give us a complete
// framebuffer for this format - GL only requires a subset of formats to be
// colour-renderable, and the point of the scenario is blending, not format support.
bool MakeTarget(GLenum internalFormat) {
Destroy();
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kExtent, kExtent);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
return status == GL_FRAMEBUFFER_COMPLETE;
}
void DrawColor(float r, float g, float b, float a) {
glUseProgram(m_program);
glUniform4f(glGetUniformLocation(m_program, "uColor"), r, g, b, a);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
unsigned int m_program = 0;
};
// One draw of opaque black, then a 50%-alpha white draw over it with the ordinary
// SRC_ALPHA / ONE_MINUS_SRC_ALPHA function. Blending gives mid-grey; a pipeline built with
// blendEnable = VK_FALSE gives white, because the source simply overwrites.
//
// The tolerance is wide on purpose: RGBA4 has four bits per channel, so "mid-grey" is one of
// a handful of representable values and the test must not become a quantisation test.
void ExpectBlendedRatherThanOverwritten(const char* what) {
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty()) << what;
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
EXPECT_GT(int(centre.r), 40) << what << ": got " << centre << ", which is darker than a blend of "
"black and 50% white";
EXPECT_LT(int(centre.r), 215) << what << ": got " << centre
<< ", which is the source colour - blending was disabled";
}
} // namespace
// The ordinary case, and the one broken today rather than only after the format table was
// unified: a three-channel colour renderbuffer. Its image has been R8G8B8A8_UNORM all along while
// the blend probe asked about R8G8B8_UNORM, which most drivers do not support at all.
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAThreeChannelRenderbuffer) {
if (!Ready()) GTEST_SKIP();
if (!MakeTarget(GL_RGB8)) GTEST_SKIP() << "GL_RGB8 renderbuffer is not framebuffer-complete here";
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
ExpectBlendedRatherThanOverwritten("GL_RGB8");
Gl().EndFrame();
}
// The other half: a format whose strict converter answers a real-but-different VkFormat
// (R4G4B4A4_UNORM_PACK16) while the image is R8G8B8A8_UNORM. Blend support for the packed 16-bit
// formats is optional in Vulkan, so the probe could legitimately answer "no" for a format the
// attachment does not have.
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnALowBitPackedRenderbuffer) {
if (!Ready()) GTEST_SKIP();
if (!MakeTarget(GL_RGBA4)) GTEST_SKIP() << "GL_RGBA4 renderbuffer is not framebuffer-complete here";
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
ExpectBlendedRatherThanOverwritten("GL_RGBA4");
Gl().EndFrame();
}
// The control that keeps both of the above honest: the same sequence on the format whose probe
// and image always agreed. If this one ever fails, the scenario is measuring the blend setup
// rather than the format resolution.
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAnRgba8Renderbuffer) {
if (!Ready()) GTEST_SKIP();
if (!MakeTarget(GL_RGBA8)) GTEST_SKIP() << "GL_RGBA8 renderbuffer is not framebuffer-complete here";
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
ExpectBlendedRatherThanOverwritten("GL_RGBA8");
Gl().EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,274 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleVariablesScenario.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
//
// Scenario - gl_NumSamples REACHES THE SHADER, AND IT FOLLOWS THE DRAW FRAMEBUFFER.
//
// glslang declares gl_NumSamples only when it is NOT targeting SPIR-V - both the desktop and the
// ES branch of Initialize.cpp wrap `uniform int gl_NumSamples;` in `if (spvVersion.spv == 0)`,
// because SPIR-V has no NumSamples builtin to lower it to - and MobileGL always targets SPIR-V.
// Every fragment shader that read the built-in therefore died at COMPILE time with
// "'gl_NumSamples' : undeclared identifier", which is all 144 KHR-GL46.sample_variables.mask.*
// bodies plus their es_31_compatibility twins.
//
// The source pipeline now lowers it onto a reserved default-block uniform and the draw path writes
// the current draw framebuffer's sample count into it. Two claims, and the second is the one a
// compile-only test cannot make: the value must be the DRAW FRAMEBUFFER's, so one program drawn
// into a multisample target and then into a single-sample target has to report both counts. A
// link-time bake would pass the first assertion and fail the second, which is exactly why the
// write lives per draw.
//
// llvmpipe and lavapipe both offer 4x multisample RGBA8, so this runs for real in CI rather than
// skipping; the skips below are for a driver that offers no multisample renderbuffer at all.
#include <algorithm>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kVS = R"(#version 400 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
// gl_NumSamples scaled so each count lands on its own well-separated 8-bit value: 1 -> 16,
// 2 -> 32, 4 -> 64. Every sample of the fragment gets the same colour, so the resolve blit
// averages identical values and the readback is exact rather than approximate.
constexpr const char* kFS = R"(#version 400 core
out vec4 o_color;
void main() { o_color = vec4(float(gl_NumSamples) * (16.0 / 255.0), 0.0, 0.0, 1.0); }
)";
class SampleVariablesScenario : public ScenarioTest {};
void DrawFullViewportQuad(unsigned int program) {
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
}
} // namespace
TEST_F(SampleVariablesScenario, GlNumSamplesFollowsTheDrawFramebuffersSampleCount) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 8);
ASSERT_GE(height, 8);
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
// The compile failure this scenario exists for lands here, with glslang's own text.
ASSERT_NE(program, 0u) << error;
GLint maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
if (requestedSamples < 2) {
glDeleteProgram(program);
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
}
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// ---- multisample target ----
GLuint msFbo = 0, msRbo = 0;
glGenFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glGenRenderbuffers(1, &msRbo);
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteProgram(program);
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
}
// What the driver actually allocated - a request is a lower bound, and the shader has to
// agree with the query rather than with what was asked for.
GLint realizedSamples = 0;
glGetIntegerv(GL_SAMPLES, &realizedSamples);
ASSERT_GE(realizedSamples, 2) << "the multisample framebuffer reports GL_SAMPLES " << realizedSamples;
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawFullViewportQuad(program);
EXPECT_EQ(FirstGLError(), 0u);
// Resolve into the default framebuffer to read it back.
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
EXPECT_EQ(FirstGLError(), 0u);
{
const Image resolved = ReadPixels(width, height);
const Rgba8 centre = resolved.At(width / 2, height / 2);
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
<< "gl_NumSamples read " << (centre.r / 16.0) << " into a " << realizedSamples
<< "-sample framebuffer; 1 means the reserved uniform was never written, 0 means it was "
<< "written but never uploaded";
}
gl.EndFrame();
// ---- the SAME program into a single-sample target ----
// A link-time bake of the sample count would keep reporting the multisample value here.
GLuint ssFbo = 0, ssRbo = 0;
glGenFramebuffers(1, &ssFbo);
glBindFramebuffer(GL_FRAMEBUFFER, ssFbo);
glGenRenderbuffers(1, &ssRbo);
glBindRenderbuffer(GL_RENDERBUFFER, ssRbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ssRbo);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawFullViewportQuad(program);
EXPECT_EQ(FirstGLError(), 0u);
{
const Image single = ReadPixels(width, height);
const Rgba8 centre = single.At(width / 2, height / 2);
// GL 4.6 core 15.2.2: gl_NumSamples is ONE for a non-multisample framebuffer, where
// glGetIntegerv(GL_SAMPLES) answers zero.
EXPECT_NEAR(centre.r, 16, 2)
<< "gl_NumSamples read " << (centre.r / 16.0)
<< " into a single-sample framebuffer; the value is a property of the DRAW FRAMEBUFFER, "
<< "so re-using the program must re-write it";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteRenderbuffers(1, &ssRbo);
glDeleteFramebuffers(1, &ssFbo);
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteProgram(program);
gl.EndFrame();
}
// ARB_sample_shading is advertised, and until now glMinSampleShading was a logging no-op while
// glEnable(GL_SAMPLE_SHADING) fell out of RenderState::SetCapability's default arm - so an
// application could ask for a shading rate and get silence from both halves.
//
// What this can and cannot assert. The RATE itself is not observable from a portable shader:
// GL 4.6 core 14.3.1 makes any use of gl_SampleID or gl_SamplePosition force per-sample
// evaluation on its own, so the very built-ins that would report the rate defeat the
// measurement. What IS worth pinning is that the state now reaches both backends without
// damage: DirectGLES forwards glEnable(GL_SAMPLE_SHADING) + glMinSampleShading to the ES
// driver (and must not, on a driver that has neither, push an INVALID_ENUM into the
// application's error queue), and DirectVulkan bakes sampleShadingEnable/minSampleShading into
// a NEW pipeline - which it may only do with the device's sampleRateShading feature enabled.
TEST_F(SampleVariablesScenario, SampleShadingStateReachesTheBackendWithoutDisturbingTheDraw) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
GLint maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
if (requestedSamples < 2) {
glDeleteProgram(program);
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; sample shading needs a multisample target";
}
GLuint msFbo = 0, msRbo = 0;
glGenFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glGenRenderbuffers(1, &msRbo);
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteProgram(program);
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
}
GLint realizedSamples = 0;
glGetIntegerv(GL_SAMPLES, &realizedSamples);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glViewport(0, 0, width, height);
glEnable(GL_SAMPLE_SHADING);
glMinSampleShading(1.0f);
EXPECT_EQ(glIsEnabled(GL_SAMPLE_SHADING), static_cast<GLboolean>(GL_TRUE));
GLfloat rate = -1.0f;
glGetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &rate);
EXPECT_FLOAT_EQ(rate, 1.0f);
EXPECT_EQ(FirstGLError(), 0u) << "enabling sample shading raised a GL error";
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawFullViewportQuad(program);
EXPECT_EQ(FirstGLError(), 0u) << "the sample-shading draw raised a GL error";
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const Image resolved = ReadPixels(width, height);
const Rgba8 centre = resolved.At(width / 2, height / 2);
// The rate changes how OFTEN the shader runs, never what it computes - so the same
// gl_NumSamples reading has to come back.
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
<< "the draw changed its result once sample shading was enabled";
glMinSampleShading(0.0f);
glDisable(GL_SAMPLE_SHADING);
EXPECT_EQ(FirstGLError(), 0u);
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteProgram(program);
gl.EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,339 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.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
//
// Scenario - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END.
//
// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and
// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments,
// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM.
// Every gl_spirv conformance body died on the first of those two calls.
//
// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two
// modules that glslang compiled ahead of time (embedded below as words, so the test depends on
// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel
// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe.
//
// The two specialization constants are the load-bearing part. The vertex module scales its
// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant
// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a
// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of
// green), and BOTH would show up in the readback. A "specialization" that merely stored the
// values without folding them in is exactly the failure mode this shape is built to catch.
//
// The GLSL the modules came from:
// vertex: layout(location = 0) in vec2 aPos;
// layout(constant_id = 3) const float uScale = 1.0;
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
// fragment: layout(location = 0) out vec4 oColor;
// layout(constant_id = 7) const int uChannel = 0;
// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; }
// compiled with `glslangValidator -G --target-env opengl`.
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V
#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551
#endif
#ifndef GL_SPIR_V_BINARY
#define GL_SPIR_V_BINARY 0x9552
#endif
namespace MGITest {
namespace {
class SpirvShaderBinaryScenario : public ScenarioTest {};
// 255 words
const unsigned int kVertexModule[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
};
// 134 words
const unsigned int kFragmentModule[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u,
0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u,
0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u,
0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u,
0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au,
0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u,
0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u,
0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u,
0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u,
0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u,
};
// The quad the vertex module transforms. Full-viewport before the scale, so a scale of
// 0.5 covers exactly the middle half of each axis and the corners stay background.
const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
// The specialization constant ids the two modules declare.
constexpr unsigned int kScaleConstantId = 3;
constexpr unsigned int kChannelConstantId = 7;
unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount,
unsigned int constantId, unsigned int constantValue, std::string* outLog) {
const GLuint shader = glCreateShader(type);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words,
static_cast<GLsizei>(wordCount * sizeof(unsigned int)));
if (glGetError() != GL_NO_ERROR) {
if (outLog) *outLog = "glShaderBinary rejected the module";
glDeleteShader(shader);
return 0;
}
GLint isSpirv = GL_FALSE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) {
if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary";
glDeleteShader(shader);
return 0;
}
glSpecializeShader(shader, "main", 1, &constantId, &constantValue);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
if (outLog) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 0 ? length : 1), '\0');
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
*outLog = std::string(log.data());
}
glDeleteShader(shader);
return 0;
}
return shader;
}
} // namespace
TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) {
if (!Ready()) return;
GLint formatCount = -1;
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format";
std::vector<GLint> formats(static_cast<size_t>(formatCount), 0);
glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
<< "the count and the list have to describe the same thing";
}
TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
// 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but
// GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently.
glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule));
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_ENUM));
GLint isSpirv = GL_TRUE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(FirstGLError(), 0u);
glCompileShader(shader);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION))
<< "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader";
// glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with
// it - the transition the conformance suite checks explicitly.
const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n";
glShaderSource(shader, 1, &source, nullptr);
ASSERT_EQ(FirstGLError(), 0u);
GLint isSpirv = GL_TRUE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_FALSE);
glCompileShader(shader);
EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, SpecializeShaderErrorSurfaceMatchesTheExtension) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(FirstGLError(), 0u);
// 4242 is not one of the module's constant ids. ARB_gl_spirv enumerates that as
// GL_INVALID_VALUE, and an erroring GL command has no other effect - so the shader is left
// untouched rather than pushed into a failed-compile state.
const unsigned int badId = 4242;
const unsigned int value = 0;
glSpecializeShader(shader, "main", 1, &badId, &value);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
// Same for an entry point the module does not carry.
glSpecializeShader(shader, "notMain", 0, nullptr, nullptr);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
// Neither refusal specialized the shader, so a well-formed call still works.
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
EXPECT_EQ(FirstGLError(), 0u);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE);
// But a SECOND specialization of a shader that HAS been specialized is INVALID_OPERATION
// until glShaderBinary re-associates the module.
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
EXPECT_EQ(FirstGLError(), 0u) << "re-associating the module makes specialization legal again";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 16);
ASSERT_GE(height, 16);
std::string log;
// Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies.
unsigned int halfBits = 0;
const float half = 0.5f;
std::memcpy(&halfBits, &half, sizeof(halfBits));
const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule,
sizeof(kVertexModule) / sizeof(kVertexModule[0]),
kScaleConstantId, halfBits, &log);
ASSERT_NE(vs, 0u) << "vertex: " << log;
// Channel 1 is green; the module's own default is 0 (red), so a specialization that did
// nothing paints the wrong colour.
const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule,
sizeof(kFragmentModule) / sizeof(kFragmentModule[0]),
kChannelConstantId, 1u, &log);
ASSERT_NE(fs, 0u) << "fragment: " << log;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> programLog(static_cast<size_t>(length > 0 ? length : 1), '\0');
glGetProgramInfoLog(program, static_cast<GLsizei>(programLog.size()), nullptr, programLog.data());
FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data();
}
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u);
const Image painted = ReadPixels(width, height);
const Rgba8 centre = painted.At(width / 2, height / 2);
EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied";
EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted";
// A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the
// clear colour - which is what proves constant id 3 reached the vertex module. At the
// default scale of 1.0 the quad covers the whole viewport and this pixel would be green.
const Rgba8 corner = painted.At(1, 1);
EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied";
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
gl.EndFrame();
}
} // namespace MGITest
@@ -19,7 +19,14 @@ namespace MobileGL::MG_State::GLState {
BufferTarget::DrawIndirect, BufferTarget::Parameter, BufferTarget::ShaderStorage); BufferTarget::DrawIndirect, BufferTarget::Parameter, BufferTarget::ShaderStorage);
constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback, constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback,
BufferTarget::AtomicCounter, BufferTarget::ShaderStorage); BufferTarget::AtomicCounter, BufferTarget::ShaderStorage);
constexpr SizeT BufferBindingPointCount = 36; // How many indexed binding points each of BufferBindPointTargets gets. 84 is the GL 4.5 core
// minimum for GL_MAX_UNIFORM_BUFFER_BINDINGS (table 23.64) and this array is the capacity
// that limit is clamped against - at 36 the clamp in GL_Getter was degenerate (lo == hi) and
// no application could ever be told about, or bind to, a binding point past the 36th. The
// other three targets advertise their own, smaller ceilings out of
// GetIndexedBufferQueryPointCount, so widening this does not widen what they promise; it only
// costs the unused tail of three arrays.
constexpr SizeT BufferBindingPointCount = 84;
class BufferState { class BufferState {
public: public:
+98
View File
@@ -650,6 +650,12 @@ namespace MobileGL::MG_State {
// a graphics program carrying a compute module, which Adreno 830 does not reject // a graphics program carrying a compute module, which Adreno 830 does not reject
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it. // from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
Bool anyStage = false; Bool anyStage = false;
// Which stages the composite ACTUALLY got a shader for. Not the same question as
// "which stages have a stage program bound": one program bound with
// GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
// stages it was linked with. The transform-feedback capture stage is chosen off this,
// because it has to be the stage that will exist in the composite's own link.
Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) { for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage)); const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue; if (!stageProgram) continue;
@@ -665,9 +671,57 @@ namespace MobileGL::MG_State {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue; if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref); composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true; anyStage = true;
compositeHasStage[stage] = true;
} }
} }
if (!anyStage) return nullProgram; if (!anyStage) return nullProgram;
// Transform feedback captures the output of the LAST vertex-processing stage
// (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
// only the stage program carrying that stage can have been given. The composite is
// assembled out of the stage programs' shaders and inherits none of their
// GL-thread-owned state, so without this it links with an empty capture list and
// glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
// no transform feedback varyings") even though glValidateProgramPipeline had passed.
//
// TWO RULES, both easy to get subtly wrong and both load-bearing:
//
// (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
// take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
// deliberately bumps no version - so a request written after the stage program's
// last link is invisible to the composite cache's signature yet would be picked up
// by the next rebuild, making the capture list depend on whether some unrelated
// event happened to invalidate the cache. Worse, a name that is not an output of
// the capture stage fails the composite's OWN link, and a failed composite makes
// every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
// snapshot removes the whole class: linked state only moves at a link, and a link
// is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
// the existing cache key is sufficient by construction.
// GetTransformFeedbackInterfaceNames() is the right accessor rather than the
// resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
// (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
//
// (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
// the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
// getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
// WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
// Skipping a geometry stage that has no capture list and installing the vertex
// stage's instead made them disagree, and the composite then resolved a vertex
// program's names against the geometry intermediate - capturing where GL says it
// must not, or failing the link and killing every draw. A capture stage with an
// empty list is not a reason to look further down: it is the answer, and
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
if (!captureProgram) continue;
const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
if (!linkedNames.empty()) {
composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
captureProgram->GetTransformFeedbackBufferMode());
}
break;
}
// A pipeline with no fragment stage still rasterises, so the default fragment // A pipeline with no fragment stage still rasterises, so the default fragment
// shader is wanted here even though the separable stage programs never get one. // shader is wanted here even though the separable stage programs never get one.
composite->Link(true); composite->Link(true);
@@ -812,6 +866,22 @@ namespace MobileGL::MG_State {
m_renderState.SetPatchVertices(vertices); m_renderState.SetPatchVertices(vertices);
} }
void GLContext::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
m_renderState.SetPatchDefaultOuterLevel(levels);
}
const FloatVec4& GLContext::GetPatchDefaultOuterLevel() const {
return m_renderState.GetPatchDefaultOuterLevel();
}
void GLContext::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
m_renderState.SetPatchDefaultInnerLevel(levels);
}
const FloatVec2& GLContext::GetPatchDefaultInnerLevel() const {
return m_renderState.GetPatchDefaultInnerLevel();
}
Uint GLContext::GetPatchVertices() const { Uint GLContext::GetPatchVertices() const {
return m_renderState.GetPatchVertices(); return m_renderState.GetPatchVertices();
} }
@@ -832,6 +902,26 @@ namespace MobileGL::MG_State {
return m_renderState.GetPolygonOffsetUnits(); return m_renderState.GetPolygonOffsetUnits();
} }
void GLContext::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
m_renderState.SetPolygonOffsetClamped(factor, units, clamp);
}
Float GLContext::GetPolygonOffsetClamp() const {
return m_renderState.GetPolygonOffsetClamp();
}
void GLContext::SetClipControl(GLenum origin, GLenum depth) {
m_renderState.SetClipControl(origin, depth);
}
GLenum GLContext::GetClipOrigin() const {
return m_renderState.GetClipOrigin();
}
GLenum GLContext::GetClipDepthMode() const {
return m_renderState.GetClipDepthMode();
}
void GLContext::SetCapability(CapabilityInput cap, Bool enabled) { void GLContext::SetCapability(CapabilityInput cap, Bool enabled) {
m_renderState.SetCapability(cap, enabled); m_renderState.SetCapability(cap, enabled);
} }
@@ -1009,6 +1099,14 @@ namespace MobileGL::MG_State {
return m_renderState.GetSampleMaskValue(); return m_renderState.GetSampleMaskValue();
} }
void GLContext::SetMinSampleShadingValue(Float value) {
m_renderState.SetMinSampleShadingValue(value);
}
Float GLContext::GetMinSampleShadingValue() const {
return m_renderState.GetMinSampleShadingValue();
}
void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) { void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) {
m_renderState.SetPixelStoreParam(param, value); m_renderState.SetPixelStoreParam(param, value);
} }
+11
View File
@@ -213,9 +213,18 @@ namespace MobileGL {
Float GetPointSize() const; Float GetPointSize() const;
void SetPatchVertices(Uint vertices); void SetPatchVertices(Uint vertices);
Uint GetPatchVertices() const; Uint GetPatchVertices() const;
void SetPatchDefaultOuterLevel(const FloatVec4& levels);
const FloatVec4& GetPatchDefaultOuterLevel() const;
void SetPatchDefaultInnerLevel(const FloatVec2& levels);
const FloatVec2& GetPatchDefaultInnerLevel() const;
void SetPolygonOffset(Float factor, Float units); void SetPolygonOffset(Float factor, Float units);
void SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
Float GetPolygonOffsetFactor() const; Float GetPolygonOffsetFactor() const;
Float GetPolygonOffsetUnits() const; Float GetPolygonOffsetUnits() const;
Float GetPolygonOffsetClamp() const;
void SetClipControl(GLenum origin, GLenum depth);
GLenum GetClipOrigin() const;
GLenum GetClipDepthMode() const;
void SetHint(GLenum target, GLenum mode); void SetHint(GLenum target, GLenum mode);
GLenum GetHint(GLenum target) const; GLenum GetHint(GLenum target) const;
void SetPointFadeThresholdSize(Float size); void SetPointFadeThresholdSize(Float size);
@@ -276,6 +285,8 @@ namespace MobileGL {
Bool GetSampleCoverageInvert() const; Bool GetSampleCoverageInvert() const;
void SetSampleMaskValue(Uint32 mask); void SetSampleMaskValue(Uint32 mask);
Uint32 GetSampleMaskValue() const; Uint32 GetSampleMaskValue() const;
void SetMinSampleShadingValue(Float value);
Float GetMinSampleShadingValue() const;
void SetPixelStoreParam(PixelStoreParam param, Int value); void SetPixelStoreParam(PixelStoreParam param, Int value);
Int GetPixelStoreParam(PixelStoreParam param) const; Int GetPixelStoreParam(PixelStoreParam param) const;
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const; PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const;
@@ -10,6 +10,7 @@
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h> #include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h> #include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -29,13 +30,11 @@ namespace {
// capacity, which is also the width of the Uint32 masks backends build from it. // capacity, which is also the width of the Uint32 masks backends build from it.
static MobileGL::Int GetReflectionVertexAttribLimit( static MobileGL::Int GetReflectionVertexAttribLimit(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
constexpr MobileGL::Int capacity = // One shared definition with glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) and with
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); // BuildTBuiltInResource's gl_MaxVertexAttribs - the three used to carry three copies of
if (!env.HasBackend()) return capacity; // this formula and glslang's copy was a hardcoded 64.
return MobileGL::MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(env.HasBackend(),
const MobileGL::Int backendLimit = env.params.MaxVertexAttribs; env.params.MaxVertexAttribs);
if (backendLimit <= 0) return capacity;
return std::min(backendLimit, capacity);
} }
// Everything the post-link query surface ever asks a glslang::TType, flattened into a // Everything the post-link query surface ever asks a glslang::TType, flattened into a
@@ -622,13 +621,21 @@ namespace MobileGL::MG_State::GLState {
// mapper's collect callback is the last point at which a resource's qualifier still // mapper's collect callback is the last point at which a resource's qualifier still
// says what the SHADER declared rather than what glslang assigned, so both captures // says what the SHADER declared rather than what glslang assigned, so both captures
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot. // have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
// The binding-range rule (GLSL 4.30 4.4.5): its ceilings in, and the first violation the
// resolver finds out. Enforced at the link because mapIO's collect callback is the last
// point at which a resource's qualifier still says what the SHADER declared - see
// TMglGlslIoResolver::CheckDeclaredBindingRange.
String resourceBindingViolation;
ProgramAttrib attrib{.shaders = Move(shaders), ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = in.explicitAttribLocations, .explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation, .explicitFragmentOutLocations = in.explicitFragDataLocation,
.explicitFragmentOutIndices = in.explicitFragDataIndex, .explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings, .explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding, .storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding}; .uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding,
.resourceBindingLimits = in.env ? ResolveResourceBindingLimits(*in.env)
: MG_Util::ShaderTranspiler::ResourceBindingLimits{},
.resourceBindingViolation = &resourceBindingViolation};
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex); MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
auto result = ShaderCompiler::LinkProgram(attrib); auto result = ShaderCompiler::LinkProgram(attrib);
@@ -672,9 +679,14 @@ namespace MobileGL::MG_State::GLState {
return; return;
} }
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it // The geometry stage's link properties. GL_GEOMETRY_INPUT_TYPE is load-bearing beyond the
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one. // query surface - a draw's primitive type has to be compatible with it (GL 4.6 core
// 11.3.1) - so this block runs for every link, not only a capturing one. The other three
// are pure glGetProgramiv answers that previously had no source at all.
artifacts.gsInputPrimitive = GL_NONE; artifacts.gsInputPrimitive = GL_NONE;
artifacts.gsOutputPrimitive = GL_NONE;
artifacts.gsMaxVertices = 0;
artifacts.gsInvocations = 0;
if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) { if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) {
switch (gs->getInputPrimitive()) { switch (gs->getInputPrimitive()) {
case glslang::ElgPoints: artifacts.gsInputPrimitive = GL_POINTS; break; case glslang::ElgPoints: artifacts.gsInputPrimitive = GL_POINTS; break;
@@ -684,6 +696,77 @@ namespace MobileGL::MG_State::GLState {
case glslang::ElgTrianglesAdjacency: artifacts.gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break; case glslang::ElgTrianglesAdjacency: artifacts.gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break;
default: break; default: break;
} }
switch (gs->getOutputPrimitive()) {
case glslang::ElgPoints: artifacts.gsOutputPrimitive = GL_POINTS; break;
case glslang::ElgLineStrip: artifacts.gsOutputPrimitive = GL_LINE_STRIP; break;
case glslang::ElgTriangleStrip: artifacts.gsOutputPrimitive = GL_TRIANGLE_STRIP; break;
default: break;
}
// glslang leaves both at TQualifier::layoutNotSet (-1) when the shader declared no
// such layout, and `invocations` defaults to one per GLSL 4.60 4.4.2.2 - so clamp
// rather than forward, or GL_GEOMETRY_SHADER_INVOCATIONS reports the sentinel.
artifacts.gsMaxVertices = std::max(gs->getVertices(), 0);
artifacts.gsInvocations = std::max(gs->getInvocations(), 1);
}
// The tessellation evaluation stage's link properties, GL 4.6 core table 23.35: the
// primitive generator's mode, spacing, winding and point mode. (The control stage's
// output patch size is captured below, together with the limit check that goes with it.)
artifacts.tessGenMode = GL_NONE;
artifacts.tessGenSpacing = GL_NONE;
artifacts.tessGenVertexOrder = GL_NONE;
artifacts.tessGenPointMode = false;
if (const glslang::TIntermediate* tes = artifacts.program->getIntermediate(EShLangTessEvaluation)) {
switch (tes->getInputPrimitive()) {
case glslang::ElgTriangles: artifacts.tessGenMode = GL_TRIANGLES; break;
case glslang::ElgQuads: artifacts.tessGenMode = GL_QUADS; break;
case glslang::ElgIsolines: artifacts.tessGenMode = GL_ISOLINES; break;
default: break;
}
// GLSL 4.60 4.4.2.3: equal_spacing and ccw are the defaults, which is what an unset
// qualifier means here.
switch (tes->getVertexSpacing()) {
case glslang::EvsFractionalEven: artifacts.tessGenSpacing = GL_FRACTIONAL_EVEN; break;
case glslang::EvsFractionalOdd: artifacts.tessGenSpacing = GL_FRACTIONAL_ODD; break;
default: artifacts.tessGenSpacing = GL_EQUAL; break;
}
switch (tes->getVertexOrder()) {
case glslang::EvoCw: artifacts.tessGenVertexOrder = GL_CW; break;
default: artifacts.tessGenVertexOrder = GL_CCW; break;
}
artifacts.tessGenPointMode = tes->getPointMode();
}
// GL_TESS_CONTROL_OUTPUT_VERTICES, i.e. the `layout(vertices = N) out` the control stage
// declared, and the limit that goes with it.
//
// GL 4.6 core 11.2.1.1: the LINK fails when N is greater than MAX_PATCH_VERTICES. Nothing
// enforced it - glslang's layout handling only rejects N <= 0 (ParseHelper.cpp "must be
// greater than 0") and carries maxPatchVertices in TBuiltInResource purely so
// gl_MaxPatchVertices can expand from it, exactly the gap ValidateImageUniformLimits
// documents for image uniforms. Checked at LINK rather than at compile on purpose: the CTS
// requires the offending shader to COMPILE ("Compilation passed as allowed") and only the
// link to fail, and turning it into a parse error would newly break an application that
// compiles such a shader and never links it.
//
// The limit is the one glGetIntegerv answers (GL_Getter.cpp reads the same
// DynamicBackendParameters field), so the advertised number and the enforced number cannot
// drift apart.
artifacts.tcsOutputVertices = 0;
if (const glslang::TIntermediate* tcs = artifacts.program->getIntermediate(EShLangTessControl)) {
artifacts.tcsOutputVertices = static_cast<Int>(tcs->getVertices());
if (artifacts.tcsOutputVertices > env.params.MaxPatchVertices) {
artifacts.linkStatus = false;
// Same invariant as the compute local-size gate above: a rejected link leaves no
// TProgram behind for a query surface to find.
artifacts.program.reset();
artifacts.infoLog = std::format(
"Tessellation control shader declares an output patch of {} vertices, more than the {} "
"GL_MAX_PATCH_VERTICES allows.",
artifacts.tcsOutputVertices, env.params.MaxPatchVertices);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
} }
// ---- everything below this line up to GenerateSpirv() is the GL query surface ---- // ---- everything below this line up to GenerateSpirv() is the GL query surface ----
@@ -1104,6 +1187,20 @@ namespace MobileGL::MG_State::GLState {
in.externalIndex, uniform.name.c_str()); in.externalIndex, uniform.name.c_str());
continue; continue;
} }
// The gl_NumSamples stand-in InjectNumSamplesBuiltinShim declared. It is a driver
// uniform, not the application's: gl_NumSamples is a BUILT-IN, so a conformant
// implementation reports nothing for it in GL_ACTIVE_UNIFORMS, glGetActiveUniform or
// glGetUniformLocation, and nothing may write it through glUniform* either. Filtering
// it here does both, and costs it no storage: BuildGlobalUboRouting takes its offset
// from the SPIR-V metadata by name, not from the GL location space.
if (isGlobalUboMember(uniform) &&
uniform.name == MG_Util::ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) {
artifacts.usesReservedNumSamples = true;
MGLOG_D("ProgramObject %u: Reflection - reserved gl_NumSamples stand-in '%s' hidden from the GL "
"uniform surface",
in.externalIndex, uniform.name.c_str());
continue;
}
if (isBufferVariable(uniform)) { if (isBufferVariable(uniform)) {
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform " MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
"surface", "surface",
@@ -1595,6 +1692,25 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding(); artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
artifacts.uniformBlockBinding[i] = artifacts.uniformBlockBinding[i] =
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name); declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
// The second way a binding reaches the state layer's indexed-binding array, and the
// one glUniformBlockBinding's new bound cannot see. glslang does not range-check a
// uniform block's layout(binding = N) against anything - TBuiltInResource has no
// maxUniformBufferBindings field at all, and ParseHelper bounds only samplers and
// atomic counters - so `layout(binding = 5000) uniform Blk {...}` compiled and linked
// clean and then had both backends subscript the array at 5000 on the first draw.
// Stated against the same ceiling glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS)
// advertises; an instance array whose LAST element passes it is a link error even
// though its base fits, same rule as the explicit-location check above.
if (artifacts.uniformBlockBinding[i] >=
static_cast<Int>(MG_State::GLState::BufferBindingPointCount)) {
artifacts.infoLog =
std::format("Uniform block '{}' declares binding {}, which is not less than "
"GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).",
ubo.name, artifacts.uniformBlockBinding[i],
static_cast<Int>(MG_State::GLState::BufferBindingPointCount));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i, MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
ubo.name.c_str(), ubo.size, ubo.getBinding()); ubo.name.c_str(), ubo.size, ubo.getBinding());
} }
@@ -492,6 +492,10 @@ namespace MobileGL::MG_State::GLState {
// time, for anything cached during the pending window itself.) // time, for anything cached during the pending window itself.)
++m_backendStateVersion; ++m_backendStateVersion;
BumpLinkObservableVersions(); BumpLinkObservableVersions();
// The separable flag takes effect HERE, at the link, and nowhere else (GL 4.6 core 7.3).
// Latched before the early-outs below so a link that fails still counts as a link -
// what must not update it is a link that never happened at all.
m_linkedSeparable = m_separable;
// A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this // A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this
// is what every gated reader sees, so it has to be the complete "not linked" state - // is what every gated reader sees, so it has to be the complete "not linked" state -
// including the fields ResetLinkArtifacts deliberately preserves for its own callers. // including the fields ResetLinkArtifacts deliberately preserves for its own callers.
@@ -536,6 +540,33 @@ namespace MobileGL::MG_State::GLState {
task->in.explicitFragDataIndex = m_explicitFragDataIndex; task->in.explicitFragDataIndex = m_explicitFragDataIndex;
task->in.requestedXfbVaryings = m_requestedXfbVaryings; task->in.requestedXfbVaryings = m_requestedXfbVaryings;
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode; task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
// ARB_gl_spirv: a program built from SPIR-V declares its transform feedback through
// XfbBuffer/XfbStride/Offset DECORATIONS, and glTransformFeedbackVaryings has no effect on
// it at all. glSpecializeShader translated those decorations into the equivalent name
// request (ShaderCompiler::SpecializeAndDecompileSpirvModule), and this is where it enters
// the link - so everything downstream, the frontend packer and both backends, sees one
// declaration form instead of two.
//
// The capture stage is the LAST vertex-processing stage the program has, which is the same
// rule ProgramLinkTask::ResolveTransformFeedbackVaryings resolves the names against. The
// application's own request wins if it made one: that can only happen on a mixed program,
// which is not a shape ARB_gl_spirv defines, and honouring what the application explicitly
// asked for is the safer of the two readings.
if (task->in.requestedXfbVaryings.empty()) {
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
Bool stagePresent = false;
for (const auto& shader : m_shaders) {
if (!shader || shader->GetShaderStage() != captureStage) continue;
stagePresent = true;
if (shader->GetSpirvXfbVaryings().empty()) continue;
task->in.requestedXfbVaryings = shader->GetSpirvXfbVaryings();
task->in.requestedXfbBufferMode = shader->GetSpirvXfbBufferMode();
break;
}
if (stagePresent) break;
}
}
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber; task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
Vector<SharedPtr<ShaderCompileTask>> deps; Vector<SharedPtr<ShaderCompileTask>> deps;
@@ -787,6 +787,33 @@ namespace MobileGL::MG_State::GLState {
void MarkUBOContentDirty() const { void MarkUBOContentDirty() const {
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0; if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
} }
// ---- the reserved gl_NumSamples stand-in (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) ----
//
// PHASE A: answerable without joining the SPIR-V job, which is what lets the draw path ask
// every program this question and pay nothing for the overwhelming majority that say no.
Bool UsesReservedNumSamples() const { return Artifacts().usesReservedNumSamples; }
// Publishes `samples` into the global-UBO shadow. Returns false when there is nowhere to
// put it - no shim in this program, no SPIR-V (a cancelled phase B), or the optimizer
// dropped the member because nothing read it after all - all of which are ordinary states,
// not errors. A value-identical write is dropped without bumping the content version, so a
// steady stream of draws into one framebuffer does not force a re-upload per draw.
Bool WriteReservedNumSamples(Int samples) {
if (!UsesReservedNumSamples()) return false;
SpirvArtifacts& spirv = Spirv();
const Uint offset = spirv.reservedNumSamplesOffset;
if (offset == kInvalidUniformOffset) return false;
if (static_cast<SizeT>(offset) + sizeof(Int) > spirv.globalUboScratch.size()) return false;
Uint8* const slot = spirv.globalUboScratch.data() + offset;
Int current = 0;
Memcpy(&current, slot, sizeof(Int));
if (current == samples) return true;
Memcpy(slot, &samples, sizeof(Int));
MarkUBOContentDirty();
return true;
}
// ---- glUniform* inside the phase-A -> phase-B window ---- // ---- glUniform* inside the phase-A -> phase-B window ----
// //
// True while the program is fully linked and fully queryable but its uniform shadow's // True while the program is fully linked and fully queryable but its uniform shadow's
@@ -888,6 +915,14 @@ namespace MobileGL::MG_State::GLState {
// subset of the stages of a program pipeline. Only takes effect on the next link, // subset of the stages of a program pipeline. Only takes effect on the next link,
// which is why it is plain state here rather than something Link() consults. // which is why it is plain state here rather than something Link() consults.
Bool GetSeparable() const { return m_separable; } Bool GetSeparable() const { return m_separable; }
// What GL_PROGRAM_SEPARABLE actually reports, and what glUseProgramStages actually
// requires: the value the flag held at the program's LAST LINK, not the live flag.
// GL 4.6 core 7.3 - "the flag takes effect the next time the program is linked" - so a
// program that was told to be separable and then never linked is still NOT separable,
// which is precisely what es31cSeparateShaderObjsTests's PipelineApi and CreateShadProgApi
// assert. The live flag stays available as GetSeparable() for glGetProgramiv's sibling
// state and for the next link to latch.
Bool GetLinkedSeparable() const { return m_linkedSeparable; }
void SetSeparable(Bool separable) { void SetSeparable(Bool separable) {
m_separable = separable; m_separable = separable;
// ---- arming the uniform-write tracking latch ---- // ---- arming the uniform-write tracking latch ----
@@ -1296,6 +1331,14 @@ namespace MobileGL::MG_State::GLState {
std::set<String> uniformBlocksWithoutBinding; std::set<String> uniformBlocksWithoutBinding;
Uint activeUniformCount = 0; Uint activeUniformCount = 0;
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
// and the draw path owes it the draw framebuffer's sample count before every draw.
//
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
// gate has to be answerable without joining the SPIR-V job, or every draw of every
// program would pay a join to discover it has nothing to write.
Bool usesReservedNumSamples = false;
Uint maxUniformLocation = 0; Uint maxUniformLocation = 0;
Int uniformNameMaxLength = 0; Int uniformNameMaxLength = 0;
Int attribInNameMaxLength = 0; Int attribInNameMaxLength = 0;
@@ -1317,6 +1360,27 @@ namespace MobileGL::MG_State::GLState {
Vector<Uint32> gsStripTriangles; Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false; Bool gsStripCaptureFixup = false;
GLenum gsInputPrimitive = GL_NONE; GLenum gsInputPrimitive = GL_NONE;
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
// tessellation control stage, or 0 when the program has none. Checked against
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
Int tcsOutputVertices = 0;
// The rest of the geometry stage's link properties, and the tessellation evaluation
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
// the query surface listed the geometry pnames only to fall through to
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
// come from the linked intermediates for the same reason gsInputPrimitive and
// tcsOutputVertices do - glslang has already merged the compilation units' layout
// qualifiers and diagnosed contradictions, so the linked program is the thing that
// knows.
GLenum gsOutputPrimitive = GL_NONE;
Int gsMaxVertices = 0;
Int gsInvocations = 0;
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
GLenum tessGenMode = GL_NONE;
GLenum tessGenSpacing = GL_NONE;
GLenum tessGenVertexOrder = GL_NONE;
Bool tessGenPointMode = false;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS; GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int xfbVaryingNameMaxLength = 0; Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false; Bool xfbNeedsScatteredCapture = false;
@@ -1344,6 +1408,11 @@ namespace MobileGL::MG_State::GLState {
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass. // kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
Vector<Uint> uniformOffsets; Vector<Uint> uniformOffsets;
Vector<Uint8> globalUboScratch; Vector<Uint8> globalUboScratch;
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
// uniformOffsets, because the member has no GL location at all: the link task keeps
// it out of the GL-visible uniform index space so no application can see or write it.
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
// False for a program whose SPIR-V was never produced (phase B cancelled at // False for a program whose SPIR-V was never produced (phase B cancelled at
// teardown or by a relink) or whose optimizer run failed. GL has no way to // teardown or by a relink) or whose optimizer run failed. GL has no way to
// retract a LINK_STATUS it already reported true, so such a program stays // retract a LINK_STATUS it already reported true, so such a program stays
@@ -1467,6 +1536,14 @@ namespace MobileGL::MG_State::GLState {
m_requestedXfbVaryings = Move(names); m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode; m_requestedXfbBufferMode = bufferMode;
} }
// NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
// needs the capture list of the stage program it flattens, and the obvious source - what
// glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
// effect until the stage program's next link, and it bumps no version, so reading it makes
// the composite's capture list depend on when the composite cache happened to be
// invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
// AND cache-safe, because linked state only moves at a link and the composite signature
// already keys on the link version. See GLContext::GetProgramForDraw.
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; } GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); } SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const { const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
@@ -1501,6 +1578,22 @@ namespace MobileGL::MG_State::GLState {
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the // GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
// program has no geometry stage. Draws must present a compatible primitive type. // program has no geometry stage. Draws must present a compatible primitive type.
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; } GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
// GL_GEOMETRY_OUTPUT_TYPE (GL_POINTS, GL_LINE_STRIP or GL_TRIANGLE_STRIP),
// GL_GEOMETRY_VERTICES_OUT and GL_GEOMETRY_SHADER_INVOCATIONS of the linked geometry
// stage. Meaningless without one - glGetProgramiv raises INVALID_OPERATION there.
GLenum GetGeometryOutputType() const { return Artifacts().gsOutputPrimitive; }
Int GetGeometryVerticesOut() const { return Artifacts().gsMaxVertices; }
Int GetGeometryShaderInvocations() const { return Artifacts().gsInvocations; }
// GL_TESS_CONTROL_OUTPUT_VERTICES of the linked tessellation control stage, or 0 when
// the program has no such stage. Never greater than GL_MAX_PATCH_VERTICES: a program
// that declared more does not link at all (GL 4.6 core 11.2.1.1).
Int GetTessControlOutputVertices() const { return Artifacts().tcsOutputVertices; }
// GL_TESS_GEN_MODE / _SPACING / _VERTEX_ORDER / _POINT_MODE of the linked tessellation
// evaluation stage.
GLenum GetTessGenMode() const { return Artifacts().tessGenMode; }
GLenum GetTessGenSpacing() const { return Artifacts().tessGenSpacing; }
GLenum GetTessGenVertexOrder() const { return Artifacts().tessGenVertexOrder; }
Bool GetTessGenPointMode() const { return Artifacts().tessGenPointMode; }
Uint GetExternalIndex() const { return m_externalIndex; } Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL // Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
@@ -1626,6 +1719,11 @@ namespace MobileGL::MG_State::GLState {
Bool m_deleteStatus = false; Bool m_deleteStatus = false;
Bool m_binaryRetrievableHint = false; Bool m_binaryRetrievableHint = false;
Bool m_separable = false; Bool m_separable = false;
// m_separable as of the last link; see GetLinkedSeparable. Latched by Link() rather than
// carried in LinkArtifacts because it is a GL-thread-owned decision made at enqueue time,
// not a result the worker computes - and because a FAILED link still latches it, exactly
// as a successful one does.
Bool m_linkedSeparable = false;
// Monotone "this program may ever be a pipeline stage" latch; see SetSeparable for why // Monotone "this program may ever be a pipeline stage" latch; see SetSeparable for why
// it is a latch and not just m_separable. Outside LinkArtifacts on purpose: a relink // it is a latch and not just m_separable. Outside LinkArtifacts on purpose: a relink
// clears the write SET, but a program that was separable is still separable after it. // clears the write SET, but a program that was separable is still separable after it.
@@ -278,6 +278,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformOffsets.clear(); artifacts.uniformOffsets.clear();
artifacts.globalUboScratch.clear(); artifacts.globalUboScratch.clear();
artifacts.reservedNumSamplesOffset = ProgramObject::kInvalidUniformOffset;
// kInvalidUniformOffset marks locations that end up without global-UBO backing // kInvalidUniformOffset marks locations that end up without global-UBO backing
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass // (e.g. the optimizer eliminated every use of the uniform); the fallback pass
// below gives those locations tail storage so glUniform* always has a target. // below gives those locations tail storage so glUniform* always has a target.
@@ -311,6 +312,18 @@ namespace MobileGL::MG_State::GLState {
artifacts.globalUboScratch.resize(size); artifacts.globalUboScratch.resize(size);
} }
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) { for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
// The gl_NumSamples stand-in is routed by NAME and nothing else. It has no GL
// location to look up - DoReflection hides it from the GL uniform index space
// precisely so no application can address it - so the lookup below would find
// nothing and log it as unbacked. Only the fragment stage declares it, and
// every stage's copy sits at the same offset in the one shared global UBO.
if (name == NUM_SAMPLES_UNIFORM_NAME) {
artifacts.reservedNumSamplesOffset = offset;
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - reserved gl_NumSamples stand-in '%s' "
"backed at UBO offset %u",
externalIndex, name.c_str(), offset);
continue;
}
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend // SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
// reflection keys arrays as "arr[0]" (GL naming), so retry with the // reflection keys arrays as "arr[0]" (GL naming), so retry with the
// suffix before declaring the uniform unbacked. // suffix before declaring the uniform unbacked.
@@ -140,17 +140,21 @@ namespace {
return std::nullopt; return std::nullopt;
} }
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than // What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers. Derived by the shared
// queried: the compile runs on a worker with no context, and the pname is not a plain backend // ResolveResourceBindingLimits so the compile-time scan below and the link-time general check
// parameter - the getter caps the backend's count by the state layer's fixed binding-point // (TMglGlslIoResolver::CheckDeclaredBindingRange) can never disagree about the number.
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the //
// number the application was told, not against either half of it. // Why BOTH still exist. GLSL makes an over-range binding a COMPILE-time error, and this scan
// is the only place MobileGL can raise one - glslang's own ceilings are switched off by the
// relaxed Vulkan parse and cannot be turned back on without changing the parse everything
// else depends on. The link-time check covers the four kinds a lexical scan of unexpanded
// source cannot see at all (samplers, images, uniform blocks, atomic counters, whose binding
// only survives inside a synthesized block NAME) and re-covers storage blocks as a backstop.
// The conformance predicate is compile AND link, so either site satisfies it; the split is
// about WHICH error GL reports, not about whether the shader is rejected.
static MobileGL::Int MaxShaderStorageBufferBindings( static MobileGL::Int MaxShaderStorageBufferBindings(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const MobileGL::Int frontendPoints = return MobileGL::MG_State::GLState::ResolveResourceBindingLimits(env).MaxShaderStorageBufferBindings;
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
if (!env.HasBackend()) return frontendPoints;
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
} }
// The half of a compile that depends on nothing but the source text, the stage and the // The half of a compile that depends on nothing but the source text, the stage and the
@@ -10,9 +10,54 @@
#include <Includes.h> #include <Includes.h>
#include <MG_Util/Async/JobNode.h> #include <MG_Util/Async/JobNode.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h> #include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h> #include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// THE one derivation of the binding ceilings a shader-declared layout(binding = N) is judged
// against. Two readers have to agree on them - the compile-time storage-block scan below and
// the link-time general check in TMglGlslIoResolver - and the numbers are recomputed here
// rather than queried because both readers run on a worker with no context.
//
// Each is exactly what glGetIntegerv answers for the matching pname, and none of them is a
// plain backend parameter: the buffer families are additionally capped by the state layer's
// indexed-binding array (GL_Getter's GetIndexedBufferQueryPointCount does the same), because
// a shader must be judged against the number the APPLICATION was told, not against either
// half of it. Lives in MG_State rather than in MG_Util/ShaderTranspiler/Types.h purely
// because BufferBindingPointCount is state-layer knowledge that the transpiler layer must
// not reach up for.
inline MG_Util::ShaderTranspiler::ResourceBindingLimits ResolveResourceBindingLimits(
const MG_Util::ShaderTranspiler::CompileEnv& env) {
namespace ST = MG_Util::ShaderTranspiler;
ST::ResourceBindingLimits limits;
const Int bindingPoints = static_cast<Int>(BufferBindingPointCount);
// The atomic-counter ceiling is a frontend constant, so it holds even with no backend -
// and it is the number BuildTBuiltInResource compiles a layout(binding = N) atomic_uint
// against, which is what makes it enforceable at all.
limits.MaxAtomicCounterBufferBindings = std::min<Int>(bindingPoints, ST::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
// So is the uniform-buffer one: GL_MAX_UNIFORM_BUFFER_BINDINGS is clamped to the indexed
// binding array in the getter and its floor (the GL 4.5 core minimum of 84) is that same
// array's width, so the backend's own number never moves it.
limits.MaxUniformBufferBindings = bindingPoints;
// The storage-buffer ceiling has the same shape as GetIndexedBufferQueryPointCount's: the
// backend's count capped by the array, and the array alone when there is no backend. That
// "no backend" arm is not a detail - it is what the GPU-free test binary runs under, and
// it has to keep matching what glGetIntegerv answers there.
limits.MaxShaderStorageBufferBindings =
env.HasBackend()
? std::min<Int>(bindingPoints, std::max<Int>(env.params.MaxShaderStorageBufferBindings, 0))
: bindingPoints;
if (!env.HasBackend()) {
// The two genuinely per-DEVICE ceilings have nothing to be measured against here, and
// zero means "do not enforce this kind" rather than "reject everything".
return limits;
}
limits.MaxSamplerBindings = std::max<Int>(env.params.MaxCombinedTextureImageUnits, 0);
limits.MaxImageBindings = std::max<Int>(env.params.MaxImageUnits, 0);
return limits;
}
// glslang has no "detach this thread" API in the vendored revision, but TShader::parse // glslang has no "detach this thread" API in the vendored revision, but TShader::parse
// leaves the calling thread's TLS pool allocator pointing at the shader's own pool and // leaves the calling thread's TLS pool allocator pointing at the shader's own pool and
// never restores it. Left there, the next allocation this thread makes - in an unrelated // never restores it. Left there, the next allocation this thread makes - in an unrelated
@@ -15,7 +15,77 @@
#include <MG_Util/ShaderTranspiler/Types.h> #include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
void ShaderObject::SetSpirvBinary(Vector<Uint32>&& binary) {
// A module replaces whatever this object stood for, so the compiled state of the old
// source goes with it - including a compile still in flight.
ReleaseCompileNode();
m_spirvBinary = Move(binary);
m_hasSpirvBinary = true;
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
}
const String& ShaderObject::GetApplicationShaderSource() const {
static const String kNoSource;
// Both the unspecialized and the specialized windows answer empty: in the first m_source
// already is empty, in the second it holds generated GLSL that the application never wrote.
return m_hasSpirvBinary ? kNoSource : *m_source;
}
void ShaderObject::SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode) {
ReleaseCompileNode();
// The latch goes up HERE and nowhere else - this is the one path that actually specialized
// the shader.
m_specialized = true;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings = Move(xfbVaryings);
m_spirvXfbBufferMode = xfbBufferMode;
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know
// the source was not written by the application - which is the whole reason this hop
// exists, and the reason a SPIR-V program's GL-visible surface (uniform locations, block
// indices, transform-feedback layout) is populated at all.
m_source = MakeShared<const String>(Move(glsl));
InvalidateCompiledState();
Compile();
}
void ShaderObject::RecordSpecializationFailure(String&& infoLog) {
ReleaseCompileNode();
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
m_specializationFailed = true;
m_specializationInfoLog = Move(infoLog);
}
void ShaderObject::SetShaderSource(const String& source) { void ShaderObject::SetShaderSource(const String& source) {
// glShaderSource on a SPIR-V shader takes the object back to being a GLSL one, and
// GL_SPIR_V_BINARY must then read FALSE (ARB_gl_spirv; gl4cGlSpirvTests'
// spirv_modules_state_queries_test checks exactly this transition). The stored module goes
// with the flag - re-specializing it would be re-specializing a shader the application has
// already replaced. The memo below is skipped on purpose: the source may well be
// byte-identical to the empty string this object has been holding, and keeping the
// "compiled state" of that would keep the module's verdict too.
if (m_hasSpirvBinary || m_specializationFailed) {
m_hasSpirvBinary = false;
m_spirvBinary.clear();
m_spirvBinary.shrink_to_fit();
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
ReleaseCompileNode();
m_source = MakeShared<const String>(source);
InvalidateCompiledState();
return;
}
// P0b layer 1. glShaderSource always REPLACES the source, but replacing it with a // P0b layer 1. glShaderSource always REPLACES the source, but replacing it with a
// byte-identical one cannot change what a compile would produce: the whole // byte-identical one cannot change what a compile would produce: the whole
// pipeline (preprocess -> lexical checks -> glslang parse) is a pure function of // pipeline (preprocess -> lexical checks -> glslang parse) is a pure function of
@@ -36,6 +106,10 @@ namespace MobileGL::MG_State::GLState {
} }
void ShaderObject::SetShaderSource(String&& source) { void ShaderObject::SetShaderSource(String&& source) {
if (m_hasSpirvBinary || m_specializationFailed) {
SetShaderSource(static_cast<const String&>(source));
return;
}
if (SourceMatchesCompiledState(source)) return; if (SourceMatchesCompiledState(source)) return;
ReleaseCompileNode(); ReleaseCompileNode();
m_source = MakeShared<const String>(Move(source)); m_source = MakeShared<const String>(Move(source));
@@ -65,6 +65,51 @@ namespace MobileGL {
void SetShaderSource(const String& source); void SetShaderSource(const String& source);
void SetShaderSource(String&& source); void SetShaderSource(String&& source);
void Compile(); void Compile();
// ---- GL_ARB_gl_spirv ----
// glShaderBinary(GL_SHADER_BINARY_FORMAT_SPIR_V): the object stops standing for a
// GLSL source and starts standing for an application-supplied SPIR-V module. The
// module is held verbatim until glSpecializeShader names an entry point for it -
// ARB_gl_spirv makes the pair a two-step operation, and glCompileShader in between is
// INVALID_OPERATION rather than a compile of anything.
//
// Both directions clear the other: glShaderSource on a SPIR-V shader takes it back to
// being a GLSL shader with GL_SPIR_V_BINARY reading FALSE, which the conformance suite
// checks explicitly.
void SetSpirvBinary(Vector<Uint32>&& binary);
Bool HasSpirvBinary() const { return m_hasSpirvBinary; }
// ARB_gl_spirv: "Once specialized, a shader may not be re-specialized without first
// re-associating the original SPIR-V module with it, through ShaderBinary." A second
// glSpecializeShader is GL_INVALID_OPERATION, and this latch is what answers that.
//
// Set ONLY on the success path. A specialization that FAILED did not specialize the
// shader, and the conformance suite relies on that distinction: it deliberately fails
// specialization (a bad entry point, then an unknown constant id) on one shader object
// and then requires the next, well-formed call on that same object to be accepted.
Bool HasBeenSpecialized() const { return m_specialized; }
const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
// glSpecializeShader's half: hand the object the GLSL its module specializes to and
// let the ordinary pipeline compile it.
void SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode);
// The capture the object's SPIR-V module DECLARED, as the equivalent
// glTransformFeedbackVaryings request. Empty for a GLSL shader and for a SPIR-V module
// that declares no transform feedback. ProgramObject::Link picks this up from the
// program's last vertex-processing stage, because ARB_gl_spirv makes decorations the
// only declaration form for a SPIR-V program and glTransformFeedbackVaryings has no
// effect on one.
const Vector<String>& GetSpirvXfbVaryings() const { return m_spirvXfbVaryings; }
GLenum GetSpirvXfbBufferMode() const { return m_spirvXfbBufferMode; }
// What glGetShaderSource / GL_SHADER_SOURCE_LENGTH must answer. A shader created from
// glShaderBinary never had glShaderSource called on it, so GL 4.6 core 7.1 makes its
// source the empty string - even after glSpecializeShader, when m_source holds the
// SPIRV-Cross GLSL the module was translated into. That text is MobileGL's, not the
// application's, and handing it back invites an application to cache and re-submit it.
const String& GetApplicationShaderSource() const;
// The other half: specialization itself failed (a bad entry point, a constant id the
// module does not declare, a module spirv-val rejects). There is nothing to compile,
// so the verdict is recorded directly - COMPILE_STATUS false with this log - and both
// queries answer from it without touching the compile pipeline.
void RecordSpecializationFailure(String&& infoLog);
// Gives up this object's claim on its compile node, cancelling the node only if // Gives up this object's claim on its compile node, cancelling the node only if
// this object was its LAST claimant. Called at the points where the object's // this object was its LAST claimant. Called at the points where the object's
// compiled state stops being observable through THIS name: a real source change, // compiled state stops being observable through THIS name: a real source change,
@@ -99,14 +144,16 @@ namespace MobileGL {
const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; } const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; }
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; } const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return Compiled().infoLog; } const String& GetInfoLog() const {
return m_specializationFailed ? m_specializationInfoLog : Compiled().infoLog;
}
// Explicit layout(location = N) qualifiers on this shader's default-block // Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap // uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
// discarded them (see CollectExplicitUniformLocations). // discarded them (see CollectExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const { const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return Compiled().explicitUniformLocations; return Compiled().explicitUniformLocations;
} }
Bool GetCompileStatus() const { return Compiled().compileStatus; } Bool GetCompileStatus() const { return m_specializationFailed ? false : Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; } Bool GetDeleteStatus() const { return m_deleteStatus; }
// Blocks until a pending compile has published its artifacts. Public for the // Blocks until a pending compile has published its artifacts. Public for the
@@ -248,6 +295,25 @@ namespace MobileGL {
// query optimistically for the current node. Cleared wherever the node // query optimistically for the current node. Cleared wherever the node
// changes hands (AdoptCompileNode) or goes away (DropCompileNode). // changes hands (AdoptCompileNode) or goes away (DropCompileNode).
mutable Bool m_optimisticAnswerLatched = false; mutable Bool m_optimisticAnswerLatched = false;
// The application-supplied SPIR-V module and the flag GL_SPIR_V_BINARY reports. The
// module is kept after specialization too: glSpecializeShader may legally run again on
// the same object with different constants, and the second call has to re-specialize
// the ORIGINAL words rather than the ones the first call folded.
Vector<Uint32> m_spirvBinary;
Bool m_hasSpirvBinary = false;
// "This shader has been specialized"; see HasBeenSpecialized. Cleared by anything that
// re-associates a module (SetSpirvBinary) or turns the object back into a GLSL shader
// (either SetShaderSource overload) - which is exactly the re-association ARB_gl_spirv
// names as the way to make a second specialization legal again.
Bool m_specialized = false;
Vector<String> m_spirvXfbVaryings;
GLenum m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
// A specialization that failed before any compile could start. Kept beside the
// compile artifacts rather than inside them because there is no compile job to hang
// it on - see RecordSpecializationFailure. Cleared by anything that gives the object
// a new meaning (a new source, a new module, a fresh specialization).
Bool m_specializationFailed = false;
String m_specializationInfoLog;
}; };
} // namespace MG_State::GLState } // namespace MG_State::GLState
} // namespace MobileGL } // namespace MobileGL
@@ -217,12 +217,43 @@ namespace MobileGL {
return m_parameters.PatchVertices; return m_parameters.PatchVertices;
} }
void RenderState::SetPolygonOffset(Float factor, Float units) { // BumpVersions(), not just ++m_version, for the same reason SetPatchVertices does it:
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return; // these levels are compiled INTO the synthesized pass-through tessellation control
// stage on both backends, so changing one makes an already-built program stale.
//
// The redundant-write guard compares BIT PATTERNS, not floats: glPatchParameterfv
// accepts NaN, and a float compare would let a re-set of the identical NaN tuple fall
// through and bump the pipeline-state version - invalidating DirectVulkan's pipeline
// memo and DirectGLES's render-state span - on every single call.
void RenderState::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
if (BitwiseEqual(m_parameters.PatchDefaultOuterLevel, levels)) return;
m_parameters.PolygonOffsetFactor = factor; m_parameters.PatchDefaultOuterLevel = levels;
m_parameters.PolygonOffsetUnits = units; BumpVersions();
++m_version; }
const FloatVec4& RenderState::GetPatchDefaultOuterLevel() const {
return m_parameters.PatchDefaultOuterLevel;
}
void RenderState::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
if (BitwiseEqual(m_parameters.PatchDefaultInnerLevel, levels)) return;
m_parameters.PatchDefaultInnerLevel = levels;
BumpVersions();
}
const FloatVec2& RenderState::GetPatchDefaultInnerLevel() const {
return m_parameters.PatchDefaultInnerLevel;
}
void RenderState::SetPolygonOffset(Float factor, Float units) {
// GL 4.6 core 14.6.5 defines PolygonOffset(factor, units) as EQUIVALENT to
// PolygonOffsetClamp(factor, units, 0) - the equivalence is total, so the clamp is
// written too, not merely left alone. Leaving it meant a glPolygonOffsetClamp(1, 1,
// 0.5) followed by a plain glPolygonOffset(3, 4) still reported a clamp of 0.5, and
// the early-out below could even skip the version bump while doing it.
SetPolygonOffsetClamped(factor, units, 0.0f);
} }
Float RenderState::GetPolygonOffsetFactor() const { Float RenderState::GetPolygonOffsetFactor() const {
@@ -233,6 +264,37 @@ namespace MobileGL {
return m_parameters.PolygonOffsetUnits; return m_parameters.PolygonOffsetUnits;
} }
void RenderState::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units &&
m_parameters.PolygonOffsetClamp == clamp)
return;
m_parameters.PolygonOffsetFactor = factor;
m_parameters.PolygonOffsetUnits = units;
m_parameters.PolygonOffsetClamp = clamp;
++m_version;
}
Float RenderState::GetPolygonOffsetClamp() const {
return m_parameters.PolygonOffsetClamp;
}
void RenderState::SetClipControl(GLenum origin, GLenum depth) {
if (m_parameters.ClipOrigin == origin && m_parameters.ClipDepthMode == depth) return;
m_parameters.ClipOrigin = origin;
m_parameters.ClipDepthMode = depth;
++m_version;
}
GLenum RenderState::GetClipOrigin() const {
return m_parameters.ClipOrigin;
}
GLenum RenderState::GetClipDepthMode() const {
return m_parameters.ClipDepthMode;
}
// -------------------- Capabilities -------------------- // -------------------- Capabilities --------------------
namespace { namespace {
// CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller // CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller
@@ -270,6 +332,7 @@ namespace MobileGL {
SET_CAPABILITY(SampleAlphaToOne, enabled); SET_CAPABILITY(SampleAlphaToOne, enabled);
SET_CAPABILITY(SampleCoverage, enabled); SET_CAPABILITY(SampleCoverage, enabled);
SET_CAPABILITY(SampleMask, enabled); SET_CAPABILITY(SampleMask, enabled);
SET_CAPABILITY(SampleShading, enabled);
SET_CAPABILITY(StencilTest, enabled); SET_CAPABILITY(StencilTest, enabled);
SET_CAPABILITY(ProgramPointSize, enabled); SET_CAPABILITY(ProgramPointSize, enabled);
case CapabilityInput::Blend: { case CapabilityInput::Blend: {
@@ -344,6 +407,7 @@ namespace MobileGL {
RETURN_CAPABILITY(SampleAlphaToOne); RETURN_CAPABILITY(SampleAlphaToOne);
RETURN_CAPABILITY(SampleCoverage); RETURN_CAPABILITY(SampleCoverage);
RETURN_CAPABILITY(SampleMask); RETURN_CAPABILITY(SampleMask);
RETURN_CAPABILITY(SampleShading);
RETURN_CAPABILITY(StencilTest); RETURN_CAPABILITY(StencilTest);
RETURN_CAPABILITY(ProgramPointSize); RETURN_CAPABILITY(ProgramPointSize);
case CapabilityInput::Blend: case CapabilityInput::Blend:
@@ -737,6 +801,20 @@ namespace MobileGL {
return m_parameters.SampleMaskValue; return m_parameters.SampleMaskValue;
} }
void RenderState::SetMinSampleShadingValue(Float value) {
if (m_parameters.MinSampleShadingValue == value) return;
m_parameters.MinSampleShadingValue = value;
// BumpVersions, not just ++m_version: DirectVulkan bakes the fraction into
// VkPipelineMultisampleStateCreateInfo::minSampleShading, so a cached pipeline
// built with the old value must not be reused.
BumpVersions();
}
Float RenderState::GetMinSampleShadingValue() const {
return m_parameters.MinSampleShadingValue;
}
// -------------------- Pixel Store -------------------- // -------------------- Pixel Store --------------------
void RenderState::SetPixelStoreParam(PixelStoreParam param, Int value) { void RenderState::SetPixelStoreParam(PixelStoreParam param, Int value) {
#define SET_PIXEL_STORE_PARAM(paramNameHead, paramNameTail, val) \ #define SET_PIXEL_STORE_PARAM(paramNameHead, paramNameTail, val) \
@@ -240,8 +240,24 @@ namespace MobileGL {
Float PointSize = 1.0f; Float PointSize = 1.0f;
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes. // GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
Uint PatchVertices = 3; Uint PatchVertices = 3;
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL (glPatchParameterfv). The
// tessellation levels used when a program has an evaluation stage and NO control stage -
// GL's fixed-function pass-through (4.6 core 11.2.2). Both backends have to synthesize
// that stage, and they bake these numbers into it, so a change here makes an already-built
// one stale exactly as PATCH_VERTICES does. Default 1.0, per table 23.44.
FloatVec4 PatchDefaultOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
FloatVec2 PatchDefaultInnerLevel = FloatVec2(1.0f, 1.0f);
Float PolygonOffsetFactor = 0.0f; Float PolygonOffsetFactor = 0.0f;
Float PolygonOffsetUnits = 0.0f; Float PolygonOffsetUnits = 0.0f;
// GL_POLYGON_OFFSET_CLAMP (GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp): the maximum
// magnitude of the offset glPolygonOffsetClamp's third argument allows. Zero - the default
// - means "no clamp", which is exactly the behaviour glPolygonOffset leaves behind.
Float PolygonOffsetClamp = 0.0f;
// glClipControl (GL 4.5 core 13.5). Defaults per table 23.7 are the pre-4.5 fixed
// behaviour: origin at the lower left, depth mapped from -1..1.
GLenum ClipOrigin = GL_LOWER_LEFT;
GLenum ClipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
// Blending // Blending
Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> BlendStates; Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> BlendStates;
@@ -271,6 +287,10 @@ namespace MobileGL {
Float SampleCoverageValue = 1.0f; Float SampleCoverageValue = 1.0f;
Bool SampleCoverageInvert = false; Bool SampleCoverageInvert = false;
Uint32 SampleMaskValue = 0xffffffffu; Uint32 SampleMaskValue = 0xffffffffu;
// glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples
// that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial
// value is 0, and the value is clamped to [0, 1] on the way in.
Float MinSampleShadingValue = 0.0f;
Array<StencilFaceState, 2> StencilStates{}; Array<StencilFaceState, 2> StencilStates{};
// Cull Face // Cull Face
@@ -319,6 +339,7 @@ namespace MobileGL {
Bool SampleAlphaToOneEnabled = false; Bool SampleAlphaToOneEnabled = false;
Bool SampleCoverageEnabled = false; Bool SampleCoverageEnabled = false;
Bool SampleMaskEnabled = false; Bool SampleMaskEnabled = false;
Bool SampleShadingEnabled = false;
Bool StencilTestEnabled = false; Bool StencilTestEnabled = false;
Bool ProgramPointSizeEnabled = false; Bool ProgramPointSizeEnabled = false;
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one // glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
@@ -374,9 +395,21 @@ namespace MobileGL {
Float GetPointSize() const; Float GetPointSize() const;
void SetPatchVertices(Uint vertices); void SetPatchVertices(Uint vertices);
Uint GetPatchVertices() const; Uint GetPatchVertices() const;
void SetPatchDefaultOuterLevel(const FloatVec4& levels);
const FloatVec4& GetPatchDefaultOuterLevel() const;
void SetPatchDefaultInnerLevel(const FloatVec2& levels);
const FloatVec2& GetPatchDefaultInnerLevel() const;
void SetPolygonOffset(Float factor, Float units); void SetPolygonOffset(Float factor, Float units);
// glPolygonOffsetClamp. Writes the same factor/units as glPolygonOffset plus the
// clamp, because that is what the entry point does - glPolygonOffset is the
// clamp = 0 case of it (GL 4.6 core 14.6.5).
void SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
Float GetPolygonOffsetFactor() const; Float GetPolygonOffsetFactor() const;
Float GetPolygonOffsetUnits() const; Float GetPolygonOffsetUnits() const;
Float GetPolygonOffsetClamp() const;
void SetClipControl(GLenum origin, GLenum depth);
GLenum GetClipOrigin() const;
GLenum GetClipDepthMode() const;
// Hints. target must be one of the 4 GL 3.3 core hint targets (validated by the caller). // Hints. target must be one of the 4 GL 3.3 core hint targets (validated by the caller).
void SetHint(GLenum target, GLenum mode); void SetHint(GLenum target, GLenum mode);
GLenum GetHint(GLenum target) const; GLenum GetHint(GLenum target) const;
@@ -454,6 +487,9 @@ namespace MobileGL {
Bool GetSampleCoverageInvert() const; Bool GetSampleCoverageInvert() const;
void SetSampleMaskValue(Uint32 mask); void SetSampleMaskValue(Uint32 mask);
Uint32 GetSampleMaskValue() const; Uint32 GetSampleMaskValue() const;
// glMinSampleShading. `value` is stored as given; the entry point clamps.
void SetMinSampleShadingValue(Float value);
Float GetMinSampleShadingValue() const;
// Pixel Store // Pixel Store
void SetPixelStoreParam(PixelStoreParam param, Int value); void SetPixelStoreParam(PixelStoreParam param, Int value);
@@ -155,9 +155,21 @@ namespace MobileGL {
// an answer whichever form was written. Integer <-> float uses the plain value, matching // an answer whichever form was written. Integer <-> float uses the plain value, matching
// what glTexParameterIiv/Iuiv mean: those forms are for integer texture formats, whose // what glTexParameterIiv/Iuiv mean: those forms are for integer texture formats, whose
// border components are the raw integers rather than a normalized fraction. // border components are the raw integers rather than a normalized fraction.
//
// Which of the three the application actually WROTE is recorded separately in
// borderColorForm, because the derived values erase it: a backend has to know whether to
// forward the colour through glSamplerParameterfv or glSamplerParameterIiv (and which
// VkBorderColor family to ask Vulkan for), and the numbers alone cannot say. That is also
// why every setter's early-out tests the form as well as the value - a float (0,0,0,1)
// followed by an integer (0,0,0,1) is a real state change even though nothing numeric
// moved, and swallowing it would leave the backend syncing the wrong entry point forever.
void SamplerObject::SetBorderColor(const FloatVec4& color) { void SamplerObject::SetBorderColor(const FloatVec4& color) {
if (color == m_samplerParameters.borderColor) return; if (color == m_samplerParameters.borderColor &&
m_samplerParameters.borderColorForm == BorderColorForm::Float) {
return;
}
m_samplerParameters.borderColorForm = BorderColorForm::Float;
m_samplerParameters.borderColor = color; m_samplerParameters.borderColor = color;
m_samplerParameters.borderColorI = m_samplerParameters.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()), IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
@@ -169,8 +181,12 @@ namespace MobileGL {
} }
void SamplerObject::SetBorderColorI(const IntVec4& color) { void SamplerObject::SetBorderColorI(const IntVec4& color) {
if (color == m_samplerParameters.borderColorI) return; if (color == m_samplerParameters.borderColorI &&
m_samplerParameters.borderColorForm == BorderColorForm::Int) {
return;
}
m_samplerParameters.borderColorForm = BorderColorForm::Int;
m_samplerParameters.borderColorI = color; m_samplerParameters.borderColorI = color;
m_samplerParameters.borderColorUI = m_samplerParameters.borderColorUI =
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()), UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
@@ -182,8 +198,12 @@ namespace MobileGL {
} }
void SamplerObject::SetBorderColorUI(const UintVec4& color) { void SamplerObject::SetBorderColorUI(const UintVec4& color) {
if (color == m_samplerParameters.borderColorUI) return; if (color == m_samplerParameters.borderColorUI &&
m_samplerParameters.borderColorForm == BorderColorForm::Uint) {
return;
}
m_samplerParameters.borderColorForm = BorderColorForm::Uint;
m_samplerParameters.borderColorUI = color; m_samplerParameters.borderColorUI = color;
m_samplerParameters.borderColorI = m_samplerParameters.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()), IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
@@ -206,6 +226,10 @@ namespace MobileGL {
return m_samplerParameters.borderColorUI; return m_samplerParameters.borderColorUI;
} }
BorderColorForm SamplerObject::GetBorderColorForm() const {
return m_samplerParameters.borderColorForm;
}
SamplerCompareMode SamplerObject::GetCompareMode() const { SamplerCompareMode SamplerObject::GetCompareMode() const {
return m_samplerParameters.compareMode; return m_samplerParameters.compareMode;
} }
@@ -56,6 +56,19 @@ namespace MobileGL {
Unknown = -1 Unknown = -1
}; };
// Which of the three GL_TEXTURE_BORDER_COLOR entry-point families last wrote the border colour,
// and therefore which of the three stored representations is AUTHORITATIVE. GL 4.6 core 8.10:
// TexParameterIiv/Iuiv store an integer border colour "unmodified, with an internal data type of
// integer", TexParameterfv stores a floating-point one, and the derived forms are only a
// convenience for a getter of the other spelling. A backend cannot pick the right driver entry
// point (glSamplerParameterIiv vs fv) or the right VkBorderColor family without this: numerically
// the three representations are always populated, so the value alone says nothing about the form.
enum class BorderColorForm : Uint8 {
Float,
Int,
Uint
};
struct SamplerParameters { struct SamplerParameters {
SamplerWrapMode wrapS = SamplerWrapMode::Repeat; SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
SamplerWrapMode wrapT = SamplerWrapMode::Repeat; SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
@@ -79,6 +92,7 @@ namespace MobileGL {
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f}; FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 borderColorI = {0, 0, 0, 0}; IntVec4 borderColorI = {0, 0, 0, 0};
UintVec4 borderColorUI = {0, 0, 0, 0}; UintVec4 borderColorUI = {0, 0, 0, 0};
BorderColorForm borderColorForm = BorderColorForm::Float;
}; };
namespace MG_State { namespace MG_State {
@@ -117,6 +131,7 @@ namespace MobileGL {
const FloatVec4& GetBorderColor() const; const FloatVec4& GetBorderColor() const;
const IntVec4& GetBorderColorI() const; const IntVec4& GetBorderColorI() const;
const UintVec4& GetBorderColorUI() const; const UintVec4& GetBorderColorUI() const;
BorderColorForm GetBorderColorForm() const;
Uint GetExternalIndex() const; Uint GetExternalIndex() const;
Uint16 GetVersion() const; Uint16 GetVersion() const;
// Globally-unique, never-reused id for this sampler object's lifetime. Lets a // Globally-unique, never-reused id for this sampler object's lifetime. Lets a
@@ -113,8 +113,14 @@ namespace MobileGL {
return m_sampler->GetBorderColor(); return m_sampler->GetBorderColor();
} }
// The redundancy filters test the FORM as well as the value: the derived representations
// make a float (0,0,0,1) and an integer (0,0,0,1) numerically identical, but they are
// different GL state and the DirectGLES sync memoises on m_textureParamsVersion.
void TextureObjectBase::SetBorderColor(const FloatVec4& color) { void TextureObjectBase::SetBorderColor(const FloatVec4& color) {
if (color == m_sampler->GetBorderColor()) return; if (color == m_sampler->GetBorderColor() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Float) {
return;
}
m_sampler->SetBorderColor(color); m_sampler->SetBorderColor(color);
++m_textureParamsVersion; ++m_textureParamsVersion;
@@ -125,7 +131,10 @@ namespace MobileGL {
} }
void TextureObjectBase::SetBorderColorI(const IntVec4& color) { void TextureObjectBase::SetBorderColorI(const IntVec4& color) {
if (color == m_sampler->GetBorderColorI()) return; if (color == m_sampler->GetBorderColorI() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Int) {
return;
}
m_sampler->SetBorderColorI(color); m_sampler->SetBorderColorI(color);
++m_textureParamsVersion; ++m_textureParamsVersion;
@@ -136,12 +145,19 @@ namespace MobileGL {
} }
void TextureObjectBase::SetBorderColorUI(const UintVec4& color) { void TextureObjectBase::SetBorderColorUI(const UintVec4& color) {
if (color == m_sampler->GetBorderColorUI()) return; if (color == m_sampler->GetBorderColorUI() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Uint) {
return;
}
m_sampler->SetBorderColorUI(color); m_sampler->SetBorderColorUI(color);
++m_textureParamsVersion; ++m_textureParamsVersion;
} }
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
return m_sampler->GetBorderColorForm();
}
TextureSwizzleParam TextureObjectBase::GetSwizzleParam(TextureSwizzleParam param) const { TextureSwizzleParam TextureObjectBase::GetSwizzleParam(TextureSwizzleParam param) const {
switch (param) { switch (param) {
case TextureSwizzleParam::Red: case TextureSwizzleParam::Red:
@@ -40,6 +40,8 @@ namespace MobileGL::MG_State::GLState {
virtual void SetBorderColorI(const IntVec4& color) = 0; virtual void SetBorderColorI(const IntVec4& color) = 0;
virtual const UintVec4& GetBorderColorUI() const = 0; virtual const UintVec4& GetBorderColorUI() const = 0;
virtual void SetBorderColorUI(const UintVec4& color) = 0; virtual void SetBorderColorUI(const UintVec4& color) = 0;
// Which of the three setters above last ran; see SamplerParameters::borderColorForm.
virtual BorderColorForm GetBorderColorForm() const = 0;
virtual TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const = 0; virtual TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const = 0;
virtual void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 0; virtual void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 0;
virtual void SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) = 0; virtual void SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) = 0;
@@ -49,6 +51,12 @@ namespace MobileGL::MG_State::GLState {
virtual void SetMaxLevel(Uint maxLevel) = 0; virtual void SetMaxLevel(Uint maxLevel) = 0;
virtual Bool IsImmutable() const = 0; virtual Bool IsImmutable() const = 0;
virtual Uint GetImmutableLevels() const = 0; virtual Uint GetImmutableLevels() const = 0;
// How many levels THIS object can address, i.e. the bound a level argument has to
// stay under. The same number as GetImmutableLevels() for an ordinary immutable
// texture, but NOT for a view: GL 4.6 core 8.18 defines TEXTURE_IMMUTABLE_LEVELS on a
// view as the ORIGINAL texture's value, which says nothing about what the view itself
// can reach, and bounding by it lets a level the view does not have through.
virtual Uint GetAddressableLevelCount() const = 0;
virtual void SetImmutableLevels(Uint levels) = 0; virtual void SetImmutableLevels(Uint levels) = 0;
virtual Uint16 GetTextureParamsVersion() const = 0; virtual Uint16 GetTextureParamsVersion() const = 0;
// Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty). // Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty).
@@ -123,6 +131,7 @@ namespace MobileGL::MG_State::GLState {
void SetBorderColorI(const IntVec4& color) override; void SetBorderColorI(const IntVec4& color) override;
const UintVec4& GetBorderColorUI() const override; const UintVec4& GetBorderColorUI() const override;
void SetBorderColorUI(const UintVec4& color) override; void SetBorderColorUI(const UintVec4& color) override;
BorderColorForm GetBorderColorForm() const override;
TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override; TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override;
const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const override; const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const override;
void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override; void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override;
@@ -132,6 +141,10 @@ namespace MobileGL::MG_State::GLState {
void SetMaxLevel(Uint maxLevel) override; void SetMaxLevel(Uint maxLevel) override;
Bool IsImmutable() const override; Bool IsImmutable() const override;
Uint GetImmutableLevels() const override; Uint GetImmutableLevels() const override;
// m_immutableLevels is already the VIEW-relative count for a view (its constructor
// stores <numlevels> there so the level-range clamp works in view coordinates), so
// this one accessor is correct for both and needs no override.
Uint GetAddressableLevelCount() const override { return m_immutableLevels; }
void SetImmutableLevels(Uint levels) override; void SetImmutableLevels(Uint levels) override;
Uint16 GetTextureParamsVersion() const override; Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override; Uint64 GetContentVersion() const override;
@@ -15,6 +15,8 @@
#include <MG_Backend/DirectGLES/Utils.h> #include <MG_Backend/DirectGLES/Utils.h>
#include <limits>
using namespace MobileGL; using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
@@ -1296,8 +1298,13 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an // rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
// ES link error against a program that has nothing else wrong with it. // ES link error against a program that has nothing else wrong with it.
namespace {
const FloatVec4 kDefaultOuter(1.0f, 1.0f, 1.0f, 1.0f);
const FloatVec2 kDefaultInner(1.0f, 1.0f);
} // namespace
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) { TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", ""); const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
EXPECT_EQ(out.find("#version 320 es"), 0u) << out; EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out; EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
EXPECT_TRUE(Contains(out, EXPECT_TRUE(Contains(out,
@@ -1314,10 +1321,63 @@ TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out; EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
} }
// glPatchParameterfv's state is compiled INTO this stage: ES has no PATCH_DEFAULT_*_LEVEL and no
// entry point to forward it to, so a generator that ignored these arguments would tessellate every
// control-stage-less program at level 1 whatever the application asked for.
TEST(PassthroughTessControlEsslTest, BakesTheDefaultTessLevelsIn) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 3.0f, 4.0f, 5.0f),
FloatVec2(6.5f, 7.25f));
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 2.0;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 3.0;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[2] = 4.0;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[3] = 5.0;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 6.5;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[1] = 7.25;")) << out;
}
// Every level literal carries a decimal point even when the value is integral: ESSL reads
// `gl_TessLevelOuter[0] = 1;` as an int assigned to a float and refuses to compile the stage,
// which would take the whole program down with it.
TEST(PassthroughTessControlEsslTest, SpellsIntegralLevelsAsFloatLiterals) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 2.0f, 2.0f, 2.0f),
FloatVec2(2.0f, 2.0f));
EXPECT_FALSE(Contains(out, "= 2;")) << out;
}
// glPatchParameterfv accepts any float, NaN and infinity included, and GL 4.6 core 11.2.2
// discards a patch ONLY when a relevant outer level is <= 0 - everything else is clamped into
// [1, MAX_TESS_GEN_LEVEL]. So the three non-finite inputs do not share one answer: NaN is
// unspecified and 0.0 is the safe reading, -inf really does discard, and +inf must tessellate at
// the maximum. Baking 0.0 for +inf inverted "as finely as possible" into "draw nothing".
TEST(PassthroughTessControlEsslTest, NonFiniteLevelsFollowTheDiscardRule) {
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
const Float infinity = std::numeric_limits<Float>::infinity();
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
FloatVec4(notANumber, -infinity, infinity, 1.0f),
FloatVec2(notANumber, 1.0f));
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 0.0;")) << out;
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[2] = 0.0;"))
<< "a positive infinity clamps to GL_MAX_TESS_GEN_LEVEL, not to a discarded patch" << out;
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 0.0;")) << out;
EXPECT_FALSE(Contains(out, "nan")) << out;
EXPECT_FALSE(Contains(out, "inf")) << out;
}
// A level below the old six-decimal format's resolution is still a POSITIVE level, which GL clamps
// to 1 and draws; rendering it as "0.000000" discarded the patch instead.
TEST(PassthroughTessControlEsslTest, TinyPositiveLevelsDoNotFlushToZero) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
FloatVec4(1e-7f, 1.0f, 1.0f, 1.0f),
FloatVec2(1.0f, 1.0f));
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.000000;")) << out;
}
// ES 3.1 reaches tessellation only through the extension; the caller has already established // ES 3.1 reaches tessellation only through the extension; the caller has already established
// that the driver runs the evaluation stage at all, so the only question is the spelling. // that the driver runs the evaluation stage at all, so the only question is the spelling.
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) { TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
const String out = BuildPassthroughTessControlEssl(310, 3, "", ""); const String out = BuildPassthroughTessControlEssl(310, 3, "", "", kDefaultOuter, kDefaultInner);
EXPECT_EQ(out.find("#version 310 es"), 0u) << out; EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out; EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
} }
@@ -1325,7 +1385,7 @@ TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) { TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; "; const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
const String outMembers = " highp vec4 gl_Position; "; const String outMembers = " highp vec4 gl_Position; ";
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers); const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers, kDefaultOuter, kDefaultInner);
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out; EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out; EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
} }
@@ -1252,3 +1252,194 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
EXPECT_EQ(g_driverIndexedColorMasks[2].a, GL_TRUE) << "a native buffer keeps its alpha writes"; EXPECT_EQ(g_driverIndexedColorMasks[2].a, GL_TRUE) << "a native buffer keeps its alpha writes";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
} }
// --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) ---------------------------------
//
// Four of them were missing from the bound-target path while its DSA sibling
// (glNamedFramebufferTexture) implemented all four, which is what KHR-GL4x.geometry_shader.
// layered_fbo.fb_texture_* fails on. Two of them - the attachment-range check and the
// default-framebuffer rejection - newly REFUSE calls that used to succeed, so they are pinned
// here rather than left to the conformance suite.
TEST_F(FramebufferTest, FramebufferTextureRejectsTheDefaultFramebuffer) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 64, 32);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// MobileGL models framebuffer 0 as a real FramebufferObject, so the null test that used to
// stand in for this could never fire and the attach silently "succeeded".
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
DrainPendingGlErrors();
}
TEST_F(FramebufferTest, FramebufferTextureRejectsAColourAttachmentPastTheLimit) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 64, 32);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The same limit ValidateColorAttachmentInRange reads, so the test cannot disagree with the
// implementation about where the boundary is.
const GLint limit = MG_Backend::pActiveBackendObject
? static_cast<GLint>(
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxColorAttachments)
: static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
ASSERT_GT(limit, 0);
ASSERT_LT(limit, 32) << "the test needs a colour attachment enum past the limit to exist";
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER,
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit), texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
DrainPendingGlErrors();
// The last legal one still attaches, so the boundary is off-by-none.
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER,
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit - 1), texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTextureReportsInvalidValueForANameThatWasNeverGenerated) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// INVALID_VALUE, not INVALID_OPERATION: the entry point used to resolve the texture object
// first and report the miss with the wrong code, pre-empting ValidateTextureName.
const GLuint neverGenerated = std::numeric_limits<GLuint>::max();
ASSERT_FALSE(MG_State::pGLContext->ValidateTextureName(neverGenerated));
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, neverGenerated, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
}
TEST_F(FramebufferTest, FramebufferTextureRejectsALevelTheTextureDoesNotHave) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
// Two levels of immutable storage: level 1 is legal, level 2 is not.
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the last level the texture has is legal";
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 2);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, -1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
}
// The four conditions above are stated once in GL 4.6 core 9.2.8 for the WHOLE family, and
// glFramebufferTexture2D / 3D / TextureLayer reach the attachment through their own code rather
// than through the shared helper - so each of them has to be asked separately or one entry point
// answers differently from its aliases. glFramebufferTexture2D is the most-used of the five, and
// the default-framebuffer case is the damaging one: the attach used to succeed and replace
// framebuffer 0's colour attachment, which nothing ever puts back.
TEST_F(FramebufferTest, FramebufferTexture2DRejectsTheDefaultFramebufferAndBadAttachments) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
ASSERT_NE(defaultFramebuffer, nullptr);
const auto& colorBefore = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::Color0);
const Bool hadTextureBefore = colorBefore.IsTexture();
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
DrainPendingGlErrors();
// ...and, more to the point, the default framebuffer still describes the surface.
const auto& colorAfter = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::Color0);
EXPECT_EQ(colorAfter.IsTexture(), hadTextureBefore);
if (colorAfter.IsTexture() && hadTextureBefore) {
EXPECT_NE(colorAfter.GetTexture()->GetExternalIndex(), texture)
<< "the refused attach must not have replaced framebuffer 0's colour attachment";
}
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const GLint limit = MG_Backend::pActiveBackendObject
? static_cast<GLint>(
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxColorAttachments)
: static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit), GL_TEXTURE_2D,
texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
DrainPendingGlErrors();
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE) << "the texture has two levels, not three";
DrainPendingGlErrors();
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, -1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
// The legal call still works, so the boundary is off-by-none.
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTextureLayerRejectsTheDefaultFramebufferAndBadLevels) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &texture);
MG_Impl::GLImpl::TextureStorage3D(texture, 2, GL_RGBA8, 16, 16, 4);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The attach path used to bypass every one of these while the DETACH path (texture == 0) went
// through the fixed helper, so one entry point answered two different ways.
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
DrainPendingGlErrors();
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 2, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 1, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The DSA sibling is the entry point the bound-target family was aligned WITH, so an out-of-range
// immutable level has to be rejected there too - otherwise the alignment created a fresh
// asymmetry in the opposite direction.
TEST_F(FramebufferTest, NamedFramebufferTextureRejectsALevelTheTextureDoesNotHave) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 2);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
}
@@ -101,9 +101,14 @@ namespace {
return builtIns; return builtIns;
} }
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) { const FloatVec4 kDefaultOuter(1.0f, 1.0f, 1.0f, 1.0f);
const FloatVec2 kDefaultInner(1.0f, 1.0f);
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices,
Uint32 perVertexMembers = ProgramFactory::kDefaultPerVertexMembers) {
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices); const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices, kDefaultOuter,
kDefaultInner, perVertexMembers);
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source}; ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
@@ -162,11 +167,85 @@ TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being // user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
// the right gate and both have to move together. // the right gate and both have to move together.
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) { TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
const String source = ProgramFactory::BuildPassthroughTessControlSource(4); const String source = ProgramFactory::BuildPassthroughTessControlSource(
4, kDefaultOuter, kDefaultInner, ProgramFactory::kDefaultPerVertexMembers);
EXPECT_EQ(source.find("layout(location"), String::npos) << source; EXPECT_EQ(source.find("layout(location"), String::npos) << source;
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source; EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
} }
// glPatchParameterfv's levels are compiled into this stage - Vulkan has no dynamic state for them -
// so two different level sets must produce two different sources AND two different cache keys.
// Without the second half a pipeline memoised at one set of levels would be handed back after the
// application changed them, and the tessellation would silently stay at the old levels.
TEST_F(PassthroughTessControlTest, BakesTheDefaultTessLevelsInAndKeysOnThem) {
constexpr Uint32 kMembers = ProgramFactory::kDefaultPerVertexMembers;
const FloatVec4 outer(2.0f, 3.0f, 4.0f, 5.0f);
const FloatVec2 inner(6.5f, 7.25f);
const String source = ProgramFactory::BuildPassthroughTessControlSource(4, outer, inner,
ProgramFactory::kDefaultPerVertexMembers);
EXPECT_NE(source.find("gl_TessLevelOuter[0] = 2.0;"), String::npos) << source;
EXPECT_NE(source.find("gl_TessLevelOuter[3] = 5.0;"), String::npos) << source;
EXPECT_NE(source.find("gl_TessLevelInner[0] = 6.5;"), String::npos) << source;
EXPECT_NE(source.find("gl_TessLevelInner[1] = 7.25;"), String::npos) << source;
const Uint64 defaultKey =
ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembers);
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, outer, inner, kMembers), defaultKey);
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, inner, kMembers), defaultKey);
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(3, kDefaultOuter, kDefaultInner, kMembers), defaultKey);
EXPECT_EQ(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembers), defaultKey);
// ...and the gl_PerVertex member set is in the same key, for the same reason: two programs at
// different GLSL versions need differently-shaped modules, and a pipeline memoised against one
// shape must not be handed back for the other.
constexpr Uint32 kMembersWithCull =
ProgramFactory::kDefaultPerVertexMembers |
static_cast<Uint32>(ProgramFactory::PerVertexMemberBit::CullDistance);
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembersWithCull),
defaultKey);
}
// The generated stage still has to COMPILE with non-default levels: an integral level spelled
// without a decimal point is an int literal, and `gl_TessLevelOuter[0] = 2;` does not compile.
TEST_F(PassthroughTessControlTest, CompilesWithNonDefaultLevels) {
using namespace MG_Util::ShaderTranspiler;
const String source =
ProgramFactory::BuildPassthroughTessControlSource(4, FloatVec4(2.0f, 2.0f, 2.0f, 2.0f),
FloatVec2(2.0f, 2.0f),
ProgramFactory::kDefaultPerVertexMembers);
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << source;
}
// The generator honours the mask it is given, in both directions. This is what covers the
// pre-cutoff three-member form now that no authorable tessellation evaluation stage produces it
// (ARB_tessellation_shader is GL 4.0 and gl_CullDistance joins the block at 400), and it is also
// the fallback ReflectPassthroughTessControlNeed uses when it cannot read a module's block.
TEST_F(PassthroughTessControlTest, RedeclaresExactlyTheRequestedMembers) {
using Bit = ProgramFactory::PerVertexMemberBit;
constexpr Uint32 kWithCull = ProgramFactory::kDefaultPerVertexMembers | static_cast<Uint32>(Bit::CullDistance);
constexpr Uint32 kBuiltInCullDistance = 4;
constexpr Uint32 kBuiltInClipDistance = 3;
const Vector<Uint32> withoutCull = CompileGeneratedSource(4, ProgramFactory::kDefaultPerVertexMembers);
ASSERT_FALSE(withoutCull.empty());
const std::set<Uint32> withoutCullBuiltIns = DeclaredBuiltIns(withoutCull);
EXPECT_TRUE(withoutCullBuiltIns.contains(kBuiltInClipDistance));
EXPECT_FALSE(withoutCullBuiltIns.contains(kBuiltInCullDistance))
<< "the three-member mask must not emit gl_CullDistance";
for (const auto& [structId, shape] : BuiltInBlockShapes(withoutCull)) {
EXPECT_EQ(StructMemberCount(withoutCull, structId), 3u) << "structId=" << structId;
}
const Vector<Uint32> withCull = CompileGeneratedSource(4, kWithCull);
ASSERT_FALSE(withCull.empty());
EXPECT_TRUE(DeclaredBuiltIns(withCull).contains(kBuiltInCullDistance))
<< "the four-member mask must emit gl_CullDistance";
for (const auto& [structId, shape] : BuiltInBlockShapes(withCull)) {
EXPECT_EQ(StructMemberCount(withCull, structId), 4u) << "structId=" << structId;
}
}
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this // THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program // stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation // its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
@@ -179,13 +258,33 @@ TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) { TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation: // MORE THAN ONE VERSION, because the shape is a function of the neighbour's GLSL version and
// a vertex stage feeding an evaluation stage with no control stage in between. // a single-version case cannot see that. glslang gates gl_PerVertex's gl_CullDistance member
static const char* kVs = R"(#version 430 core // on a version cutoff, and this case used to link #version 430 ONLY - which is exactly why a
// generator hardcoded to the pre-cutoff three-member form looked correct while every program
// above it, including every ESSL program (the source processor rewrites those to
// "#version 460 core"), was silently mismatched.
//
// The expected member COUNT is deliberately not spelled per version any more. It moved once
// already (the fork's GL_ARB_cull_distance work lowered the cutoff from 450 to 400, so 430
// went from three members to four), and pinning it here only produced a test that failed for
// being right. What must hold - and is what this case now asserts - is that the generator
// reproduces whatever glslang produced, at every version, plus the floor that a per-vertex
// block always has at least gl_Position. A tessellation evaluation stage cannot be authored
// below #version 400 at all (ARB_tessellation_shader is GL 4.0), so 400 is the bottom of the
// reachable range; the pre-cutoff three-member form is covered through the explicit-mask case
// below instead.
for (const char* version : {"#version 400 core", "#version 430 core", "#version 460 core"}) {
SCOPED_TRACE(version);
// Deliberately the shape of
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation: a vertex stage
// feeding an evaluation stage with no control stage in between.
const String vs = String(version) + R"(
layout(location = 0) in vec4 g_in_position; layout(location = 0) in vec4 g_in_position;
void main() { gl_Position = g_in_position; } void main() { gl_Position = g_in_position; }
)"; )";
static const char* kTes = R"(#version 430 core const String tes = String(version) + R"(
layout(quads) in; layout(quads) in;
void main() { void main() {
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x); vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
@@ -193,55 +292,62 @@ void main() {
gl_Position = mix(p0, p1, gl_TessCoord.y); gl_Position = mix(p0, p1, gl_TessCoord.y);
} }
)"; )";
static const char* kFs = R"(#version 430 core const String fs = String(version) + R"(
layout(location = 0) out vec4 g_fs_out; layout(location = 0) out vec4 g_fs_out;
void main() { g_fs_out = vec4(0, 1, 0, 1); } void main() { g_fs_out = vec4(0, 1, 0, 1); }
)"; )";
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER}; const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
const Vector<const char*> sources{kVs, kTes, kFs}; const Vector<const String*> sources{&vs, &tes, &fs};
Vector<SharedPtr<glslang::TShader>> shaders; Vector<SharedPtr<glslang::TShader>> shaders;
for (SizeT i = 0; i < types.size(); ++i) { for (SizeT i = 0; i < types.size(); ++i) {
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]}; ShaderAttrib attrib{.shaderType = types[i], .sourceStr = *sources[i]};
auto compiled = ShaderCompiler::CompileShader(attrib); auto compiled = ShaderCompiler::CompileShader(attrib);
ASSERT_TRUE(compiled) << compiled.error().log; ASSERT_TRUE(compiled) << (compiled ? String{} : compiled.error().log);
shaders.push_back(compiled.value()); shaders.push_back(compiled.value());
} }
ProgramAttrib programAttrib{.shaders = shaders}; ProgramAttrib programAttrib{.shaders = shaders};
auto linked = ShaderCompiler::LinkProgram(programAttrib); auto linked = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(linked) << linked.error().log; ASSERT_TRUE(linked) << (linked ? String{} : linked.error().log);
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()}; ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binary); ASSERT_TRUE(binary);
ASSERT_EQ(binary->size(), types.size()); ASSERT_EQ(binary->size(), types.size());
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only // The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
// built-in block that stage declares as an input, so the module holds exactly one such shape // built-in block that stage declares as an input, so the module holds exactly one such
// besides its own gl_PerVertex output - and both are the same shape, which is the point. // shape besides its own gl_PerVertex output - and both are the same shape, which is the
const auto tesShapes = BuiltInBlockShapes((*binary)[1]); // point.
ASSERT_FALSE(tesShapes.empty()); const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
const Vector<Uint32> frontendShape = tesShapes.begin()->second; ASSERT_FALSE(tesShapes.empty());
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first); const Vector<Uint32> frontendShape = tesShapes.begin()->second;
for (const auto& [structId, shape] : tesShapes) { const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree"; EXPECT_GE(frontendMembers, 1u) << "a gl_PerVertex block always carries at least gl_Position";
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers); for (const auto& [structId, shape] : tesShapes) {
} EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
}
const Vector<Uint32> passthrough = CompileGeneratedSource(4); // ...and the generator is driven the way PRODUCTION drives it: the mask comes from the
ASSERT_FALSE(passthrough.empty()); // evaluation stage's own module, not from a constant the test picked.
const auto passthroughShapes = BuiltInBlockShapes(passthrough); const Uint32 reflectedMembers = ProgramFactory::ReflectPerVertexInputMembers((*binary)[1]);
ASSERT_FALSE(passthroughShapes.empty()); EXPECT_NE(reflectedMembers, 0u) << "the input per-vertex block walk found nothing to match against";
const Vector<Uint32> passthrough = CompileGeneratedSource(4, reflectedMembers);
ASSERT_FALSE(passthrough.empty());
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
ASSERT_FALSE(passthroughShapes.empty());
Uint32 perVertexBlocksChecked = 0; Uint32 perVertexBlocksChecked = 0;
for (const auto& [structId, shape] : passthroughShapes) { for (const auto& [structId, shape] : passthroughShapes) {
// gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every // gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every
// struct that reaches here is a gl_PerVertex - gl_in's and gl_out's. // struct that reaches here is a gl_PerVertex - gl_in's and gl_out's.
EXPECT_EQ(shape, frontendShape) EXPECT_EQ(shape, frontendShape)
<< "the pass-through control stage's gl_PerVertex no longer matches the one the " << "the pass-through control stage's gl_PerVertex no longer matches the one the "
"frontend gives a linked vertex+evaluation program"; "frontend gives a linked vertex+evaluation program";
EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers) EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers)
<< "the pass-through control stage's gl_PerVertex has a different member count"; << "the pass-through control stage's gl_PerVertex has a different member count";
++perVertexBlocksChecked; ++perVertexBlocksChecked;
}
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
} }
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
} }
@@ -31,6 +31,7 @@
#include "Init.h" #include "Init.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h" #include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h" #include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_Impl/GLImpl/Drawing/GL_Drawing.h"
#include "MG_Impl/GLImpl/Program/GL_ProgramPipeline.h" #include "MG_Impl/GLImpl/Program/GL_ProgramPipeline.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
@@ -243,11 +244,17 @@ TEST_F(ProgramPipelineCompositeTest, AValueIdenticalWriteStillTakesTheSlotForIts
DeleteProgramPipelines(1, &pipeline); DeleteProgramPipelines(1, &pipeline);
} }
// glUseProgramStages here accepts a program that was never linked as separable (GL 4.6 core 7.4 // A stage program that recorded NONE of its writes, because nothing ever armed its tracking
// says it should not, and MobileGL validates only LINK_STATUS). Such a program has recorded // latch: the mirror has to fall back to carrying everything rather than carrying nothing.
// none of its writes, because nothing ever armed its tracking latch - so the mirror has to fall // Mirroring nothing would have been a fresh regression on a shape that worked before the dirty
// back to carrying everything rather than carrying nothing. Mirroring nothing would have been a // set existed.
// fresh regression on a shape that worked before the dirty set existed. //
// The shape used to be reachable through glUseProgramStages, which accepted a program that was
// never linked as separable. It no longer is: GL 4.6 core 7.4 requires the LATCHED
// PROGRAM_SEPARABLE flag and MobileGL now enforces it, and arming that flag is the very thing
// that arms the tracking latch - so no program the entry point accepts can be in this state. The
// fallback is therefore unreachable from GL and is exercised through the state layer instead,
// which is the only way left to keep it covered rather than deleting the coverage with the hole.
TEST_F(ProgramPipelineCompositeTest, ANonSeparableStageProgramStillMirrorsItsUniforms) { TEST_F(ProgramPipelineCompositeTest, ANonSeparableStageProgramStillMirrorsItsUniforms) {
const char* vsSource = R"(#version 430 core const char* vsSource = R"(#version 430 core
uniform vec4 u_vsOnly; uniform vec4 u_vsOnly;
@@ -270,9 +277,19 @@ void main() { gl_Position = u_vsOnly; }
GLuint pipeline = 0; GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline); GenProgramPipelines(1, &pipeline);
BindProgramPipeline(pipeline); BindProgramPipeline(pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs); // The fragment stage goes through the entry point; the vertex one cannot, so it is installed
// directly on the pipeline object - the same call glUseProgramStages makes once it is done
// validating, minus the validation this shape now fails.
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs); UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
ASSERT_EQ(GetError(), GL_NO_ERROR); ASSERT_EQ(GetError(), GL_NO_ERROR);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
ASSERT_EQ(GetError(), GL_INVALID_OPERATION)
<< "a program not linked as separable is not a legal pipeline stage";
{
const auto& pipelineObject = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
ASSERT_NE(pipelineObject, nullptr);
pipelineObject->SetStageProgram(ShaderStage::Vertex, MG_State::pGLContext->GetProgramObject(vs));
}
const float written[4] = {3.0f, 1.0f, 4.0f, 1.0f}; const float written[4] = {3.0f, 1.0f, 4.0f, 1.0f};
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written); ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
@@ -503,3 +520,205 @@ void main() { o_color = u_shared; }
UseProgram(0); UseProgram(0);
} }
// ---------------------------------------------------------------------------------------
// The vertex stage a pre-rasterization pipeline must have
// ---------------------------------------------------------------------------------------
// GL 4.6 core 7.4.1: a pipeline whose tessellation-control, tessellation-evaluation or geometry
// stage has an executable, but which supplies no executable VERTEX shader, makes every command
// that transfers vertices an INVALID_OPERATION. MobileGL checked only "a program is current" and
// "it linked", so a geometry+fragment pipeline drew happily and rendered nothing -
// KHR-GL4x.geometry_shader.api.fs_gs_draw_call and .pipeline_program_without_active_vs.
TEST_F(ProgramPipelineCompositeTest, AGeometryPipelineWithNoVertexStageRefusesToDraw) {
const char* kGs = R"(#version 430 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); }
)";
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kGs);
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
BindProgramPipeline(pipeline);
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
ASSERT_EQ(GetError(), GL_NO_ERROR);
// Not vacuous: the composite has to be a healthy linked program, so that the refusal below
// can only be the missing vertex stage and not a link that fell over on its own.
const auto composite = DrawProgram();
ASSERT_NE(composite, nullptr);
ASSERT_TRUE(composite->GetLinkStatus()) << "the composite itself must link for this test to mean anything";
ASSERT_TRUE(composite->HasLinkedShaderStage(ShaderStage::Geometry));
ASSERT_FALSE(composite->HasLinkedShaderStage(ShaderStage::Vertex));
DrawArrays(GL_POINTS, 0, 1);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION)
<< "a geometry stage with no vertex stage must refuse the draw";
// A dispatch shares the same "is there a program, did it link" helper and legitimately has no
// vertex stage; the rule must not have leaked onto it. There is no compute stage here, so the
// error is the compute check's own - what matters is that the draw rule did not fire first
// with a different meaning.
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
}
BindProgramPipeline(0);
DeleteProgramPipelines(1, &pipeline);
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
}
}
// ---------------------------------------------------------------------------------------------
// The composite's transform-feedback capture list.
//
// Two rules, and getting either wrong turns a working pipeline into one where EVERY draw reports
// GL_INVALID_OPERATION: an unresolvable capture name fails the composite's own link, and
// ValidateProgramForExecution rejects every draw through a pipeline whose composite did not link -
// while glValidateProgramPipeline keeps reporting TRUE.
// ---------------------------------------------------------------------------------------------
namespace {
const char* kCaptureVs = R"(#version 430 core
out gl_PerVertex { vec4 gl_Position; };
out float v_captured;
out float v_other;
void main() { gl_Position = vec4(0.0); v_captured = 1.0; v_other = 2.0; }
)";
// A geometry stage that re-emits nothing the vertex stage named, so a capture list taken from
// the VERTEX program cannot resolve against it.
const char* kPassthroughGs = R"(#version 430 core
layout(points) in;
layout(points, max_vertices = 1) out;
out gl_PerVertex { vec4 gl_Position; };
out float g_only;
void main() { gl_Position = vec4(0.0); g_only = 1.0; EmitVertex(); EndPrimitive(); }
)";
Vector<String> CompositeCaptureNames(MG_State::GLState::ProgramObject& composite) {
Vector<String> names;
for (SizeT i = 0; i < composite.GetTransformFeedbackVaryingCount(); ++i) {
if (const auto* varying = composite.GetTransformFeedbackVarying(i)) {
names.push_back(varying->name);
}
}
return names;
}
} // namespace
// glTransformFeedbackVaryings does not take effect until the program's NEXT link (GL 4.6 core
// 7.3/11.1.2.1) and deliberately bumps no version, so a request written after the stage program's
// last link is invisible to the composite cache's signature - yet the next rebuild would pick it
// up. The capture list would then depend on whether some unrelated event happened to invalidate
// the cache. Reading the LINKED snapshot removes the whole class, and makes the existing cache key
// sufficient: linked state only moves at a link, which is exactly what the key tracks.
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureListComesFromTheLinkedSnapshotNotThePendingRequest) {
const GLuint vs = CreateProgram();
{
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
ShaderSource(shader, 1, &kCaptureVs, nullptr);
CompileShader(shader);
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
AttachShader(vs, shader);
const char* captured = "v_captured";
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
LinkProgram(vs);
GLint linked = GL_FALSE;
GetProgramiv(vs, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
}
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
BindProgramPipeline(pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
ASSERT_EQ(GetError(), GL_NO_ERROR);
{
const auto composite = DrawProgram();
ASSERT_NE(composite, nullptr);
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}));
}
// A NEW request with no relink. GL says the program still captures v_captured.
const char* other = "v_other";
TransformFeedbackVaryings(vs, 1, &other, GL_INTERLEAVED_ATTRIBS);
ASSERT_EQ(GetError(), GL_NO_ERROR);
// Force a composite rebuild through something entirely unrelated to the capture list: a new
// fragment stage program moves that slot's lifetime id, so the cache signature changes.
const GLuint fs2 = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs2);
ASSERT_EQ(GetError(), GL_NO_ERROR);
{
const auto composite = DrawProgram();
ASSERT_NE(composite, nullptr);
EXPECT_TRUE(composite->GetLinkStatus()) << "the composite must still link";
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}))
<< "an unlinked request must not reach the composite";
}
// Relinking the stage program IS what makes the new request take effect - and the composite
// follows, because the relink moves the link version the cache keys on.
LinkProgram(vs);
{
const auto composite = DrawProgram();
ASSERT_NE(composite, nullptr);
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_other"}));
}
BindProgramPipeline(0);
DeleteProgramPipelines(1, &pipeline);
}
// Transform feedback captures the output of the LAST vertex-processing stage (GL 4.6 core
// 11.1.2.1) - the last stage that EXISTS, not the last one that happens to carry a capture list.
// Falling through a geometry stage with no request and installing the vertex stage's list instead
// made the two halves disagree: this loop picks whose list, the link task resolves those names
// against the geometry intermediate. Either it captures where GL says it must not, or the
// composite fails to link and every draw through the pipeline reports GL_INVALID_OPERATION.
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureStageIsTheLastVertexProcessingStageThatExists) {
const GLuint vs = CreateProgram();
{
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
ShaderSource(shader, 1, &kCaptureVs, nullptr);
CompileShader(shader);
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
AttachShader(vs, shader);
const char* captured = "v_captured";
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
LinkProgram(vs);
GLint linked = GL_FALSE;
GetProgramiv(vs, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
}
// The geometry program was never given a capture list, and "v_captured" is not one of its
// outputs - so a composite seeded from the VERTEX program's list cannot resolve it.
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kPassthroughGs);
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
BindProgramPipeline(pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
ASSERT_EQ(GetError(), GL_NO_ERROR);
const auto composite = DrawProgram();
ASSERT_NE(composite, nullptr);
EXPECT_TRUE(composite->GetLinkStatus())
<< "the geometry stage is the capture stage and has no capture list, so the composite links "
"with none - it must not inherit the vertex stage's and fail resolving it";
EXPECT_EQ(composite->GetTransformFeedbackVaryingCount(), 0u)
<< "the capture stage is the geometry program, which declared nothing to capture";
BindProgramPipeline(0);
DeleteProgramPipelines(1, &pipeline);
}
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <algorithm>
#include <cstring> #include <cstring>
#include <map> #include <map>
#include <set> #include <set>
@@ -4569,3 +4570,445 @@ subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
<< "an inactive #if arm must not have an unconditional forwarding body appended for it"; << "an inactive #if arm must not have an unconditional forwarding body appended for it";
} }
// ---------------------------------------------------------------------------------------------
// gl_NumSamples: glslang declares the built-in only when it is NOT targeting SPIR-V, and MobileGL
// always targets SPIR-V, so every fragment shader that reads it used to die at compile time with
// "'gl_NumSamples' : undeclared identifier". InjectNumSamplesBuiltinShim lowers it onto a reserved
// default-block uniform instead; the draw path fills that uniform in.
// ---------------------------------------------------------------------------------------------
namespace {
Bool HasNumSamplesShim(const String& source) {
return source.find("uniform int mg_NumSamples;") != String::npos &&
source.find("#define gl_NumSamples mg_NumSamples") != String::npos;
}
void ExpectShaderCompiles(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib attrib{.shaderType = stage, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
} // namespace
TEST_F(ProgramUtilTest, PreprocessFragmentShaderInjectsNumSamplesShim) {
using namespace MG_Util::ShaderTranspiler;
// The shape KHR-GL46.sample_variables.mask.* uses: gl_NumSamples as the bound of the loop that
// writes gl_SampleMask.
String source = R"(#version 460 core
layout(location = 0) out highp vec4 o_color;
uniform int u_sampleMask;
void main() {
for (int i = 0; i < (gl_NumSamples + 31) / 32; ++i) {
gl_SampleMask[i] = u_sampleMask & gl_SampleMaskIn[i];
}
o_color = vec4(1, 0, 0, 1);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
}
TEST_F(ProgramUtilTest, NumSamplesShimIgnoresCommentedAndPartialTokens) {
using namespace MG_Util::ShaderTranspiler;
// Comment and string text is masked before the token scan, and the scan is whole-identifier:
// "gl_NumSamplesFoo" is a different name and must not drag the shim in.
String commented = R"(#version 460 core
out vec4 fragColor;
// gl_NumSamples used to be read here
/* gl_NumSamples */
void main() { fragColor = vec4(1.0); }
)";
String suffixed = R"(#version 460 core
out vec4 fragColor;
uniform int gl_NumSamplesFoo;
void main() { fragColor = vec4(float(gl_NumSamplesFoo)); }
)";
for (String* source : {&commented, &suffixed}) {
PreprocessShaderSource(ShaderStage::Fragment, *source);
EXPECT_EQ(source->find("mg_NumSamples"), String::npos) << *source;
}
}
TEST_F(ProgramUtilTest, NumSamplesShimDoesNotDoubleInject) {
using namespace MG_Util::ShaderTranspiler;
// Re-running the preprocessor over its own output must be a no-op for this pass; a second
// "uniform int mg_NumSamples;" would not compile.
String source = R"(#version 460 core
out vec4 fragColor;
void main() { fragColor = vec4(float(gl_NumSamples)); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
ASSERT_TRUE(HasNumSamplesShim(source)) << source;
const String once = source;
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source, once) << "the shim re-fired on an already-shimmed source";
// Same guard for an application that happens to own the name itself.
String applicationOwned = R"(#version 460 core
uniform int mg_NumSamples;
out vec4 fragColor;
void main() { fragColor = vec4(float(gl_NumSamples + mg_NumSamples)); }
)";
const String before = applicationOwned;
PreprocessShaderSource(ShaderStage::Fragment, applicationOwned);
EXPECT_EQ(applicationOwned, before);
}
TEST_F(ProgramUtilTest, NumSamplesShimIsFragmentStageOnly) {
using namespace MG_Util::ShaderTranspiler;
// gl_NumSamples exists in the fragment stage and nowhere else, so a vertex or geometry source
// naming it must be left for glslang to reject rather than quietly legalized.
for (const ShaderStage stage : {ShaderStage::Vertex, ShaderStage::Geometry, ShaderStage::Compute}) {
String source = R"(#version 460 core
out int v;
void main() { v = gl_NumSamples; }
)";
PreprocessShaderSource(stage, source);
EXPECT_EQ(source.find("mg_NumSamples"), String::npos) << static_cast<int>(stage) << ":\n" << source;
}
}
TEST_F(ProgramUtilTest, NumSamplesShimHonoursTheVersionAndExtensionGate) {
using namespace MG_Util::ShaderTranspiler;
struct Case {
const char* label;
const char* versionBlock;
Bool expectShim;
};
// Mirrors glslang's own gate (Initialize.cpp): desktop from 4.00, or from 1.30 with
// ARB_sample_shading; ESSL from 3.20, or from 3.10 with OES_sample_variables - which
// GL_ANDROID_extension_pack_es31a and `#extension all : warn` also turn on
// (TParseVersions::updateExtensionBehavior).
const Case cases[] = {
{"desktop 460 core", "#version 460 core\n", true},
{"desktop 400 core", "#version 400 core\n", true},
{"desktop 330 core, no extension", "#version 330 core\n", false},
{"desktop 330 core + ARB_sample_shading",
"#version 330 core\n#extension GL_ARB_sample_shading : require\n", true},
{"desktop 330 core + all : warn",
"#version 330 core\n#extension all : warn\n", true},
{"desktop 120, no extension", "#version 120\n", false},
{"desktop 120 + all : warn (below the 1.30 floor)",
"#version 120\n#extension all : warn\n", false},
{"ESSL 320", "#version 320 es\n", true},
{"ESSL 310, no extension", "#version 310 es\n", false},
{"ESSL 310 + OES_sample_variables",
"#version 310 es\n#extension GL_OES_sample_variables : require\n", true},
// The AEP spellings. glslang applies the directive's behavior to all twelve AEP members,
// GL_OES_sample_variables among them, so these are legal ES 3.1 shaders.
{"ESSL 310 + AEP : require",
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : require\n", true},
{"ESSL 310 + AEP : enable",
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : enable\n", true},
{"ESSL 310 + AEP : warn",
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : warn\n", true},
// ...but `disable` is not an opt-in, and the implication carries the behavior with it.
{"ESSL 310 + AEP : disable",
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : disable\n", false},
{"ESSL 310 + all : warn",
"#version 310 es\n#extension all : warn\n", true},
// An AEP member that does NOT imply sample variables must not open the gate.
{"ESSL 310 + EXT_geometry_shader only",
"#version 310 es\n#extension GL_EXT_geometry_shader : require\n", false},
{"ESSL 300", "#version 300 es\n", false},
{"ESSL 300 + AEP (below the 3.10 floor)",
"#version 300 es\n#extension GL_ANDROID_extension_pack_es31a : require\n", false},
};
for (const Case& testCase : cases) {
SCOPED_TRACE(testCase.label);
String source = String(testCase.versionBlock) + R"(out vec4 fragColor;
void main() { fragColor = vec4(float(gl_NumSamples)); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(HasNumSamplesShim(source), testCase.expectShim) << source;
}
}
// ---------------------------------------------------------------------------------------------
// ES preamble extension macros. Rewriting "#version 310 es" to "#version 460 core" makes glslang
// emit its DESKTOP preamble, which defines none of the OES/AEP extension macros - so a shader's
// own "#if !GL_OES_sample_variables" guard takes the branch it was written to avoid. The macros
// travel through glslang's CUSTOM PREAMBLE rather than the shader text, because "#define GL_..."
// in an application-supplied string is a hard error (reservedPpErrorCheck).
// ---------------------------------------------------------------------------------------------
TEST_F(ProgramUtilTest, EsSourceRegainsThePreambleMacrosForTheExtensionsItNames) {
using namespace MG_Util::ShaderTranspiler;
// KHR-GL46.es_31_compatibility.sample_variables.verification.extension in miniature: the
// deliberately-broken arm must stay unreached.
String source = R"(#version 310 es
#extension GL_OES_sample_variables : enable
precision highp float;
out vec4 fragColor;
#if !GL_OES_sample_variables
this is broken
#endif
void main() { fragColor = vec4(1.0); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(CollectEsPreambleMacroDefines(source), String("#define GL_OES_sample_variables 1\n")) << source;
// And the compiler really does feed it to glslang: without the preamble this source takes the
// "this is broken" arm and dies on a reserved word.
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
}
TEST_F(ProgramUtilTest, EsPreambleMacroInjectionStaysNarrow) {
using namespace MG_Util::ShaderTranspiler;
{
SCOPED_TRACE("only the extensions the source names, and never GL_ES");
String source = R"(#version 310 es
#extension GL_OES_sample_variables : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const String defines = CollectEsPreambleMacroDefines(source);
EXPECT_EQ(defines.find("GL_OES_shader_image_atomic"), String::npos) << defines;
// GL_ES stays undefined on purpose: the shader really is compiled as desktop now, and
// flipping "#ifdef GL_ES" branches would break far more than it fixes.
EXPECT_EQ(defines.find("#define GL_ES "), String::npos) << defines;
}
{
SCOPED_TRACE("an extension glslang's DESKTOP preamble already defines is not re-defined");
String source = R"(#version 310 es
#extension GL_EXT_shader_non_constant_global_initializers : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
// Nothing to restore, so the source is not even marked.
EXPECT_EQ(source.find("mobilegl-es-preamble"), String::npos) << source;
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
}
{
SCOPED_TRACE("a desktop source is untouched - it keeps the preamble it is entitled to");
String source = R"(#version 460 core
#extension GL_OES_sample_variables : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
const String before = source;
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source, before);
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
}
{
SCOPED_TRACE("a shader that merely contains the marker text cannot steer the preamble");
String source = R"(#version 460 core
/*mobilegl-es-preamble:310*/
#extension GL_OES_sample_variables : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
// The extractor is honest about what it finds - a source carrying a well-formed marker is
// indistinguishable from one this pipeline wrote, which is exactly why the payload is
// re-derived from the whitelist here rather than read out of the marker.
EXPECT_EQ(CollectEsPreambleMacroDefines(source), String("#define GL_OES_sample_variables 1\n"));
String malformed = R"(#version 460 core
/*mobilegl-es-preamble:not-a-version*/
#extension GL_OES_sample_variables : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
EXPECT_TRUE(CollectEsPreambleMacroDefines(malformed).empty());
}
}
// ---------------------------------------------------------------------------------------------
// A repeated #version directive. glShaderSource concatenates its strings with nothing added
// between them (GL 4.6 core 7.1), so a caller that heads BOTH strings with a #version splices the
// second into the tail of the first - which is what VK-GL-CTS's ShaderImageLoadStoreBase::
// BuildProgram does.
// ---------------------------------------------------------------------------------------------
TEST_F(ProgramUtilTest, RepeatedIdenticalVersionDirectiveIsElided) {
using namespace MG_Util::ShaderTranspiler;
// Byte-for-byte the concatenation the CTS produces: kGLSLPrec ends without a newline, so the
// subcase's own "#version 310 es" lands mid-line.
String source =
"#version 310 es\n\nprecision highp float;\nprecision highp uimage2DArray;#version 310 es\n"
"layout(location = 0) in vec4 i_position;\n"
"void main() { gl_Position = i_position; }\n";
const SizeT lineCountBefore = static_cast<SizeT>(std::count(source.begin(), source.end(), '\n'));
PreprocessShaderSource(ShaderStage::Vertex, source);
// Exactly one #version survives, and the line count is untouched so __LINE__ and every
// glslang diagnostic still point where the application wrote them.
EXPECT_EQ(source.find("#version", source.find("#version") + 1), String::npos) << source;
EXPECT_EQ(static_cast<SizeT>(std::count(source.begin(), source.end(), '\n')), lineCountBefore) << source;
ExpectShaderCompiles(GL_VERTEX_SHADER, source);
}
TEST_F(ProgramUtilTest, OnlyAnExactVersionRepeatIsElided) {
using namespace MG_Util::ShaderTranspiler;
{
SCOPED_TRACE("a DIFFERENT second version is left for glslang to reject");
String source =
"#version 310 es\nprecision highp float;\n#version 320 es\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_NE(source.find("#version 320 es"), String::npos) << source;
}
{
SCOPED_TRACE("a lone non-first #version is still a lone non-first #version");
// KHR-GL33.shaders.preprocessor.directive.version_not_first_statement_1 requires this to
// fail to compile, and it only does so because the directive is left where it was.
String source =
"precision mediump float;\n#version 330\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n";
PreprocessShaderSource(ShaderStage::Fragment, source);
const SizeT versionPos = source.find("#version");
ASSERT_NE(versionPos, String::npos) << source;
EXPECT_NE(versionPos, SizeT{0}) << "the directive must not have been moved to the front:\n" << source;
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
EXPECT_FALSE(res.has_value()) << "a #version preceded by real tokens must still be rejected:\n" << source;
}
{
SCOPED_TRACE("a MALFORMED repeat is left alone");
String source =
"#version 330 core\nout vec4 c;\n#version 330 foobar\nvoid main() { c = vec4(1.0); }\n";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_NE(source.find("#version 330 foobar"), String::npos) << source;
}
}
// glslang applies an #extension directive's behavior to every extension the named one IMPLIES
// (TParseVersions::updateExtensionBehavior, Versions.cpp:1039-1064). Both consumers of the
// extension sets have to see that expansion or the AEP spelling of a shader behaves differently
// from the byte-equivalent one that names its members directly.
TEST_F(ProgramUtilTest, AepFansOutToItsMemberExtensionMacros) {
using namespace MG_Util::ShaderTranspiler;
// The CTS-shaped guard, opted in the AEP way. Before the fan-out this took the broken arm.
String source = R"(#version 310 es
#extension GL_ANDROID_extension_pack_es31a : require
precision highp float;
out vec4 fragColor;
#if !GL_OES_sample_variables
this is broken
#endif
#if !GL_OES_shader_multisample_interpolation
this is also broken
#endif
void main() { fragColor = vec4(float(gl_NumSamples)); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
// Both halves of the AEP path: the built-in shim AND the restored member macros.
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
const String defines = CollectEsPreambleMacroDefines(source);
for (const char* member : {"GL_ANDROID_extension_pack_es31a", "GL_OES_sample_variables",
"GL_OES_shader_image_atomic", "GL_OES_shader_multisample_interpolation",
"GL_OES_texture_storage_multisample_2d_array", "GL_EXT_geometry_shader",
"GL_EXT_gpu_shader5", "GL_EXT_primitive_bounding_box",
"GL_EXT_shader_io_blocks", "GL_EXT_tessellation_shader",
"GL_EXT_texture_buffer", "GL_EXT_texture_cube_map_array"}) {
EXPECT_NE(defines.find(String("#define ") + member + " 1\n"), String::npos)
<< member << " missing from:\n" << defines;
}
// GL_KHR_blend_equation_advanced is an AEP member glslang propagates to, but its macro is in
// the DESKTOP preamble too - so the rewrite never took it away and it must not be restored.
EXPECT_EQ(defines.find("GL_KHR_blend_equation_advanced"), String::npos) << defines;
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
}
TEST_F(ProgramUtilTest, ExtensionImplicationIsTransitiveAndStaysNamed) {
using namespace MG_Util::ShaderTranspiler;
{
SCOPED_TRACE("geometry/tessellation imply the matching io_blocks");
// glslang re-enters updateExtensionBehavior for each implication, so the graph is walked
// to a fixed point rather than one level deep.
String source = R"(#version 310 es
#extension GL_OES_geometry_shader : require
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const String defines = CollectEsPreambleMacroDefines(source);
EXPECT_NE(defines.find("#define GL_OES_geometry_shader 1\n"), String::npos) << defines;
EXPECT_NE(defines.find("#define GL_OES_shader_io_blocks 1\n"), String::npos) << defines;
// The EXT spelling is a different extension and must not come along.
EXPECT_EQ(defines.find("GL_EXT_shader_io_blocks"), String::npos) << defines;
}
{
SCOPED_TRACE("a source that names nothing implied still gets nothing");
String source = R"(#version 310 es
#extension GL_OES_sample_variables : enable
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const String defines = CollectEsPreambleMacroDefines(source);
EXPECT_EQ(defines, String("#define GL_OES_sample_variables 1\n")) << defines;
}
{
SCOPED_TRACE("`all` opens the built-in gate but does not define every ES macro");
// The two questions differ: `all : warn` really does turn every extension on in glslang,
// but the preamble macros are defined before any #extension line runs, so `all` says
// nothing about which ones the ES -> desktop rewrite took away.
String source = R"(#version 310 es
#extension all : warn
out vec4 fragColor;
void main() { fragColor = vec4(float(gl_NumSamples)); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
EXPECT_EQ(source.find("mobilegl-es-preamble"), String::npos) << source;
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
}
}
// The mid-line #version probe must search THE LINE, not the rest of the file: an unbounded
// std::string::find makes InspectShaderLanguage quadratic on the ordinary resolved-shader-pack
// shape (one leading #version, no further '#' anywhere). This pins both halves - the detection
// still fires, and it fires on a source whose only other content is a long directive-free body.
TEST_F(ProgramUtilTest, MidLineVersionDetectionSurvivesALongDirectiveFreeBody) {
using namespace MG_Util::ShaderTranspiler;
String body;
body.reserve(64 * 1024);
for (int line = 0; line < 2000; ++line) {
body += " float v" + std::to_string(line) + " = 0.0;\n";
}
// The CTS concatenation shape, followed by a body with no '#' in it at all.
String source = "#version 310 es\nprecision highp float;#version 310 es\nout vec4 fragColor;\nvoid main() {\n" +
body + " fragColor = vec4(1.0);\n}\n";
const SizeT lineCountBefore = static_cast<SizeT>(std::count(source.begin(), source.end(), '\n'));
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version", source.find("#version") + 1), String::npos) << source.substr(0, 200);
EXPECT_EQ(static_cast<SizeT>(std::count(source.begin(), source.end(), '\n')), lineCountBefore);
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
}
+111
View File
@@ -11,6 +11,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstdlib> #include <cstdlib>
#include <ios>
#include "Includes.h" #include "Includes.h"
#include "Init.h" #include "Init.h"
@@ -814,3 +815,113 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
} }
EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected); EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected);
} }
// ---------------------------------------------------------------------------------------------
// GL_ARB_pipeline_statistics_query, core since 4.6. The eleven counter targets had no arm in
// glBeginQuery's accepted-target list, so the very first glBeginQuery(GL_VERTICES_SUBMITTED)
// raised GL_INVALID_ENUM and killed
// pipeline_statistics_query_tests_ARB.api_coverage_invalid_glbeginquery_calls before it could
// check anything. MobileGL instruments none of the counters and says so through the mechanism
// GL 4.6 core 4.2.1 provides for exactly this: GL_QUERY_COUNTER_BITS = 0, which the conformance
// suite reads and treats as "skip the functional half of this target".
// ---------------------------------------------------------------------------------------------
TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) {
static constexpr GLenum kTargets[] = {
GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED,
GL_VERTEX_SHADER_INVOCATIONS, GL_TESS_CONTROL_SHADER_PATCHES,
GL_TESS_EVALUATION_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS,
GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES,
GL_CLIPPING_OUTPUT_PRIMITIVES,
};
for (const GLenum target: kTargets) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(target, id);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
<< "glBeginQuery must accept pipeline-statistics target 0x" << std::hex << target;
GLint current = 0;
MG_Impl::GLImpl::GetQueryiv(target, GL_CURRENT_QUERY, &current);
EXPECT_EQ(static_cast<GLuint>(current), id) << "GL_CURRENT_QUERY has to track this target too";
MG_Impl::GLImpl::EndQuery(target);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(id), GL_TRUE);
GLint counterBits = -1;
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(counterBits, 0) << "an uninstrumented counter reports zero bits, per GL 4.6 core 4.2.1";
// The result is immediately available (nothing was ever submitted to wait on) and reads
// as the zero the zero counter-bit answer marks indeterminate.
GLuint available = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, static_cast<GLuint>(GL_TRUE));
GLuint result = 0xDEADBEEFu;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
}
// The negative half the conformance case actually asserts: an object already latched onto one
// pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what
// per-target active slots buy - a single shared slot would have reported "a query is already
// active on this target" for an unrelated target instead.
TEST_F(QueryTest, PipelineStatisticsQueryObjectRefusesASecondTargetAndTargetsAreIndependent) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, id);
MG_Impl::GLImpl::EndQuery(GL_VERTICES_SUBMITTED);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, id);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Two different objects on two different targets are simultaneously active, because each
// target owns its own slot.
GLuint first = 0;
GLuint second = 0;
MG_Impl::GLImpl::GenQueries(1, &first);
MG_Impl::GLImpl::GenQueries(1, &second);
MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, first);
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, second);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint current = 0;
MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, &current);
EXPECT_EQ(static_cast<GLuint>(current), first);
MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, &current);
EXPECT_EQ(static_cast<GLuint>(current), second);
// Deleting an ACTIVE query implicitly ends it and releases its slot; the sibling target is
// untouched.
MG_Impl::GLImpl::DeleteQueries(1, &first);
MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, &current);
EXPECT_EQ(current, 0);
MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, &current);
EXPECT_EQ(static_cast<GLuint>(current), second);
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_SUBMITTED);
MG_Impl::GLImpl::DeleteQueries(1, &second);
MG_Impl::GLImpl::DeleteQueries(1, &id);
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
}
}
// glCreateQueries keeps its own, shorter accepted-target list on purpose: it is unchanged here,
// and this pins that the pipeline-statistics addition did not leak into it.
TEST_F(QueryTest, EndQueryOnAPipelineStatisticsTargetWithNoActiveQueryIsInvalidOperation) {
MG_Impl::GLImpl::EndQuery(GL_FRAGMENT_SHADER_INVOCATIONS);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
+376 -3
View File
@@ -36,13 +36,21 @@
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
#include <MG_Util/Types.h> #include <MG_Util/Types.h>
#include <limits>
#include <set> #include <set>
namespace { namespace {
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject { class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
public: public:
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params): // `type` defaults to Unknown, which is what every existing case wanted: a limits-only
m_params(params) {} // double with no backend identity. A case that captures a CompileEnv from it and then
// compares the result against glGetIntegerv has to pass a REAL type, because
// CompileEnv::HasBackend() is what BuildTBuiltInResource bounds gl_MaxVertexAttribs by
// while the getter bounds it by "a backend object exists" - two spellings of the same
// thing in production, and only in production.
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params,
MobileGL::BackendType type = MobileGL::BackendType::Unknown):
m_params(params), m_type(type) {}
void Initialize() override {} void Initialize() override {}
MobileGL::Bool InitCapabilities() override { return true; } MobileGL::Bool InitCapabilities() override { return true; }
@@ -55,10 +63,11 @@ namespace {
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
return m_params; return m_params;
} }
MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; } MobileGL::BackendType GetBackendType() const override { return m_type; }
private: private:
MobileGL::MG_Backend::DynamicBackendParameters m_params; MobileGL::MG_Backend::DynamicBackendParameters m_params;
MobileGL::BackendType m_type = MobileGL::BackendType::Unknown;
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{}; MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{};
MobileGL::RendererInfo m_info{ MobileGL::RendererInfo m_info{
.RendererName = "Test", .RendererName = "Test",
@@ -722,6 +731,62 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
} }
// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from
// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and
// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at
// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource
// with no device consulted at all, which let glslang accept a gl_CullDistance write that then
// discarded every primitive it touched.
TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities caps;
caps.MaxClipDistances = 8;
caps.MaxCullDistances = 8;
caps.MaxCombinedClipAndCullDistances = 8;
caps.SupportsShaderClipDistance = false;
caps.SupportsShaderCullDistance = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
// Clip only: cull stays zero, and the combined limit still describes the clip capacity.
caps.SupportsShaderClipDistance = true;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
caps.SupportsShaderCullDistance = true;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
}
// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the
// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the
// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it.
TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) {
using namespace MobileGL;
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
MG_External::GLESCapabilities caps;
backend.ApplyGLESCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
caps.SupportsClipDistance = true;
caps.MaxClipDistances = 8;
caps.MaxCullDistances = 8;
caps.MaxCombinedClipAndCullDistances = 8;
backend.ApplyGLESCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
}
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded // GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test // GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan // devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
@@ -1234,6 +1299,256 @@ void main() {
MG_State::pGLContext = Move(previousContext); MG_State::pGLContext = Move(previousContext);
} }
// THE invariant every KHR-GL45.limits.* case checks, in one place. When the conformance table
// gives a limit both a glGetIntegerv pname and a GLSL built-in constant, it reads the query and
// then compiles a shader that writes the built-in into an SSBO and demands EXACT equality - so a
// limit answered from two unreconciled tables fails the SECOND half of the case, with a message
// about a number rather than about the two tables. Seven of them did: gl_MaxVertexAttribs said 64
// against a query of 32, gl_MaxDrawBuffers 32 against 8, gl_MaxCombinedTextureImageUnits 80
// against 96, gl_MaxVaryingComponents 60 against 64, gl_MaxCombinedShaderOutputResources 8
// against 29.
//
// KEEP THIS TABLE GROWING. Every pname added to GL_Getter that also has a gl_Max* built-in
// belongs here; that is what stops the next one from drifting.
TEST(GetterSanity, EveryLimitWithABuiltinAgreesWithItsQuery) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject =
MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{}, BackendType::DirectGLES);
struct LimitPair {
GLenum pname;
const char* builtin;
};
const LimitPair pairs[] = {
{GL_MAX_VERTEX_ATTRIBS, "gl_MaxVertexAttribs"},
{GL_MAX_VERTEX_UNIFORM_COMPONENTS, "gl_MaxVertexUniformComponents"},
{GL_MAX_VERTEX_UNIFORM_VECTORS, "gl_MaxVertexUniformVectors"},
{GL_MAX_VERTEX_OUTPUT_COMPONENTS, "gl_MaxVertexOutputComponents"},
{GL_MAX_VARYING_COMPONENTS, "gl_MaxVaryingComponents"},
{GL_MAX_VARYING_VECTORS, "gl_MaxVaryingVectors"},
{GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, "gl_MaxVertexTextureImageUnits"},
{GL_MAX_TEXTURE_IMAGE_UNITS, "gl_MaxTextureImageUnits"},
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "gl_MaxCombinedTextureImageUnits"},
{GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, "gl_MaxFragmentUniformComponents"},
{GL_MAX_FRAGMENT_UNIFORM_VECTORS, "gl_MaxFragmentUniformVectors"},
{GL_MAX_FRAGMENT_INPUT_COMPONENTS, "gl_MaxFragmentInputComponents"},
{GL_MAX_DRAW_BUFFERS, "gl_MaxDrawBuffers"},
{GL_MAX_IMAGE_UNITS, "gl_MaxImageUnits"},
// The SAME token (0x8F39) under two spellings, and the two glslang fields behind them
// must therefore carry the same value.
{GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS, "gl_MaxCombinedImageUnitsAndFragmentOutputs"},
{GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES, "gl_MaxCombinedShaderOutputResources"},
{GL_MAX_CLIP_DISTANCES, "gl_MaxClipDistances"},
{GL_MAX_CULL_DISTANCES, "gl_MaxCullDistances"},
{GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, "gl_MaxCombinedClipAndCullDistances"},
{GL_MAX_SAMPLES, "gl_MaxSamples"},
{GL_MIN_PROGRAM_TEXEL_OFFSET, "gl_MinProgramTexelOffset"},
{GL_MAX_PROGRAM_TEXEL_OFFSET, "gl_MaxProgramTexelOffset"},
{GL_MAX_GEOMETRY_INPUT_COMPONENTS, "gl_MaxGeometryInputComponents"},
{GL_MAX_GEOMETRY_OUTPUT_COMPONENTS, "gl_MaxGeometryOutputComponents"},
{GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS, "gl_MaxGeometryTextureImageUnits"},
{GL_MAX_GEOMETRY_OUTPUT_VERTICES, "gl_MaxGeometryOutputVertices"},
{GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS, "gl_MaxGeometryTotalOutputComponents"},
{GL_MAX_GEOMETRY_UNIFORM_COMPONENTS, "gl_MaxGeometryUniformComponents"},
{GL_MAX_PATCH_VERTICES, "gl_MaxPatchVertices"},
{GL_MAX_TESS_GEN_LEVEL, "gl_MaxTessGenLevel"},
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "gl_MaxTessControlInputComponents"},
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "gl_MaxTessControlOutputComponents"},
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "gl_MaxTessControlTextureImageUnits"},
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "gl_MaxTessControlUniformComponents"},
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "gl_MaxTessControlTotalOutputComponents"},
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "gl_MaxTessEvaluationInputComponents"},
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "gl_MaxTessEvaluationOutputComponents"},
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "gl_MaxTessEvaluationTextureImageUnits"},
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "gl_MaxTessEvaluationUniformComponents"},
{GL_MAX_TESS_PATCH_COMPONENTS, "gl_MaxTessPatchComponents"},
{GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "gl_MaxTransformFeedbackBuffers"},
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "gl_MaxTransformFeedbackInterleavedComponents"},
// gl_MaxAtomicCounterBindings is glslang's name for the binding count; the GL spelling is
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS.
{GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "gl_MaxAtomicCounterBindings"},
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "gl_MaxAtomicCounterBufferSize"},
};
// The compile runs against a captured env, exactly as the pipeline's does - that is what
// makes "the resource table" mean the same thing here as it does in production.
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
for (const LimitPair& pair : pairs) {
GLint reported = -424242;
MG_Impl::GLImpl::GetIntegerv(pair.pname, &reported);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
<< pair.builtin << "'s pname is not answerable at all";
// A negative array size is a compile error, so the stage only compiles when the built-in
// equals what the query just reported. Two-sided by construction: a resource table that
// is too permissive fails it exactly like one that is too tight. One shader per pair, so
// a failure names the limit instead of reporting "something disagreed".
const String source = String("#version 460 core\nout vec4 mgColor;\nconst int mgAgree = (") +
pair.builtin + " == " + std::to_string(reported) +
") ? 1 : -1;\nint mgProbe[mgAgree];\nvoid main() { mgProbe[0] = 0; mgColor = "
"vec4(float(mgProbe[0])); }\n";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_FRAGMENT_SHADER,
.sourceStr = source,
.env = env.get(),
});
EXPECT_TRUE(compiled) << pair.builtin << " does not equal glGetIntegerv's " << reported << ":\n"
<< (compiled ? String() : compiled.error().log);
}
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// GL_MAX_ELEMENT_INDEX is 64-bit state whose required value (2^32-1) does not fit a GLint, so it
// needs its own case in BOTH widths: the 64-bit query has to answer 4294967295 and the 32-bit one
// has to saturate, per the GL state-query conversion rules. It used to be a single `1024 * 1024;
// // TODO` in the 32-bit table, and glGetInteger64v - which is how the conformance suite reads it
// - widened that.
TEST(GetterSanity, MaxElementIndexIsTheFull32BitIndexCeiling) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint64 wide = -1;
MG_Impl::GLImpl::GetInteger64v(GL_MAX_ELEMENT_INDEX, &wide);
EXPECT_EQ(wide, static_cast<GLint64>(0xFFFFFFFFLL));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint narrow = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ELEMENT_INDEX, &narrow);
EXPECT_EQ(narrow, INT32_MAX) << "the 32-bit query must saturate, not truncate or wrap";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// GL 4.6 core table 23.53 gives GL_MAX_SAMPLES a minimum of four and the three per-category
// ceilings a minimum of ONE. Flooring the latter at four is the advertised-caps lie that made
// KHR-GL46.sample_variables.mask.rgba8i run at all: the frontend promised four integer samples,
// the backend clamped the realised allocation to the one the driver can back, and the application
// wrote per-sample data it could never read.
TEST(GetterSanity, PerCategoryMultisampleCeilingsAreProbedRatherThanFlooredAtFour) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::DynamicBackendParameters params;
params.MaxSamples = 4;
params.MaxColorTextureSamples = 4;
params.MaxDepthTextureSamples = 2;
params.MaxIntegerSamples = 1;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_INTEGER_SAMPLES, &reported);
EXPECT_EQ(reported, 1) << "an integer multisample texture is backed by one sample here, and "
"saying otherwise is what the application allocates against";
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &reported);
EXPECT_EQ(reported, 2);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &reported);
EXPECT_EQ(reported, 4);
// ...while GL_MAX_SAMPLES keeps its floor of four, which is the one the spec really requires.
params.MaxSamples = 1;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &reported);
EXPECT_EQ(reported, 4);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// GL_ARB_cull_distance below #version 450, which is the band the conformance suite actually
// compiles in: cull_distance.coverage emits its compute shader at "#version 420 core" with
// `#extension GL_ARB_cull_distance : require` and reads gl_MaxCullDistances. Registering the
// extension name alone was not enough - `require` started succeeding while the constants stayed
// gated on 450, so the shader traded one error for another.
//
// The three cases below are the whole contract: the macro must be true exactly where the feature
// is, the constants must exist under the extension, and using the feature WITHOUT the extension
// must still fail (otherwise the gate is decorative).
TEST(ShaderCompilerSanity, ArbCullDistanceIsUsableBelow450) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
const auto compileFragment = [&env](const String& source) {
return MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_FRAGMENT_SHADER,
.sourceStr = source,
.env = env.get(),
});
};
// The coverage shader's shape, reduced to a fragment stage: require the extension, then read
// the constant it brings.
const String withExtension = R"(#version 420 core
#extension GL_ARB_cull_distance : require
out vec4 mgColor;
void main() { mgColor = vec4(float(gl_MaxCullDistances + gl_MaxCombinedClipAndCullDistances)); }
)";
auto extensionCompiled = compileFragment(withExtension);
EXPECT_TRUE(extensionCompiled) << (extensionCompiled ? String() : extensionCompiled.error().log);
// The macro has to agree with that, or the standard `#ifdef` probe lies in one direction or
// the other. It is defined from 400 up, where the built-ins exist...
const String macroProbe420 = R"(#version 420 core
out vec4 mgColor;
#ifndef GL_ARB_cull_distance
#error GL_ARB_cull_distance should be defined at 420
#endif
void main() { mgColor = vec4(0.0); }
)";
auto macro420 = compileFragment(macroProbe420);
EXPECT_TRUE(macro420) << (macro420 ? String() : macro420.error().log);
// ...and NOT below it, where they do not. A shader whose `#ifdef GL_ARB_cull_distance` branch
// reads gl_MaxCullDistances used to take that branch at 330 and fail to compile.
const String macroProbe330 = R"(#version 330 core
out vec4 mgColor;
#ifdef GL_ARB_cull_distance
#error GL_ARB_cull_distance must not be advertised where the built-ins do not exist
#endif
void main() { mgColor = vec4(0.0); }
)";
auto macro330 = compileFragment(macroProbe330);
EXPECT_TRUE(macro330) << (macro330 ? String() : macro330.error().log);
// The gate is real: below 450 the constants are reachable ONLY through the extension.
const String withoutExtension = R"(#version 420 core
out vec4 mgColor;
void main() { mgColor = vec4(float(gl_MaxCullDistances)); }
)";
EXPECT_FALSE(compileFragment(withoutExtension))
<< "gl_MaxCullDistances must require GL_ARB_cull_distance below #version 450";
// ...and at 450 it is core, so no directive is needed.
const String core450 = R"(#version 450 core
out vec4 mgColor;
void main() { mgColor = vec4(float(gl_MaxCullDistances)); }
)";
auto coreCompiled = compileFragment(core450);
EXPECT_TRUE(coreCompiled) << (coreCompiled ? String() : coreCompiled.error().log);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL; using namespace MobileGL;
@@ -2745,3 +3060,61 @@ TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMi
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY)); EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY)); EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
} }
// GL_MAX_COMBINED_*_UNIFORM_COMPONENTS is components + blocks * (blockSize / 4). The product was
// formed in signed 32-bit, and a Vulkan host that reports a large VkPhysicalDeviceLimits::
// maxUniformBufferRange (a Mali driver answers 0xFFFFFFFF, which the loader saturates to
// INT32_MAX) made 14 * (2147483647 / 4) + 4096 wrap to -1073737742 - which is byte for byte what
// the conformance suite read back as "Limit value is: -1073737742 when it should not be smaller
// than 58368". GLES escaped it only because the ES driver answers 65536 for the block size.
TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
using namespace MobileGL;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS (0x8266), NOT the per-stage
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS (0x8263) this list used to name. The per-stage token is
// answered by a frontend constant and never reaches GetMaxCombinedUniformComponents at all, so
// both assertions on it were vacuous - and it displaced the ONE reader whose block count comes
// from the backend (ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks)) rather
// than from a frontend constant, i.e. the only call site where the saturation actually depends
// on data a driver supplies.
static constexpr GLenum kCombinedPnames[] = {
GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS,
GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS, GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS,
GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS, GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS,
};
// The GL 4.6 core table 23.64 floor, which all six combined pnames carry.
static constexpr GLint kCombinedFloor = 58368;
{
MG_Backend::DynamicBackendParameters params;
params.MaxUniformBlockSize = std::numeric_limits<GLint>::max();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
for (const GLenum pname: kCombinedPnames) {
GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
EXPECT_GT(reported, 0) << "pname 0x" << pname << " wrapped to a negative combined component count";
EXPECT_GE(reported, kCombinedFloor) << "pname 0x" << pname << " fell under the GL 4.6 floor";
}
MG_Backend::pActiveBackendObject.reset();
}
// An ordinary 64 KiB block size still produces the plain arithmetic, not a saturated value:
// saturation must be the ceiling, never the answer.
{
MG_Backend::DynamicBackendParameters params;
params.MaxUniformBlockSize = 65536;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, &reported);
// 4096 default-block components + 14 blocks x (65536 / 4) components each.
EXPECT_EQ(reported, 4096 + 14 * (65536 / 4));
EXPECT_LT(reported, std::numeric_limits<GLint>::max());
MG_Backend::pActiveBackendObject.reset();
}
MG_State::pGLContext.reset();
}
@@ -563,7 +563,12 @@ namespace {
// BuildTBuiltInResource does not read it. // BuildTBuiltInResource does not read it.
a.maxComputeWorkGroupInvocations = 128; a.maxComputeWorkGroupInvocations = 128;
b.maxComputeWorkGroupInvocations = 2048; b.maxComputeWorkGroupInvocations = 2048;
// (4) a spread of DynamicBackendParameters fields the front end never reads // (4) a spread of DynamicBackendParameters fields the front end never reads.
// MaxTextureImageUnits used to be here and is NOT any more: the GL 4.6 API-surface wave
// made BuildTBuiltInResource read it (gl_MaxTextureImageUnits expands from it), so it
// moved to TheFrontendFingerprintMovesWithEveryFrontendLimit. That migration is the
// third one this helper has survived; check BuildTBuiltInResource before adding a field
// here.
a.params.MaxColorTextureSamples = 1; a.params.MaxColorTextureSamples = 1;
b.params.MaxColorTextureSamples = 8; b.params.MaxColorTextureSamples = 8;
a.params.MaxTextureSize = 4096; a.params.MaxTextureSize = 4096;
@@ -572,8 +577,8 @@ namespace {
b.params.MaxViewports = 16; b.params.MaxViewports = 16;
a.params.MaxUniformBufferBindings = 24; a.params.MaxUniformBufferBindings = 24;
b.params.MaxUniformBufferBindings = 84; b.params.MaxUniformBufferBindings = 84;
a.params.MaxTextureImageUnits = 16; a.params.MaxRenderbufferSize = 4096;
b.params.MaxTextureImageUnits = 32; b.params.MaxRenderbufferSize = 16384;
return {a, b}; return {a, b};
} }
} // namespace } // namespace
@@ -677,6 +682,20 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
// wave4's 4fc3531d: glslang rejects gl_ClipDistance[i] past this at parse AND expands // wave4's 4fc3531d: glslang rejects gl_ClipDistance[i] past this at parse AND expands
// gl_MaxClipDistances from it, so it is both a compile gate and a baked constant. // gl_MaxClipDistances from it, so it is both a compile gate and a baked constant.
{"params.MaxClipDistances", [](CompileEnv& e) { e.params.MaxClipDistances += 1; }}, {"params.MaxClipDistances", [](CompileEnv& e) { e.params.MaxClipDistances += 1; }},
// The GL 4.6 API-surface wave: six more TBuiltInResource fields that used to be stock
// glslang literals. The cull pair is the MaxClipDistances story exactly (parse gate plus
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances); the texture-image-unit three
// and MaxSamples are baked constants (gl_MaxTextureImageUnits,
// gl_MaxVertexTextureImageUnits, gl_MaxCombinedTextureImageUnits, gl_MaxSamples - the
// last of which also sizes gl_SampleMask[]).
{"params.MaxCullDistances", [](CompileEnv& e) { e.params.MaxCullDistances += 1; }},
{"params.MaxCombinedClipAndCullDistances",
[](CompileEnv& e) { e.params.MaxCombinedClipAndCullDistances += 1; }},
{"params.MaxTextureImageUnits", [](CompileEnv& e) { e.params.MaxTextureImageUnits += 1; }},
{"params.MaxVertexTextureImageUnits", [](CompileEnv& e) { e.params.MaxVertexTextureImageUnits += 1; }},
{"params.MaxCombinedTextureImageUnits",
[](CompileEnv& e) { e.params.MaxCombinedTextureImageUnits += 1; }},
{"params.MaxSamples", [](CompileEnv& e) { e.params.MaxSamples += 1; }},
{"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }}, {"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }},
{"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }}, {"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }},
{"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }}, {"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }},
@@ -536,10 +536,15 @@ void main() { g_color = vec4(1); }
[] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM}, [] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); }, {"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
GL_INVALID_ENUM}, GL_INVALID_ENUM},
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION // A mode the enum check accepts falls through to the no-program path, which is now
// that used to win is still raised for the calls it is actually about. // a SILENT drop rather than an error: GL 4.6 core 7.3 and ES 3.1 7.3 both make a draw
// with no current program and no bound pipeline UNDEFINED, not erroneous, and
// es31cSeparateShaderObjsTests.StateInteraction reads glGetError() straight after
// useProgram(0) + bindProgramPipeline(0) + glDrawElements and requires GL_NO_ERROR.
// Dropping the draw is one of the shapes "undefined" may take; inventing an error is
// not. The enum check above still outranks it, which is what this case is really for.
{"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); }, {"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); },
GL_INVALID_OPERATION}, GL_NO_ERROR},
}); });
} }
+394
View File
@@ -26,6 +26,9 @@
#include "Includes.h" #include "Includes.h"
#include "Init.h" #include "Init.h"
#include <limits>
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h> #include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
@@ -632,3 +635,394 @@ TEST_F(RenderStateTest, TheFirstScissorWriteBumpsTheVersionEvenWhenTheValueDoesN
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0)); indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
EXPECT_EQ(indexed.GetVersion(), indexedSettled); EXPECT_EQ(indexed.GetVersion(), indexedSettled);
} }
// --- glPatchParameterfv (GL 4.6 core 11.2.2) ---------------------------------------------------
//
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL are the tessellation levels a
// program with an evaluation stage and NO control stage runs at. glPatchParameterfv was a stub
// that stored nothing and raised nothing, so the state could never move off its 1.0 default and
// both backends hardcoded 1.0 into the pass-through control stage they synthesize. The getters
// were absent too, which is what KHR-GL4x.tessellation_shader.single.
// default_values_of_context_wide_properties dies on.
TEST_F(RenderStateTest, PatchDefaultLevelsStartAtTheGLDefault) {
GLfloat outer[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
ExpectSingleGlError(GL_NO_ERROR);
for (const GLfloat level : outer) EXPECT_FLOAT_EQ(level, 1.0f);
GLfloat inner[2] = {-1.0f, -1.0f};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
ExpectSingleGlError(GL_NO_ERROR);
for (const GLfloat level : inner) EXPECT_FLOAT_EQ(level, 1.0f);
}
TEST_F(RenderStateTest, PatchDefaultLevelsRoundTripThroughEveryGetter) {
const GLfloat outerIn[4] = {2.0f, 3.5f, 4.0f, 5.25f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerIn);
ExpectSingleGlError(GL_NO_ERROR);
const GLfloat innerIn[2] = {6.5f, 7.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, innerIn);
ExpectSingleGlError(GL_NO_ERROR);
GLfloat outer[4] = {};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
EXPECT_FLOAT_EQ(outer[0], 2.0f);
EXPECT_FLOAT_EQ(outer[1], 3.5f);
EXPECT_FLOAT_EQ(outer[2], 4.0f);
EXPECT_FLOAT_EQ(outer[3], 5.25f);
GLfloat inner[2] = {};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
EXPECT_FLOAT_EQ(inner[0], 6.5f);
EXPECT_FLOAT_EQ(inner[1], 7.0f);
ExpectSingleGlError(GL_NO_ERROR);
// Float state read through the integer and boolean getters: glGetIntegerv rounds (GL 4.6 core
// 2.2.2) and glGetBooleanv delegates to it, so both must ANSWER rather than report
// INVALID_ENUM - which is exactly what the conformance suite asks them first.
GLint outerInts[4] = {};
MG_Impl::GLImpl::GetIntegerv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerInts);
EXPECT_EQ(outerInts[0], 2);
EXPECT_EQ(outerInts[1], 4) << "3.5 rounds away from zero";
EXPECT_EQ(outerInts[3], 5);
ExpectSingleGlError(GL_NO_ERROR);
GLboolean outerBools[4] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerBools);
EXPECT_EQ(outerBools[0], GL_TRUE);
ExpectSingleGlError(GL_NO_ERROR);
GLdouble outerDoubles[4] = {};
MG_Impl::GLImpl::GetDoublev(GL_PATCH_DEFAULT_OUTER_LEVEL, outerDoubles);
EXPECT_DOUBLE_EQ(outerDoubles[3], 5.25) << "glGetDoublev must widen all four, not just the first";
ExpectSingleGlError(GL_NO_ERROR);
// glGetInteger64v shares glGetIntegerv's accepted-pname set (GL 4.6 core 22.1), so it owes the
// same component count. Its own table listed neither pname, so three of the four words were
// left holding whatever the caller's buffer held - and no error said so.
GLint64 outerLongs[4] = {9, 9, 9, 9};
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_OUTER_LEVEL, outerLongs);
EXPECT_EQ(outerLongs[0], 2);
EXPECT_EQ(outerLongs[3], 5) << "glGetInteger64v must write all four, not just the first";
GLint64 innerLongs[2] = {9, 9};
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_INNER_LEVEL, innerLongs);
EXPECT_EQ(innerLongs[1], 7);
ExpectSingleGlError(GL_NO_ERROR);
// Put the context back where the rest of the binary expects it.
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
const GLfloat defaults2[2] = {1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
DrainPendingGlErrors();
}
// GL 4.6 core 2.2.2: a float state comes back through glGetBooleanv as GL_FALSE only when it is
// zero. Deriving the answer from glGetIntegerv - which rounds - reported GL_FALSE for a level of
// 0.25, which is neither zero nor anything the application asked to be rounded.
TEST_F(RenderStateTest, PatchDefaultLevelsBelowHalfAreStillTrueAsBooleans) {
const GLfloat fractional[4] = {0.25f, 0.0f, 0.4f, 0.25f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, fractional);
const GLfloat fractionalInner[2] = {0.25f, 0.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, fractionalInner);
ExpectSingleGlError(GL_NO_ERROR);
GLboolean outer[4] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
EXPECT_EQ(outer[0], GL_TRUE) << "0.25 is not zero";
EXPECT_EQ(outer[1], GL_FALSE) << "0.0 is the one value that is false";
EXPECT_EQ(outer[2], GL_TRUE);
GLboolean inner[2] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
EXPECT_EQ(inner[0], GL_TRUE);
EXPECT_EQ(inner[1], GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
const GLfloat defaults2[2] = {1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
DrainPendingGlErrors();
}
TEST_F(RenderStateTest, PatchParameterfvRejectsEveryOtherPname) {
const GLfloat levels[4] = {1.0f, 1.0f, 1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_VERTICES, levels);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::PatchParameterfv(GL_MAX_PATCH_VERTICES, levels);
ExpectSingleGlError(GL_INVALID_ENUM);
// The integer setter keeps its own, disjoint, accepted pname.
MG_Impl::GLImpl::PatchParameteri(GL_PATCH_DEFAULT_OUTER_LEVEL, 4);
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(RenderStateTest, PatchDefaultLevelsAreTreatedAsPipelineState) {
// Load-bearing: both backends compile these numbers into the pass-through tessellation control
// stage they synthesize, so a change has to invalidate an already-built program the same way a
// glPatchParameteri does. Bumping only the all-state version would leave DirectVulkan's
// pipeline memo - which keys on the PIPELINE-state version - handing back a pipeline built
// with the old levels.
MG_State::GLState::RenderState state;
const Uint initialPipelineVersion = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
EXPECT_GT(state.GetPipelineStateVersion(), initialPipelineVersion);
const Uint settled = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), settled) << "a redundant write is free";
state.SetPatchDefaultInnerLevel(FloatVec2(3.0f, 3.0f));
EXPECT_GT(state.GetPipelineStateVersion(), settled);
}
// glPatchParameterfv accepts NaN by design, and NaN is never equal to itself under IEEE `==`. A
// value-compared redundant-write guard therefore never settles: every re-set of the identical
// tuple bumps the pipeline-state version, and - one level down - DirectGLES's staleness clause
// re-transpiles, re-compiles and re-links the synthesized pass-through stage on every draw. Both
// compare BIT PATTERNS instead, which is what DirectVulkan's module key already hashes.
TEST_F(RenderStateTest, ARedundantNaNPatchLevelWriteSettlesInsteadOfBumpingForever) {
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
MG_State::GLState::RenderState state;
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
const Uint afterFirst = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), afterFirst)
<< "the identical NaN tuple is not a state change";
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
const Uint afterInner = state.GetPipelineStateVersion();
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), afterInner);
// A genuinely different tuple still moves, so the guard has not simply gone blind.
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 2.0f, 1.0f, 1.0f));
EXPECT_GT(state.GetPipelineStateVersion(), afterInner);
}
// --- desktop GL_PRIMITIVE_RESTART state --------------------------------------------------------
//
// The cap and its index are what a desktop application enables instead of ES's
// GL_PRIMITIVE_RESTART_FIXED_INDEX. Both halves have to be answerable, because the backends read
// them on every indexed draw to decide whether the index data needs rewriting.
TEST_F(RenderStateTest, PrimitiveRestartCapAndIndexAreBothQueryable) {
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Enable(GL_PRIMITIVE_RESTART);
MG_Impl::GLImpl::PrimitiveRestartIndex(1026u);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_TRUE);
GLint index = 0;
MG_Impl::GLImpl::GetIntegerv(GL_PRIMITIVE_RESTART_INDEX, &index);
EXPECT_EQ(index, 1026);
// The fixed-index cap is a separate piece of state and must not have moved.
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART_FIXED_INDEX), GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Disable(GL_PRIMITIVE_RESTART);
MG_Impl::GLImpl::PrimitiveRestartIndex(0u);
DrainPendingGlErrors();
}
// glMinSampleShading was a logging no-op while ARB_sample_shading was advertised and
// glEnable(GL_SAMPLE_SHADING) fell through RenderState::SetCapability's default arm, so an
// application could turn sample shading on, ask for a rate, and get neither - with every query
// agreeing that nothing had happened.
TEST_F(RenderStateTest, MinSampleShadingRoundTripsAndClamps) {
DrainPendingGlErrors();
// GL 4.6 core table 23.10: the initial value is 0.
GLfloat initial = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &initial);
EXPECT_FLOAT_EQ(initial, 0.0f);
MG_Impl::GLImpl::MinSampleShading(0.25f);
GLfloat value = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 0.25f);
// The fraction survives the double query too, and rounds - not truncates - for the integer one.
GLdouble asDouble = -1.0;
MG_Impl::GLImpl::GetDoublev(GL_MIN_SAMPLE_SHADING_VALUE, &asDouble);
EXPECT_NEAR(asDouble, 0.25, 1e-6);
GLint asInt = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MIN_SAMPLE_SHADING_VALUE, &asInt);
EXPECT_EQ(asInt, 0);
// A non-zero fraction is GL_TRUE, which the integer path would have rounded away first.
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_MIN_SAMPLE_SHADING_VALUE, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
// "value is clamped to [0, 1]" - not an error, a clamp.
MG_Impl::GLImpl::MinSampleShading(2.0f);
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 1.0f);
MG_Impl::GLImpl::MinSampleShading(-3.0f);
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::MinSampleShading(0.0f);
}
TEST_F(RenderStateTest, SampleShadingEnableIsStoredAndQueryable) {
DrainPendingGlErrors();
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
MG_Impl::GLImpl::Enable(GL_SAMPLE_SHADING);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_TRUE);
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_SAMPLE_SHADING, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
GLint asInt = 0;
MG_Impl::GLImpl::GetIntegerv(GL_SAMPLE_SHADING, &asInt);
EXPECT_EQ(asInt, GL_TRUE);
MG_Impl::GLImpl::Disable(GL_SAMPLE_SHADING);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// glClipControl and glPolygonOffsetClamp were DECLARE_GL_FUNCTION_STUB_HEAD entry points: they
// took their arguments, recorded nothing and raised no error, and the state variables they own
// (GL_CLIP_ORIGIN, GL_CLIP_DEPTH_MODE, GL_POLYGON_OFFSET_CLAMP) had no arm in any getter, so the
// very first query of a conformance case raised GL_INVALID_ENUM and killed it. These assertions
// are state-shaped on purpose - the rasterization half of clip control is a backend question, but
// the state machine has to round-trip regardless of what a backend does with it.
TEST_F(RenderStateTest, ClipControlStateRoundTripsAndDefaultsToLowerLeftNegativeOneToOne) {
DrainPendingGlErrors();
GLint origin = 0;
GLint depthMode = 0;
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_LOWER_LEFT);
EXPECT_EQ(depthMode, GL_NEGATIVE_ONE_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_UPPER_LEFT);
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Every getter flavour has to answer, not just the integer one - the conformance suite reads
// this state through all of them.
GLfloat asFloat = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_CLIP_ORIGIN, &asFloat);
EXPECT_EQ(static_cast<GLint>(asFloat), GL_UPPER_LEFT);
GLint64 asInt64 = 0;
MG_Impl::GLImpl::GetInteger64v(GL_CLIP_DEPTH_MODE, &asInt64);
EXPECT_EQ(static_cast<GLint>(asInt64), GL_ZERO_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(RenderStateTest, ClipControlRejectsBadEnumsAndLeavesTheStateAlone) {
DrainPendingGlErrors();
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
DrainPendingGlErrors();
MG_Impl::GLImpl::ClipControl(GL_FRONT, GL_ZERO_TO_ONE);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_FRONT);
ExpectSingleGlError(GL_INVALID_ENUM);
GLint origin = 0;
GLint depthMode = 0;
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_UPPER_LEFT) << "a rejected glClipControl must not change the state";
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE) << "a rejected glClipControl must not change the state";
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
DrainPendingGlErrors();
}
TEST_F(RenderStateTest, PolygonOffsetClampStoresTheClampAndTheFactorUnitsPair) {
DrainPendingGlErrors();
GLfloat clamp = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "the default clamp is zero, i.e. no clamping";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::PolygonOffsetClamp(1.5f, 2.5f, 0.5f);
GLfloat factor = 0.0f;
GLfloat units = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 1.5f);
EXPECT_FLOAT_EQ(units, 2.5f);
EXPECT_FLOAT_EQ(clamp, 0.5f) << "the fractional clamp must survive - the integer path rounds it away";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// glcPolygonOffsetClampTests reads GL_POLYGON_OFFSET_CLAMP through all five getters and
// requires no error from any of them; that is what used to kill the availability case.
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_POLYGON_OFFSET_CLAMP, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
GLint asInt = -1;
MG_Impl::GLImpl::GetIntegerv(GL_POLYGON_OFFSET_CLAMP, &asInt);
EXPECT_EQ(asInt, 1) << "0.5 rounds to nearest for the integer query";
GLint64 asInt64 = -1;
MG_Impl::GLImpl::GetInteger64v(GL_POLYGON_OFFSET_CLAMP, &asInt64);
EXPECT_EQ(asInt64, 1);
GLdouble asDouble = -1.0;
MG_Impl::GLImpl::GetDoublev(GL_POLYGON_OFFSET_CLAMP, &asDouble);
EXPECT_NEAR(asDouble, 0.5, 1e-6);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// GL 4.6 core 14.6.5 defines glPolygonOffset(factor, units) as EQUIVALENT to
// glPolygonOffsetClamp(factor, units, 0) - totally, not "except for the clamp". So it writes
// all three, and a clamp left over from an earlier glPolygonOffsetClamp must be gone.
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 3.0f);
EXPECT_FLOAT_EQ(units, 4.0f);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "glPolygonOffset IS PolygonOffsetClamp(factor, units, 0)";
// The same rule when factor and units do NOT change: the clamp still has to be cleared, which
// an early-out keyed on the factor/units pair alone would skip.
MG_Impl::GLImpl::PolygonOffsetClamp(3.0f, 4.0f, 0.75f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
ASSERT_FLOAT_EQ(clamp, 0.75f);
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "a no-op factor/units write must still clear the clamp";
MG_Impl::GLImpl::PolygonOffsetClamp(0.0f, 0.0f, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_TEXTURE_BUFFER_BINDING (0x8C2A) is the same token as GL_TEXTURE_BUFFER; as a glGetIntegerv
// pname it asks which BUFFER object is bound there, and it had no arm at all, so
// esextcTextureBufferParameters died on its first query.
TEST_F(RenderStateTest, TextureBufferBindingAnswersTheBoundBufferName) {
DrainPendingGlErrors();
GLint binding = -1;
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_ARB_spirv_extensions. Zero is legal and true: MobileGL relies on no SPIR-V extension, so
// glGetStringi(GL_SPIR_V_EXTENSIONS, i) is never legally reached.
TEST_F(RenderStateTest, NumSpirVExtensionsIsQueryableAndZero) {
DrainPendingGlErrors();
GLint count = -1;
MG_Impl::GLImpl::GetIntegerv(GL_NUM_SPIR_V_EXTENSIONS, &count);
EXPECT_EQ(count, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
File diff suppressed because it is too large Load Diff
@@ -25,6 +25,7 @@
#include "Init.h" #include "Init.h"
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h> #include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
@@ -190,6 +191,36 @@ namespace {
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 3); EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 3);
} }
// ...and the level a FRAMEBUFFER may attach is the view's own count, not the inherited
// TEXTURE_IMMUTABLE_LEVELS the test above pins. Bounding glFramebufferTexture by the latter
// accepted a level the view cannot reach, which attaches a 0x0 image: the framebuffer then
// reports COMPLETE and nothing can be drawn into it.
TEST_F(TextureViewTest, AFramebufferAttachIsBoundedByTheViewsOwnLevelCount) {
const GLuint storage = MakeImmutable2D(4, 32, 32);
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, /*minlevel=*/2,
/*numlevels=*/2, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
// The inherited query really does report the original's four levels...
ASSERT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 4);
// ...while the view itself has two.
ASSERT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 2);
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, view, 1);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, view, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
DrainPendingGlErrors();
}
TEST_F(TextureViewTest, ViewClampsItsLevelCountToWhatRemains) { TEST_F(TextureViewTest, ViewClampsItsLevelCountToWhatRemains) {
const GLuint storage = MakeImmutable2D(3, 16, 16); const GLuint storage = MakeImmutable2D(3, 16, 16);
const GLuint view = GenTexture(); const GLuint view = GenTexture();
@@ -1139,6 +1139,11 @@ namespace MobileGL::MG_Util::BackendLoader {
// optimistic 8 behind, so the frontend promised eight clip planes and every draw with a // optimistic 8 behind, so the frontend promised eight clip planes and every draw with a
// clipping program silently rendered nothing. The guarded probe below only ever widens it. // clipping program silently rendered nothing. The guarded probe below only ever widens it.
GLint maxClipDistances = 0; GLint maxClipDistances = 0;
// The cull half of the same extension, and the same "zero is a legal answer" rule: a cull
// distance discards the whole primitive, so promising eight on a driver that has none does
// not fail loudly, it drops every draw of a culling program.
GLint maxCullDistances = 0;
GLint maxCombinedClipAndCullDistances = 0;
GLint maxViewports = 16; GLint maxViewports = 16;
// GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal // GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal
// answer rather than a placeholder: with neither geometry shaders nor a viewport array // answer rather than a placeholder: with neither geometry shaders nor a viewport array
@@ -1333,6 +1338,23 @@ namespace MobileGL::MG_Util::BackendLoader {
"rejected; reporting no clip distances"); "rejected; reporting no clip distances");
maxClipDistances = 0; maxClipDistances = 0;
} }
// GL_MAX_CULL_DISTANCES_EXT (0x82F9) and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT
// (0x82FA) are the same tokens as their desktop spellings and arrive with the same
// extension, so they are probed under the same guard and the same drain sandwich.
drainErrors();
glesFuncs.glGetIntegerv(GL_MAX_CULL_DISTANCES, &maxCullDistances);
if (drainErrors()) {
MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CULL_DISTANCES was "
"rejected; reporting no cull distances");
maxCullDistances = 0;
}
drainErrors();
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &maxCombinedClipAndCullDistances);
if (drainErrors()) {
MGLOG_W("GL_EXT_clip_cull_distance is advertised but "
"GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES was rejected; deriving it from the pair");
maxCombinedClipAndCullDistances = 0;
}
} }
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims); glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is // GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
@@ -1562,6 +1584,14 @@ namespace MobileGL::MG_Util::BackendLoader {
// A driver is free to write nonsense into an out-param it then rejects, and without the // A driver is free to write nonsense into an out-param it then rejects, and without the
// extension the probe above never ran at all - so the flag, not the local, decides. // extension the probe above never ran at all - so the flag, not the local, decides.
caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0; caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0;
caps.MaxCullDistances = caps.SupportsClipDistance ? std::max(maxCullDistances, 0) : 0;
// The combined limit can never be smaller than either half (GL 4.6 core 11.1.3.10 / the
// EXT spec say so), so a driver that rejected the combined query but answered the other
// two still gets a usable - and never over-stated - number.
caps.MaxCombinedClipAndCullDistances =
caps.SupportsClipDistance
? std::max({maxCombinedClipAndCullDistances, caps.MaxClipDistances, caps.MaxCullDistances})
: 0;
caps.MaxViewports = maxViewports; caps.MaxViewports = maxViewports;
caps.LayerProvokingVertex = layerProvokingVertex; caps.LayerProvokingVertex = layerProvokingVertex;
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex; caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
@@ -1651,6 +1681,8 @@ namespace MobileGL::MG_Util::BackendLoader {
// and "this driver has no clip distances". // and "this driver has no clip distances".
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances, MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances,
caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)"); caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)");
MGLOG_I(" GL_MAX_CULL_DISTANCES: %d", caps.MaxCullDistances);
MGLOG_I(" GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES: %d", caps.MaxCombinedClipAndCullDistances);
MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports); MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports);
MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight); MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin, MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
@@ -1299,6 +1299,11 @@ namespace MobileGL {
// GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe // GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe
// in FillInGLESCapabilities. // in FillInGLESCapabilities.
Int MaxClipDistances = 0; Int MaxClipDistances = 0;
// Same contract, same reason, same extension: GL_MAX_CULL_DISTANCES_EXT and
// GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT exist in ES only under
// GL_EXT_clip_cull_distance, so zero is the honest answer without it.
Int MaxCullDistances = 0;
Int MaxCombinedClipAndCullDistances = 0;
Int MaxViewports = 16; Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX // GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX
// (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what // (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what
@@ -199,6 +199,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments); caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments);
caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments); caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments);
caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances); caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances);
caps.MaxCullDistances = SaturateToInt(p.limits.maxCullDistances);
caps.MaxCombinedClipAndCullDistances = SaturateToInt(p.limits.maxCombinedClipAndCullDistances);
caps.MaxViewports = SaturateToInt(p.limits.maxViewports); caps.MaxViewports = SaturateToInt(p.limits.maxViewports);
caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]); caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]);
caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]); caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]);
@@ -239,6 +241,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE; caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE; caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE; caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE;
caps.SupportsShaderCullDistance = supportedFeatures.shaderCullDistance == VK_TRUE;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
HasUsableShaderSubgroupSupport(subgroupProps); HasUsableShaderSubgroupSupport(subgroupProps);
@@ -319,6 +322,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments); caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments);
caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments); caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments);
caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances); caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances);
caps.MaxCullDistances = SaturateToInt(properties.limits.maxCullDistances);
caps.MaxCombinedClipAndCullDistances = SaturateToInt(properties.limits.maxCombinedClipAndCullDistances);
caps.MaxViewports = SaturateToInt(properties.limits.maxViewports); caps.MaxViewports = SaturateToInt(properties.limits.maxViewports);
caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]); caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]);
caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]); caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]);
@@ -336,6 +341,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsFragmentStoresAndAtomics = false; caps.SupportsFragmentStoresAndAtomics = false;
caps.SupportsGeometryShader = false; caps.SupportsGeometryShader = false;
caps.SupportsShaderClipDistance = false; caps.SupportsShaderClipDistance = false;
caps.SupportsShaderCullDistance = false;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
caps.SupportsShaderSubgroup = false; caps.SupportsShaderSubgroup = false;
caps.SubgroupSize = 0; caps.SubgroupSize = 0;
@@ -68,6 +68,11 @@ namespace MobileGL {
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
Int MaxClipDistances = 8; Int MaxClipDistances = 8;
// VkPhysicalDeviceLimits::maxCullDistances / maxCombinedClipAndCullDistances, gated
// by SupportsShaderCullDistance exactly as the clip pair is gated by
// SupportsShaderClipDistance.
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16; Int MaxViewports = 16;
Int MaxViewportWidth = 16384; Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384; Int MaxViewportHeight = 16384;
@@ -106,6 +111,11 @@ namespace MobileGL {
// device has it, and without it a shader writing gl_ClipDistance is invalid. Very // device has it, and without it a shader writing gl_ClipDistance is invalid. Very
// widely supported, hence read from the device features and never assumed false. // widely supported, hence read from the device features and never assumed false.
Bool SupportsShaderClipDistance = false; Bool SupportsShaderClipDistance = false;
// VkPhysicalDeviceFeatures::shaderCullDistance, the same story one field down:
// VulkanRenderer already ENABLES this feature where the device has it, but nobody
// ever read the limits it unlocks, so the frontend advertised eight cull distances
// from a literal instead of from the device.
Bool SupportsShaderCullDistance = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Bool SupportsShaderSubgroup = false; Bool SupportsShaderSubgroup = false;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_Util/Math/FixedPointConversion.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>
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Util {
// GL 4.6 core 2.3.5 "Fixed-Point Data Conversions", for the 32-bit signed normalized pair that
// GL_TEXTURE_BORDER_COLOR is specified and queried in when the NON-"I" integer entry points are
// used (glTexParameteriv / glSamplerParameteriv / glGetTexParameteriv / glGetSamplerParameteriv).
// The "I" entry points (TexParameterIiv / Iuiv) carry a raw integer border colour instead and
// must NOT go through these.
//
// The two directions have to be an exact pair or a legal round trip is destroyed: the CTS writes
// {0,1,2,4} with glTexParameteriv and demands {0,1,2,4} back from glGetTexParameteriv. Reading
// with a bare static_cast<GLint> (which is what the truncating read used to do) answers {0,0,0,0}
// because equation 2.2 has already scaled those integers down to ~1e-9.
//
// b = 32, so the scale is 2^31 - 1 = 2147483647. It is held in DOUBLE deliberately: as a binary32
// it rounds up to 2^31, and the inverse direction would then answer -2147483648 for f = -1.0
// where the equation says -2147483647. The forward direction is unaffected either way (a small
// integer divided by 2147483647 lands on the same float as one divided by 2^31), so one exact
// constant serves both and the pair stays a true inverse: c -> c/(2^31-1) -> c.
inline constexpr double kSignedNormalizedInt32Scale = 2147483647.0;
// Equation 2.2: c / (2^(b-1) - 1), clamped below at -1 so the extra negative code (-2^31) does
// not produce a value outside [-1, 1].
inline Float SignedNormalizedInt32ToFloat(Int32 value) {
return std::max(static_cast<Float>(static_cast<double>(value) / kSignedNormalizedInt32Scale), -1.0f);
}
// Equation 2.3: round(f * (2^(b-1) - 1)). f is clamped to [-1, 1] first, as the equation's domain
// requires; the multiply is done in double so a near-1 float cannot round past INT32_MAX before
// the cast, which is undefined behaviour rather than a saturating one.
inline Int32 FloatToSignedNormalizedInt32(Float value) {
if (std::isnan(value)) return 0;
const Float clamped = std::clamp(value, -1.0f, 1.0f);
const double scaled = std::round(static_cast<double>(clamped) * kSignedNormalizedInt32Scale);
return static_cast<Int32>(std::clamp(scaled, -2147483648.0, 2147483647.0));
}
} // namespace MobileGL::MG_Util
+12
View File
@@ -10,6 +10,8 @@
#include <Includes.h> #include <Includes.h>
#include <cstring>
namespace MobileGL { namespace MobileGL {
template <typename Derived, typename T, SizeT N> template <typename Derived, typename T, SizeT N>
struct VecBase { struct VecBase {
@@ -84,6 +86,16 @@ namespace MobileGL {
} }
}; };
// Bit-pattern equality, for a vector used as a cache or staleness KEY rather than as a
// number. IEEE `==` - which operator== above is - says a NaN never equals itself, so a single
// NaN component makes every comparison answer "changed" and whatever the key guards is
// rebuilt on every use, forever. Two zeros of opposite sign compare unequal here, which only
// ever costs one extra rebuild.
template <typename Derived, typename T, SizeT N>
Bool BitwiseEqual(const VecBase<Derived, T, N>& a, const VecBase<Derived, T, N>& b) {
return std::memcmp(a.data.data(), b.data.data(), sizeof(T) * N) == 0;
}
template <typename T> template <typename T>
struct Vec2 : public VecBase<Vec2<T>, T, 2> { struct Vec2 : public VecBase<Vec2<T>, T, 2> {
using Base = VecBase<Vec2<T>, T, 2>; using Base = VecBase<Vec2<T>, T, 2>;
@@ -10,6 +10,15 @@
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
Uint32 FixedRestartIndexForGLType(GLenum indexType) {
switch (indexType) {
case GL_UNSIGNED_BYTE: return 0xFFu;
case GL_UNSIGNED_SHORT: return 0xFFFFu;
case GL_UNSIGNED_INT: return 0xFFFFFFFFu;
default: return 0;
}
}
SizeT GetGLTypeSize(GLenum type) { SizeT GetGLTypeSize(GLenum type) {
switch (type) { switch (type) {
// Scalars // Scalars
+14
View File
@@ -12,5 +12,19 @@
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
SizeT GetGLTypeSize(GLenum type); SizeT GetGLTypeSize(GLenum type);
// The largest value an index of this type can hold, which is also the value
// GL_PRIMITIVE_RESTART_FIXED_INDEX (and its GLES/Vulkan equivalents) restart on. Zero for a
// type that cannot index at all.
//
// Shared rather than re-derived per backend on purpose: three places have to agree about
// what an index of this type can be - whether a rewrite is needed at all, what the rewrite
// compares against, and whether the driver should be told to restart. GL 4.6 core 10.3.6
// compares the FETCHED index, zero-extended, against the full 32-bit
// PRIMITIVE_RESTART_INDEX, so a restart index greater than this value matches no index and
// the draw restarts nowhere. Truncating it to the type's width instead - which one of these
// three places used to do - turns a legal vertex index into a restart.
Uint32 FixedRestartIndexForGLType(GLenum indexType);
} // namespace MG_Util } // namespace MG_Util
} // namespace MobileGL } // namespace MobileGL
@@ -23,6 +23,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
} }
} // namespace } // namespace
Int ResolveMaxVertexAttribs(const Bool hasBackend, const Int backendMaxVertexAttribs) {
constexpr Int capacity = static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
if (!hasBackend) return capacity;
if (backendMaxVertexAttribs <= 0) return capacity;
return std::min(backendMaxVertexAttribs, capacity);
}
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) { Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) {
Uint64 state = 0x9e3779b97f4a7c15ull; Uint64 state = 0x9e3779b97f4a7c15ull;
HashValue(state, env.maxComputeWorkGroupSize[0]); HashValue(state, env.maxComputeWorkGroupSize[0]);
@@ -67,6 +74,24 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// and expands gl_MaxClipDistances from the same number, so it decides both whether a // and expands gl_MaxClipDistances from the same number, so it decides both whether a
// shader compiles at all and what a module that reads the constant generates. // shader compiles at all and what a module that reads the constant generates.
HashValue(state, env.params.MaxClipDistances); HashValue(state, env.params.MaxClipDistances);
// The cull-distance pair, added when the GL 4.6 API-surface wave made them env-derived:
// they were bare literals (8/8) in BuildTBuiltInResource while no backend had ever been
// asked whether it can host a cull distance. Exactly the MaxClipDistances class - glslang
// bounds gl_CullDistance[i] against maxCullDistances at parse and expands
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances from the same numbers.
HashValue(state, env.params.MaxCullDistances);
HashValue(state, env.params.MaxCombinedClipAndCullDistances);
// The texture-image-unit family, made env-derived in the same wave. They were stock
// glslang defaults (32/32/80) that disagreed with what glGetIntegerv answered, and
// gl_MaxTextureImageUnits / gl_MaxVertexTextureImageUnits / gl_MaxCombinedTextureImageUnits
// expand from them.
HashValue(state, env.params.MaxTextureImageUnits);
HashValue(state, env.params.MaxVertexTextureImageUnits);
HashValue(state, env.params.MaxCombinedTextureImageUnits);
// gl_MaxSamples, which also sizes gl_SampleMask[] / gl_SampleMaskIn[] and bounds a
// constant index into them, so a module that touches either generates different SPIR-V
// on two backends that report different sample counts.
HashValue(state, env.params.MaxSamples);
// The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be // The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be
// hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many // hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many
// words that the day they became backend-derived they would have to move in here - // words that the day they became backend-derived they would have to move in here -
+19 -4
View File
@@ -84,11 +84,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into // * the DynamicBackendParameters fields BuildTBuiltInResource copies into
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms, // TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms, // MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. glslang // MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances,
// MaxCullDistances, MaxCombinedClipAndCullDistances, MaxTextureImageUnits,
// MaxVertexTextureImageUnits, MaxCombinedTextureImageUnits, MaxSamples. glslang
// enforces those at parse, so they decide whether a shader compiles at all and can // enforces those at parse, so they decide whether a shader compiles at all and can
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d), // change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d)
// the third time in three waves that a hardcoded TBuiltInResource field became // and the six after it at the GL 4.6 API-surface wave - the fourth time in four
// env-derived - assume the next wave does it again and re-audit. // waves that a hardcoded TBuiltInResource field became env-derived. Assume the next
// wave does it again and re-audit.
// * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each. // * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each.
// These moved IN at the dev merge that brought wave3's cb155c5b, which made // These moved IN at the dev merge that brought wave3's cb155c5b, which made
// BuildTBuiltInResource read them from the env instead of hardcoding a permissive // BuildTBuiltInResource read them from the env instead of hardcoding a permissive
@@ -160,6 +163,18 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
} }
}; };
// How many vertex input locations exist, from the frontend's point of view: the backend's
// advertised count bounded by the state layer's current-value storage capacity
// (VertexArrayObject::MAX_VERTEX_ATTRIBS). ONE definition, because three places have to
// agree on it and used to carry three copies of the formula - glGetIntegerv
// (VertexArrayImpl::GetMaxVertexAttribs), the limit reflection records vertex inputs
// against (ProgramLinkTask), and gl_MaxVertexAttribs (BuildTBuiltInResource, which had a
// hardcoded 64 the other two never saw). A disagreement there is not cosmetic: glslang
// ACCEPTS a vertex input at a location the runtime cannot bind, and the draw then silently
// reads nothing. `hasBackend` false means "no backend to be bounded by" and yields the
// storage capacity, matching what all three did before.
Int ResolveMaxVertexAttribs(Bool hasBackend, Int backendMaxVertexAttribs);
// Hashes every semantically relevant member. Public so a test can assert that two // Hashes every semantically relevant member. Public so a test can assert that two
// different envs really do produce different P0b cache keys. // different envs really do produce different P0b cache keys.
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env); Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
@@ -12,6 +12,11 @@
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#include <algorithm>
#include <format>
#include <cmath>
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h" #include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
#include "SpirvPasses/FlattenInterfaceStructPass.h" #include "SpirvPasses/FlattenInterfaceStructPass.h"
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h" #include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
@@ -64,6 +69,30 @@
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
// Above every plausible GL_MAX_TESS_GEN_LEVEL (the GL core minimum is 64), so it lands on the
// same clamped result the device's own maximum would. +inf has to reach the tessellator as
// "as finely as possible", not as "discard".
static constexpr const char* kClampedHighTessLevelLiteral = "65536.0";
String TessellationLevelLiteral(Float value) {
// GL leaves a NaN level unspecified; 0.0 is the safe reading, and unlike "nan" it compiles.
if (std::isnan(value)) return "0.0";
// -inf is <= 0 and discards the patch, exactly like 0.0. +inf clamps to the maximum.
if (std::isinf(value)) return value > 0.0f ? kClampedHighTessLevelLiteral : "0.0";
// Shortest round-trip, not a fixed six decimals: "{:.6f}" renders every level below ~5e-7
// as "0.000000", which turns a positive level GL would clamp to 1 into a discarded patch.
String text = std::format("{}", value);
// ...but shortest round-trip spells an integral value as a bare digit sequence, which GLSL
// reads as an INT literal, so the decimal point has to be put back when nothing else marks
// the literal as floating point.
if (text.find('.') == String::npos && text.find('e') == String::npos &&
text.find('E') == String::npos) {
text += ".0";
}
return text;
}
// `env` is the compile-time backend snapshot; null means "resolve from the live // `env` is the compile-time backend snapshot; null means "resolve from the live
// backend", which is what the standalone/test entry points do. The pipeline always // backend", which is what the standalone/test entry points do. The pipeline always
// passes one, so a worker never reaches pActiveBackendObject through here. // passes one, so a worker never reaches pActiveBackendObject through here.
@@ -73,16 +102,11 @@ namespace MobileGL {
Resources.maxClipPlanes = 6; Resources.maxClipPlanes = 6;
Resources.maxTextureUnits = 32; Resources.maxTextureUnits = 32;
Resources.maxTextureCoords = 32; Resources.maxTextureCoords = 32;
Resources.maxVertexAttribs = 64; Resources.maxVertexUniformComponents = MAX_VERTEX_UNIFORM_COMPONENTS;
Resources.maxVertexUniformComponents = 4096; Resources.maxVaryingFloats = MAX_VARYING_COMPONENTS;
Resources.maxVaryingFloats = 64;
Resources.maxVertexTextureImageUnits = 32;
Resources.maxCombinedTextureImageUnits = 80;
Resources.maxTextureImageUnits = 32;
Resources.maxFragmentUniformComponents = 4096; Resources.maxFragmentUniformComponents = 4096;
Resources.maxDrawBuffers = 32; Resources.maxVertexUniformVectors = MAX_VERTEX_UNIFORM_VECTORS;
Resources.maxVertexUniformVectors = 128; Resources.maxVaryingVectors = MAX_VARYING_VECTORS;
Resources.maxVaryingVectors = 8;
Resources.maxFragmentUniformVectors = 256; Resources.maxFragmentUniformVectors = 256;
Resources.maxVertexOutputVectors = 16; Resources.maxVertexOutputVectors = 16;
Resources.maxFragmentInputVectors = 15; Resources.maxFragmentInputVectors = 15;
@@ -93,14 +117,12 @@ namespace MobileGL {
Resources.maxComputeImageUniforms = 8; Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE; Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE; Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = 60; Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS;
Resources.maxVertexOutputComponents = 64; Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64; Resources.maxGeometryInputComponents = 64;
Resources.maxGeometryOutputComponents = 128; Resources.maxGeometryOutputComponents = 128;
Resources.maxFragmentInputComponents = 128; Resources.maxFragmentInputComponents = 128;
Resources.maxImageUnits = 8; Resources.maxImageUnits = 8;
Resources.maxCombinedImageUnitsAndFragmentOutputs = 8;
Resources.maxCombinedShaderOutputResources = 8;
Resources.maxImageSamples = 0; Resources.maxImageSamples = 0;
Resources.maxVertexImageUniforms = 0; Resources.maxVertexImageUniforms = 0;
Resources.maxTessControlImageUniforms = 0; Resources.maxTessControlImageUniforms = 0;
@@ -113,16 +135,18 @@ namespace MobileGL {
Resources.maxGeometryTotalOutputComponents = 1024; Resources.maxGeometryTotalOutputComponents = 1024;
Resources.maxGeometryUniformComponents = 1024; Resources.maxGeometryUniformComponents = 1024;
Resources.maxGeometryVaryingComponents = 64; Resources.maxGeometryVaryingComponents = 64;
Resources.maxTessControlInputComponents = 128; // The tessellation block is shared with glGetIntegerv through Types.h; see the
Resources.maxTessControlOutputComponents = 128; // "Never move one of these without the other" note there.
Resources.maxTessControlTextureImageUnits = 16; Resources.maxTessControlInputComponents = MAX_TESS_CONTROL_INPUT_COMPONENTS;
Resources.maxTessControlUniformComponents = 1024; Resources.maxTessControlOutputComponents = MAX_TESS_CONTROL_OUTPUT_COMPONENTS;
Resources.maxTessControlTotalOutputComponents = 4096; Resources.maxTessControlTextureImageUnits = MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS;
Resources.maxTessEvaluationInputComponents = 128; Resources.maxTessControlUniformComponents = MAX_TESS_CONTROL_UNIFORM_COMPONENTS;
Resources.maxTessEvaluationOutputComponents = 128; Resources.maxTessControlTotalOutputComponents = MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS;
Resources.maxTessEvaluationTextureImageUnits = 16; Resources.maxTessEvaluationInputComponents = MAX_TESS_EVALUATION_INPUT_COMPONENTS;
Resources.maxTessEvaluationUniformComponents = 1024; Resources.maxTessEvaluationOutputComponents = MAX_TESS_EVALUATION_OUTPUT_COMPONENTS;
Resources.maxTessPatchComponents = 120; Resources.maxTessEvaluationTextureImageUnits = MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS;
Resources.maxTessEvaluationUniformComponents = MAX_TESS_EVALUATION_UNIFORM_COMPONENTS;
Resources.maxTessPatchComponents = MAX_TESS_PATCH_COMPONENTS;
Resources.maxPatchVertices = 32; Resources.maxPatchVertices = 32;
Resources.maxTessGenLevel = 64; Resources.maxTessGenLevel = 64;
Resources.maxViewports = 16; Resources.maxViewports = 16;
@@ -148,9 +172,6 @@ namespace MobileGL {
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE; Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4; Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64; Resources.maxTransformFeedbackInterleavedComponents = 64;
Resources.maxCullDistances = 8;
Resources.maxCombinedClipAndCullDistances = 8;
Resources.maxSamples = 4;
Resources.maxMeshOutputVerticesNV = 256; Resources.maxMeshOutputVerticesNV = 256;
Resources.maxMeshOutputPrimitivesNV = 512; Resources.maxMeshOutputPrimitivesNV = 512;
Resources.maxMeshWorkGroupSizeX_NV = 32; Resources.maxMeshWorkGroupSizeX_NV = 32;
@@ -180,12 +201,37 @@ namespace MobileGL {
Resources.maxImageUnits = dynamicParameters.MaxImageUnits; Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs = Resources.maxCombinedImageUnitsAndFragmentOutputs =
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
// GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES and
// GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS are the SAME token (0x8F39), so
// the two glslang fields have to carry the same value: glGetIntegerv answers this
// expression while gl_MaxCombinedShaderOutputResources expanded from a stale
// literal 8, and the CTS compares the two directly.
Resources.maxCombinedShaderOutputResources =
Resources.maxCombinedImageUnitsAndFragmentOutputs;
Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms; Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms;
Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms; Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits; Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
// The texture-image-unit family and the draw-buffer count. These were stock
// glslang defaults (32 / 32 / 80 / 32) that had nothing to do with what
// glGetIntegerv answers off the same backend, and the divergence is a live
// correctness bug rather than a reporting one: gl_MaxDrawBuffers = 32 makes
// glslang ACCEPT a fragment output at location 8..31 that the runtime cannot
// bind, and gl_MaxCombinedTextureImageUnits = 80 under-reports a device that
// really has 96.
Resources.maxTextureImageUnits = dynamicParameters.MaxTextureImageUnits;
Resources.maxVertexTextureImageUnits = dynamicParameters.MaxVertexTextureImageUnits;
Resources.maxCombinedTextureImageUnits = dynamicParameters.MaxCombinedTextureImageUnits;
Resources.maxDrawBuffers = dynamicParameters.MaxDrawBuffers;
// The same number glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) reports and the same one
// reflection records vertex inputs against - see ResolveMaxVertexAttribs.
Resources.maxVertexAttribs = ResolveMaxVertexAttribs(
env ? env->HasBackend() : (activeBackend != nullptr), dynamicParameters.MaxVertexAttribs);
// gl_MaxSamples, floored exactly as GL_Getter::GetAdvertisedMaxSamples floors
// GL_MAX_SAMPLES. It also sizes gl_SampleMask[] / gl_SampleMaskIn[].
Resources.maxSamples = std::max(dynamicParameters.MaxSamples, MIN_ADVERTISED_MAX_SAMPLES);
// Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for // Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for
// i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the // i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the
// same number, so tracking the backend limit is what turns "the program links, // same number, so tracking the backend limit is what turns "the program links,
@@ -194,6 +240,14 @@ namespace MobileGL {
// also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances // also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances
// agree, which KHR-GLxx.clip_distance.coverage compares directly. // agree, which KHR-GLxx.clip_distance.coverage compares directly.
Resources.maxClipDistances = dynamicParameters.MaxClipDistances; Resources.maxClipDistances = dynamicParameters.MaxClipDistances;
// The cull pair, for the same reason and with a sharper edge: cull distance
// discards the WHOLE primitive, so a shader that gets to declare gl_CullDistance
// on a backend that cannot host one does not render subtly wrong pixels, it
// renders nothing at all. These were literal 8s that no backend was ever asked
// about; a backend without cull distances now reports 0 and glslang rejects the
// declaration with a diagnostic the application can read.
Resources.maxCullDistances = dynamicParameters.MaxCullDistances;
Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances;
// The compute work-group limits are the env's, not the backend parameters': they // The compute work-group limits are the env's, not the backend parameters': they
// are the only ones that come from a REAL indexed driver query, which // are the only ones that come from a REAL indexed driver query, which
@@ -242,7 +296,15 @@ namespace MobileGL {
tshader->setStrings(src, 1); tshader->setStrings(src, 1);
tshader->setNanMinMaxClamp(true); tshader->setNanMinMaxClamp(true);
tshader->setInvertY(true); tshader->setInvertY(true);
tshader->setPreamble("#undef VULKAN\n"); // The custom preamble is glslang string -1, which CPPdefine exempts from the
// "names beginning with GL_ can't be (un)defined" rule - so it is the only place
// an ES source's extension macros can be put back after PreprocessShaderSource
// rewrote its #version to desktop and cost it glslang's ES preamble. Empty for
// every source that was not rewritten from ES, which is almost all of them.
//
// setPreamble stores the POINTER, so the buffer has to outlive parse() below.
const String preamble = String("#undef VULKAN\n") + CollectEsPreambleMacroDefines(source);
tshader->setPreamble(preamble.c_str());
if (flags & ShaderCompileBits::CompileForOpenGL) { if (flags & ShaderCompileBits::CompileForOpenGL) {
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450); tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450); tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
@@ -487,12 +549,28 @@ namespace MobileGL {
attrib.explicitFragmentOutIndices, attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings, attrib.explicitOpaqueUniformBindings,
attrib.storageBlocksWithoutBinding, attrib.storageBlocksWithoutBinding,
attrib.uniformBlocksWithoutBinding); attrib.uniformBlocksWithoutBinding,
&attrib.resourceBindingLimits,
attrib.resourceBindingViolation);
break; break;
} }
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper()); auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
if (!program->mapIO(resolver.get(), ioMapper.get())) { const bool mapped = program->mapIO(resolver.get(), ioMapper.get());
// The binding-range verdict is read BEFORE mapIO's own outcome, and unconditionally:
// the resolver fills it during the collect phase, which runs whether or not doMap()
// later succeeds, and a shader that names an out-of-range binding is rejected for
// THAT reason no matter what else the mapper made of it. Reporting the mapper's
// generic failure instead would hand the application an info log that says nothing
// about the declaration it has to fix.
if (attrib.resourceBindingViolation != nullptr && !attrib.resourceBindingViolation->empty()) {
ResultInfo r;
r.log = *attrib.resourceBindingViolation;
r.errc = -5;
return std::unexpected(r);
}
if (!mapped) {
ResultInfo r; ResultInfo r;
r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog()); r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog());
r.errc = -4; r.errc = -4;
@@ -1604,6 +1682,197 @@ namespace MobileGL {
return true; return true;
} }
namespace {
// The execution model an application-supplied module's entry point must carry for
// the shader object it was handed to. glShaderBinary attaches a module to a shader
// of a fixed type, and ARB_gl_spirv requires the specialized entry point to match.
SpvExecutionModel ExecutionModelForShaderType(GLenum shaderType) {
switch (shaderType) {
case GL_VERTEX_SHADER:
return SpvExecutionModelVertex;
case GL_TESS_CONTROL_SHADER:
return SpvExecutionModelTessellationControl;
case GL_TESS_EVALUATION_SHADER:
return SpvExecutionModelTessellationEvaluation;
case GL_GEOMETRY_SHADER:
return SpvExecutionModelGeometry;
case GL_COMPUTE_SHADER:
return SpvExecutionModelGLCompute;
case GL_FRAGMENT_SHADER:
default:
return SpvExecutionModelFragment;
}
}
// The decorated capture layout, as the equivalent glTransformFeedbackVaryings
// request. GL 4.6 core 11.1.2.1 / ARB_transform_feedback3 give the name list two
// pseudo-varyings that are exactly what a decoration layout needs: gl_NextBuffer
// moves to the next capture buffer, and gl_SkipComponentsN (N in 1..4) advances the
// cursor without capturing. Together they can express any offset/stride layout
// whose offsets are component-aligned, which SPIR-V's are (Offset is in bytes and
// xfb offsets are four-byte aligned by rule).
Vector<String> BuildXfbVaryingRequest(const Vector<SpirvXfbCapture>& captures) {
Vector<String> names;
if (captures.empty()) return names;
auto emitSkip = [&names](Uint32 components) {
while (components > 0) {
const Uint32 step = std::min<Uint32>(components, 4);
names.push_back("gl_SkipComponents" + std::to_string(step));
components -= step;
}
};
Uint32 currentBuffer = captures.front().buffer;
Uint32 cursorComponents = 0;
Uint32 currentStride = 0;
// Buffers below the first captured one still have to be stepped over, so the
// Nth gl_NextBuffer really does land on buffer N.
for (Uint32 buffer = 0; buffer < currentBuffer; ++buffer) {
names.push_back("gl_NextBuffer");
}
for (const SpirvXfbCapture& capture : captures) {
if (capture.buffer != currentBuffer) {
// Pad the buffer being left out to its declared stride, so the record
// size the module asked for survives.
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
for (Uint32 buffer = currentBuffer; buffer < capture.buffer; ++buffer) {
names.push_back("gl_NextBuffer");
}
currentBuffer = capture.buffer;
cursorComponents = 0;
currentStride = 0;
}
const Uint32 offsetComponents = capture.offset / 4;
if (offsetComponents > cursorComponents) emitSkip(offsetComponents - cursorComponents);
names.push_back(capture.name);
cursorComponents = offsetComponents + capture.componentCount;
currentStride = std::max(currentStride, capture.stride);
}
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
return names;
}
} // namespace
Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) {
ResultInfo r;
r.errc = -6;
if (spirv.size() < 5) {
r.log = "Error: [ARB_gl_spirv] the module is too short to be SPIR-V.";
return std::unexpected(r);
}
// 0x07230203 is SPIR-V's magic number. A module in the other byte order is a
// legal SPIR-V file but NOT one glShaderBinary accepts: ARB_gl_spirv fixes the
// word order to the host's.
if (spirv[0] != 0x07230203u) {
r.log = "Error: [ARB_gl_spirv] the module does not begin with the SPIR-V magic number.";
return std::unexpected(r);
}
PrepareSpirvValidation();
spvtools::SpirvTools tools(SPV_ENV_OPENGL_4_5);
String diagnostics;
tools.SetMessageConsumer([&diagnostics](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
if (!diagnostics.empty()) diagnostics += "\n";
diagnostics += message ? message : "";
});
if (!tools.Validate(spirv.data(), spirv.size())) {
r.log = "Error: [ARB_gl_spirv] the module failed SPIR-V validation:\n" + diagnostics;
return std::unexpected(r);
}
return {};
}
Result<ShaderCompiler::SpecializedModule> ShaderCompiler::SpecializeAndDecompileSpirvModule(
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
SpecializationFailure& outFailure) {
outFailure = SpecializationFailure::None;
SpvcSession session(spirv, SessionUsageBit::Transpile);
if (!session.IsTranspileReady()) {
// SPIRV-Cross could not parse the module. glShaderBinary's spirv-val pass is a
// validity check, not a parseability one, so this is reachable with a module
// that validates - hence a diagnosis rather than the null dereference the
// unchecked constructor used to walk into.
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -11;
r.log = "Error: [ARB_gl_spirv] the module could not be parsed:\n" +
String(session.GetLastErrorString());
return std::unexpected(r);
}
Uint32 unknownConstantId = 0;
if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) {
outFailure = SpecializationFailure::UnknownConstantId;
ResultInfo r;
r.errc = -7;
r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) +
" is not a specialization constant of this module.";
return std::unexpected(r);
}
// No `if (!entryPoint.empty())` guard any more. ARB_gl_spirv makes pEntryPoint the
// name of the entry point to specialize, and no module carries one named ""; the
// guard turned an empty name into "whichever entry point happens to be default",
// which is neither what the application asked for nor an error it was told about.
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
SPVC_SUCCESS) {
outFailure = SpecializationFailure::UnknownEntryPoint;
ResultInfo r;
r.errc = -8;
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint +
"' for this shader stage:\n" + String(session.GetLastErrorString());
return std::unexpected(r);
}
// Read the declared capture layout, then REMOVE the decorations that describe it.
// Both halves matter: without the read a SPIR-V program captures nothing, and
// without the strip the decorations round-trip through the emitted GLSL back into
// the regenerated SPIR-V, where DirectGLES's ESSL hop refuses them outright and
// loses the stage. See SpvcSession::StripTransformFeedbackDecorations.
SpecializedModule specialized;
specialized.xfbVaryings = BuildXfbVaryingRequest(session.ReflectTransformFeedbackCaptures());
session.StripTransformFeedbackDecorations();
spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -9;
r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module.";
return std::unexpected(r);
}
// DESKTOP 4.60, not the ESSL 3.20 DecompileShader emits: this source goes back in
// at the FRONT of the pipeline, to be parsed by glslang exactly like an
// application's own GLSL, and every one of MobileGL's source-level passes is
// written against the desktop dialect. The ESSL hop happens later and unchanged,
// out of the SPIR-V this re-parse produces.
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 460);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
// Vulkan semantics OFF is what makes this a GL source: descriptor sets collapse
// onto GL binding points, push constants become a uniform block, and - the point
// of the specialization pass above - every specialization constant is folded in
// as a literal instead of re-emitted as layout(constant_id = N).
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, SPVC_TRUE);
session.SetOptions(options);
const char* emitted = nullptr;
session.Compile(&emitted);
if (!emitted) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -10;
r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" +
String(session.GetLastErrorString());
return std::unexpected(r);
}
specialized.glsl = String(emitted);
return specialized;
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) { Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options; spvc_compiler_options options;
session.CreateOptions(&options); session.CreateOptions(&options);
@@ -18,6 +18,18 @@
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
// A GLSL float literal for a tessellation level, for the pass-through tessellation
// control stage both backends synthesize when a program has an evaluation stage and no
// control stage. Shared so the two generators cannot disagree about what a level means.
//
// GL 4.6 core 11.2.2 discards a patch only when a relevant OUTER level is <= 0; every
// other value is CLAMPED into [1, MAX_TESS_GEN_LEVEL]. So "draw nothing" is reserved
// for the values that really mean it, and everything else has to survive the trip
// through text: a shortest-round-trip spelling, because a fixed-decimal one flushes
// small positive levels to zero, and always with a '.' or an exponent, because a bare
// digit sequence is an INT literal and `gl_TessLevelOuter[0] = 1;` does not compile.
String TessellationLevelLiteral(Float value);
class ShaderCompiler { class ShaderCompiler {
public: public:
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib); static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
@@ -393,6 +405,78 @@ namespace MobileGL {
bool enableSpirvValidation = false); bool enableSpirvValidation = false);
static Result<String> DecompileShader(SpvcSession& session); static Result<String> DecompileShader(SpvcSession& session);
// ---- GL_ARB_gl_spirv ----
// Turn an APPLICATION-supplied SPIR-V module into the desktop GLSL the ordinary
// compile pipeline consumes.
//
// Why a round trip rather than handing the module straight to the backends. SPIR-V
// is not where MobileGL's pipeline STARTS: a program's whole GL-visible surface -
// every glGetActiveUniform, every uniform location, every block index, the
// transform-feedback layout, the default-block UBO routing - is reflected out of
// glslang's TProgram at link (ProgramLinkTask::SnapshotGlslangReflection), and
// glslang can only build one from a GLSL parse. Injecting the module at
// ProgramSpirvTask instead would skip the link entirely and leave every one of
// those queries answering nothing. Decompiling puts the application's module at
// the head of the SAME pipeline, so reflection, the relaxed default-block
// lowering, both backends and every memo tier work on it unchanged.
//
// What it costs, stated plainly: names. A module stripped of OpName (which
// ARB_gl_spirv permits, and the conformance suite deliberately does) comes back
// with SPIRV-Cross's generated identifiers rather than with none, so the
// *_MAX_LENGTH queries answer those instead of 1.
//
// `entryPoint` selects among several OpEntryPoint of this stage's execution
// model; an empty string means "whichever one is there". The specialization
// constants glSpecializeShader supplied are applied in the same pass - SPIRV-Cross
// folds each into the emitted source as a literal once Vulkan semantics are off,
// which is exactly what "specialize, then compile" means for a GLSL consumer.
//
// `constantIds` and `constantValues` are the parallel arrays the entry point
// takes. A constant id the module does not declare is GL_INVALID_VALUE per the
// extension; it is reported through the error log rather than silently ignored.
// Why the caller needs a REASON and not just a failure: ARB_gl_spirv splits the
// ways specialization can fail into two groups with different GL surfaces. A bad
// entry-point name and a constant id the module does not declare are enumerated
// errors - GL_INVALID_VALUE, and, being errors, they must leave the shader object
// exactly as it was. Everything else (a module SPIRV-Cross cannot translate) is a
// COMPILE failure, reported through COMPILE_STATUS and the info log like any other
// glCompileShader outcome. Returning one undifferentiated error is what made both
// groups look like the second.
enum class SpecializationFailure {
None,
UnknownConstantId, // GL_INVALID_VALUE
UnknownEntryPoint, // GL_INVALID_VALUE
ModuleRejected, // COMPILE_STATUS false + info log
};
// What a specialized module turns into: the GLSL the ordinary pipeline compiles,
// plus the transform-feedback capture the module DECLARED, re-expressed as the
// glTransformFeedbackVaryings request that produces the same layout.
//
// The re-expression is the whole design. ARB_gl_spirv makes XfbBuffer/XfbStride/
// Offset decorations the only way a SPIR-V program declares capture, and MobileGL's
// capture machinery - the frontend packer, DirectGLES's forwarding to the ES
// driver, DirectVulkan's XfbCaptureDecoratePass - is driven entirely by a name
// list. Translating the decorations into the equivalent name list (with
// ARB_transform_feedback3's gl_NextBuffer / gl_SkipComponentsN spelling carrying
// the buffer breaks and the gaps) hands a SPIR-V program to the machinery that
// already exists, instead of teaching every consumer a second declaration form.
struct SpecializedModule {
String glsl;
Vector<String> xfbVaryings;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
};
static Result<SpecializedModule> SpecializeAndDecompileSpirvModule(
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
SpecializationFailure& outFailure);
// spirv-val over an application-supplied module, against the environment MobileGL
// parses and emits under. glShaderBinary is where a malformed module has to be
// caught: past it the words reach SPIRV-Cross, which is not a validator.
static Result<void> ValidateSpirvModule(const Vector<Uint32>& spirv);
// Parses one trivial shader in each configuration the production path can // Parses one trivial shader in each configuration the production path can
// reach, on the calling thread, so the built-in symbol tables those // reach, on the calling thread, so the built-in symbol tables those
// configurations need are already cached before any worker asks for one. // configurations need are already cached before any worker asks for one.
@@ -13,6 +13,7 @@
#include <cerrno> #include <cerrno>
#include <climits> #include <climits>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <initializer_list> #include <initializer_list>
#include <utility> #include <utility>
#include <Config.h> #include <Config.h>
@@ -234,15 +235,132 @@ namespace {
// Whether the parsed #version directive is a well-formed one MobileGL should rewrite. A // Whether the parsed #version directive is a well-formed one MobileGL should rewrite. A
// malformed directive (see IsRecognizedGlslVersion) is left alone for glslang to reject. // malformed directive (see IsRecognizedGlslVersion) is left alone for glslang to reject.
bool hasValidVersionDirective = false; bool hasValidVersionDirective = false;
// Every extension the source NAMES in an "#extension <name> : <behavior>" directive, and
// the subset whose behavior switches it on. Both are needed and they are not the same
// question: glslang's ES preamble defines an extension's macro whatever behavior the
// shader later asks for (it is a preamble, it runs first), while whether gl_NumSamples is
// a legal identifier depends on the extension actually being ENABLED.
std::set<MobileGL::String> namedExtensions;
std::set<MobileGL::String> enabledExtensions;
// Byte ranges [begin, end) of every #version directive AFTER the first that repeats it
// exactly - same version number, same profile, both well-formed. See
// BlankRedundantVersionDirectives for why these are tolerated and nothing else is.
Vector<std::pair<SizeT, SizeT>> redundantVersionDirectives;
bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; } bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; }
}; };
struct ParsedVersionDirective {
unsigned version = 0;
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
bool isValid = false;
};
// glslang's #extension implication graph, transcribed from
// TParseVersions::updateExtensionBehavior (Versions.cpp:1039-1064). Naming one of these
// extensions applies the SAME behavior to every name it implies, so a source that says
// `#extension GL_ANDROID_extension_pack_es31a : require` has really required all twelve AEP
// members - and glslang's ES gl_NumSamples gate reads GL_OES_sample_variables, one of them.
//
// Transcribed rather than approximated: the AEP membership list is glslang's, and a guess that
// drifts from it would make MobileGL accept or reject a shader glslang does not.
// GL_KHR_blend_equation_advanced is in the list for completeness even though it has no ES
// preamble macro - IsEsOnlyPreambleExtensionMacro filters it out on its own.
const Vector<std::pair<const char*, Vector<const char*>>>& GetExtensionImplications() {
static const Vector<std::pair<const char*, Vector<const char*>>> kImplications = {
{"GL_ANDROID_extension_pack_es31a",
{"GL_KHR_blend_equation_advanced", "GL_OES_sample_variables", "GL_OES_shader_image_atomic",
"GL_OES_shader_multisample_interpolation", "GL_OES_texture_storage_multisample_2d_array",
"GL_EXT_geometry_shader", "GL_EXT_gpu_shader5", "GL_EXT_primitive_bounding_box",
"GL_EXT_shader_io_blocks", "GL_EXT_tessellation_shader", "GL_EXT_texture_buffer",
"GL_EXT_texture_cube_map_array"}},
// geometry / tessellation to io_blocks
{"GL_EXT_geometry_shader", {"GL_EXT_shader_io_blocks"}},
{"GL_OES_geometry_shader", {"GL_OES_shader_io_blocks"}},
{"GL_EXT_tessellation_shader", {"GL_EXT_shader_io_blocks"}},
{"GL_OES_tessellation_shader", {"GL_OES_shader_io_blocks"}},
};
return kImplications;
}
// Closes `extensions` under the graph above. glslang propagates by RE-ENTERING
// updateExtensionBehavior, so the propagation is transitive (AEP -> GL_EXT_geometry_shader ->
// GL_EXT_shader_io_blocks); the fixed-point loop below is that re-entry.
void AddImpliedExtensions(std::set<MobileGL::String>& extensions) {
if (extensions.empty()) return;
bool grew = true;
while (grew) {
grew = false;
for (const auto& [source, implied] : GetExtensionImplications()) {
if (extensions.count(source) == 0) continue;
for (const char* name : implied) {
grew |= extensions.insert(name).second;
}
}
}
}
// Reads "<digits> [profile]" out of a "#version" directive whose keyword ends at `probe`, and
// decides whether it is one MobileGL is willing to rewrite. `code` must be the masked source,
// so a trailing comment has already become blanks.
bool ParseVersionDirectiveBody(const MobileGL::String& code, SizeT probe, SizeT lineEnd,
ParsedVersionDirective& out) {
SkipDirectiveWhitespace(code, probe, lineEnd);
unsigned version = 0;
bool hasVersionDigits = false;
while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') {
hasVersionDigits = true;
version = version * 10 + static_cast<unsigned>(code[probe] - '0');
probe++;
}
if (!hasVersionDigits) return false;
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String profileToken = ReadDirectiveIdentifier(code, probe, lineEnd);
bool profileTokenValid = true;
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
if (profileToken.empty() || profileToken == "core") {
profile = MobileGL::ShaderProfile::Core;
} else if (profileToken == "es" || profileToken == "ES") {
profile = MobileGL::ShaderProfile::ES;
} else if (profileToken == "compatibility") {
profile = MobileGL::ShaderProfile::Compatibility;
} else {
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any downstream
// routing, but mark the directive malformed.
profile = MobileGL::ShaderProfile::Core;
profileTokenValid = false;
}
// Comments are already masked to spaces, so anything non-blank left on the line is real
// trailing garbage: "#version 330 foobar" / "#version 330.0".
SkipDirectiveWhitespace(code, probe, lineEnd);
const bool hasTrailingTokens = probe < lineEnd;
out.version = version;
out.profile = profile;
out.isValid = IsRecognizedGlslVersion(version) && profileTokenValid && !hasTrailingTokens;
return true;
}
ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) { ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) {
const MobileGL::String code = MaskCommentsAndQuotedText(source); const MobileGL::String code = MaskCommentsAndQuotedText(source);
ShaderLanguageInfo info; ShaderLanguageInfo info;
info.hasUtf8Bom = HasUtf8Bom(source); info.hasUtf8Bom = HasUtf8Bom(source);
// An exact repeat of the accepted directive, wherever on the line it sits. Recorded for
// BlankRedundantVersionDirectives; never called before a valid first directive was found,
// which is what keeps a LONE misplaced #version rejected.
const auto recordIfRedundant = [&info, &code](SizeT hashPos, SizeT lineEnd) {
if (!info.hasValidVersionDirective) return;
SizeT probe = hashPos + 1;
SkipDirectiveWhitespace(code, probe, lineEnd);
if (ReadDirectiveIdentifier(code, probe, lineEnd) != "version") return;
ParsedVersionDirective parsed;
if (!ParseVersionDirectiveBody(code, probe, lineEnd, parsed)) return;
if (!parsed.isValid || parsed.version != info.version || parsed.profile != info.profile) return;
info.redundantVersionDirectives.push_back({hashPos, lineEnd});
};
SizeT lineStart = 0; SizeT lineStart = 0;
while (lineStart < code.size()) { while (lineStart < code.size()) {
SizeT lineEnd = code.find('\n', lineStart); SizeT lineEnd = code.find('\n', lineStart);
@@ -256,46 +374,49 @@ namespace {
probe = 3; probe = 3;
} }
SkipDirectiveWhitespace(code, probe, lineEnd); SkipDirectiveWhitespace(code, probe, lineEnd);
if (probe < lineEnd && code[probe] == '#') { if (probe >= lineEnd || code[probe] != '#') {
// A directive that is not first on its line is not a directive at all - except for
// the one case glShaderSource creates on its own: two strings each headed by a
// #version splice the second into the tail of the first. Only an EXACT repeat of
// the directive already accepted is recognized here; see
// BlankRedundantVersionDirectives for why that one is tolerated and nothing else.
//
// Gated on a directive having been accepted already, so an ordinary shader - which
// has none of these - pays nothing at all before its #version line.
//
// BOUNDED TO THE LINE, and that is not a detail. std::string::find(char, pos) has
// no end bound, so a `code.find('#', probe)` filtered afterwards by
// `hashPos < lineEnd` scans from this line to the END OF THE SOURCE whenever no
// '#' follows - which is the ordinary shape of a resolved shader-pack source (one
// leading #version, nothing after it), and it makes this whole sweep quadratic in
// shader size. A 131 KB glsl-transformer output in .trace-work has exactly one '#'
// in the file. Searching the line span is behaviour-identical: every hashPos the
// unbounded form could accept already had to satisfy hashPos < lineEnd.
if (info.hasValidVersionDirective && probe < lineEnd) {
const void* hash = std::memchr(code.data() + probe, '#', lineEnd - probe);
if (hash != nullptr) {
recordIfRedundant(static_cast<SizeT>(static_cast<const char*>(hash) - code.data()),
lineEnd);
}
}
} else {
const SizeT directiveStart = probe; const SizeT directiveStart = probe;
probe++; probe++;
SkipDirectiveWhitespace(code, probe, lineEnd); SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd); const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd);
if (directive == "version" && !info.HasVersionDirective()) { if (directive == "version") {
SkipDirectiveWhitespace(code, probe, lineEnd); ParsedVersionDirective parsed;
unsigned version = 0; if (ParseVersionDirectiveBody(code, probe, lineEnd, parsed)) {
bool hasVersionDigits = false; if (!info.HasVersionDirective()) {
while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') { info.version = parsed.version;
hasVersionDigits = true; info.profile = parsed.profile;
version = version * 10 + static_cast<unsigned>(code[probe] - '0'); info.versionDirectiveStart = directiveStart;
probe++; info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
} info.hasValidVersionDirective = parsed.isValid;
if (hasVersionDigits) {
info.version = version;
info.versionDirectiveStart = directiveStart;
info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd);
bool profileTokenValid = true;
if (profile.empty() || profile == "core") {
info.profile = MobileGL::ShaderProfile::Core;
} else if (profile == "es" || profile == "ES") {
info.profile = MobileGL::ShaderProfile::ES;
} else if (profile == "compatibility") {
info.profile = MobileGL::ShaderProfile::Compatibility;
} else { } else {
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any recordIfRedundant(directiveStart, lineEnd);
// downstream routing, but mark the directive malformed.
info.profile = MobileGL::ShaderProfile::Core;
profileTokenValid = false;
} }
// Comments are already masked to spaces, so anything non-blank left on the
// line is real trailing garbage: "#version 330 foobar" / "#version 330.0".
SkipDirectiveWhitespace(code, probe, lineEnd);
const bool hasTrailingTokens = probe < lineEnd;
info.hasValidVersionDirective =
IsRecognizedGlslVersion(info.version) && profileTokenValid && !hasTrailingTokens;
} }
} else if (directive == "extension") { } else if (directive == "extension") {
SkipDirectiveWhitespace(code, probe, lineEnd); SkipDirectiveWhitespace(code, probe, lineEnd);
@@ -309,6 +430,10 @@ namespace {
extension == "GL_NV_gpu_shader5"; extension == "GL_NV_gpu_shader5";
const bool enablesExtension = behavior == "enable" || behavior == "require" || const bool enablesExtension = behavior == "enable" || behavior == "require" ||
behavior == "warn"; behavior == "warn";
if (!extension.empty()) {
info.namedExtensions.insert(extension);
if (enablesExtension) info.enabledExtensions.insert(extension);
}
// Gate the whole source if it ever opts into either extension. This is deliberately // Gate the whole source if it ever opts into either extension. This is deliberately
// conservative around conditional directives and keeps legal sample qualifiers intact. // conservative around conditional directives and keeps legal sample qualifiers intact.
info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension); info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension);
@@ -319,6 +444,19 @@ namespace {
lineStart = lineEnd + (hasLineBreak ? 1 : 0); lineStart = lineEnd + (hasLineBreak ? 1 : 0);
} }
// Both extension sets are closed under glslang's implication graph BEFORE anyone reads
// them, so every consumer sees the same expansion and none of them can forget it. Applied
// here rather than at the directive because an implication may be named before its source
// (`#extension GL_EXT_shader_io_blocks : disable` then `... AEP : require`), and the
// fixed point of the whole set is what glslang's re-entrant propagation ends up at.
//
// enablesGpuShader5 is deliberately NOT recomputed from the expanded set: it gates the
// 460 version escalation on the DESKTOP ARB/NV spellings, and AEP implies the ESSL
// GL_EXT_gpu_shader5, a different extension. An ES source is rewritten to 460 core
// anyway, so there is nothing for the escalation to do there.
AddImpliedExtensions(info.namedExtensions);
AddImpliedExtensions(info.enabledExtensions);
return info; return info;
} }
@@ -367,9 +505,40 @@ namespace {
// (FindAfterVersionDirective -> InspectShaderLanguage). Each branch below leaves the bytes // (FindAfterVersionDirective -> InspectShaderLanguage). Each branch below leaves the bytes
// ahead of the directive untouched apart from the BOM erase, and each replacement text is // ahead of the directive untouched apart from the BOM erase, and each replacement text is
// exactly one newline-terminated line, so the arithmetic is exact in all three cases. // exactly one newline-terminated line, so the arithmetic is exact in all three cases.
// An exact repeat of the #version directive the shader already declared, blanked out.
//
// Strictly a repeat: InspectShaderLanguage only records a range here when the FIRST directive
// was well-formed and the later one is well-formed, names the same version number and the same
// profile, and is therefore semantically a no-op. Everything else - a differing version, a
// malformed one, or a lone #version that is simply not first - is left exactly where the
// application put it, so KHR-GL33.shaders.preprocessor.directive.version_not_first_statement_*
// and the version_invalid_token_* family keep failing to compile the way they must.
//
// Why tolerate even the repeat: glShaderSource concatenates its strings with nothing added
// between them (GL 4.6 core 7.1), and a caller that puts a #version at the head of BOTH strings
// gets the second one spliced into the tail of the first - which is exactly what VK-GL-CTS's
// ShaderImageLoadStoreBase::BuildProgram does (kGLSLPrec ends without a newline, and
// NegativeUniform's own sources begin with "#version 310 es"). Desktop drivers accept it; the
// duplicate says nothing new, so honouring it costs no semantics.
//
// Blanked rather than erased so that every offset in `info` - which was measured against this
// same source - stays valid, and so the line count, and with it __LINE__ and every glslang
// diagnostic, is untouched.
void BlankRedundantVersionDirectives(MobileGL::String& source, const ShaderLanguageInfo& info) {
for (const auto& [begin, end] : info.redundantVersionDirectives) {
if (begin >= source.size() || end > source.size() || begin >= end) continue;
std::fill(source.begin() + static_cast<std::ptrdiff_t>(begin),
source.begin() + static_cast<std::ptrdiff_t>(end), ' ');
}
}
SizeT NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) { SizeT NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
const SizeT bomBytes = info.hasUtf8Bom ? 3 : 0; const SizeT bomBytes = info.hasUtf8Bom ? 3 : 0;
// First, while every offset in `info` still refers to the untouched source. Each range
// lies strictly after the first directive, so nothing below has to account for it.
BlankRedundantVersionDirectives(source, info);
// A malformed #version (329, 331, bad profile, float/trailing tokens) is left exactly as the // A malformed #version (329, 331, bad profile, float/trailing tokens) is left exactly as the
// application wrote it so glslang rejects it - rewriting it to "#version 330 core" would // application wrote it so glslang rejects it - rewriting it to "#version 330 core" would
// silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so // silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so
@@ -1435,6 +1604,215 @@ namespace {
"#define gl_DepthRange mg_DepthRange\n"; "#define gl_DepthRange mg_DepthRange\n";
source.insert(afterVersion.Get(source), shim); source.insert(afterVersion.Get(source), shim);
} }
// Whole-identifier search over an already-masked source. A bare find() would fire on
// "mg_NumSamplesFoo" and on the word inside a comment; this fires only on the token.
bool MaskedSourceHasIdentifier(const MobileGL::String& masked, MobileGL::StringView identifier) {
SizeT pos = 0;
while ((pos = masked.find(identifier.data(), pos, identifier.size())) != MobileGL::String::npos) {
const SizeT end = pos + identifier.size();
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(masked[pos - 1]);
const bool hasRightBoundary = end >= masked.size() || !IsIdentifierChar(masked[end]);
if (hasLeftBoundary && hasRightBoundary) return true;
pos = end;
}
return false;
}
// The extension macros glslang's ES preamble defines and its DESKTOP preamble does not
// (TParseVersions::getPreamble, Versions.cpp). Transcribed rather than derived because the
// preamble is a string literal inside glslang with no programmatic accessor; the SET is what
// matters, and it is stable - these are the AEP/OES/EXT names ESSL has carried since 3.10.
//
// GL_ES and GL_FRAGMENT_PRECISION_HIGH are DELIBERATELY absent. The shader really is being
// compiled as desktop by the time this runs, so flipping an `#ifdef GL_ES` branch would hand
// glslang the ESSL half of a shader written to be portable - which is the branch that does not
// parse under core 4.60. (GL_FRAGMENT_PRECISION_HIGH is in glslang's desktop preamble anyway.)
bool IsEsOnlyPreambleExtensionMacro(const MobileGL::String& name, unsigned version) {
// Guarded by an ES version in glslang's preamble; the rest are unconditional.
if (name == "GL_NV_shader_noperspective_interpolation") return version >= 300;
static const std::set<MobileGL::String> kEsOnlyPreambleMacros = {
"GL_ANDROID_extension_pack_es31a",
"GL_EXT_YUV_target",
"GL_EXT_blend_func_extended",
"GL_EXT_frag_depth",
"GL_EXT_geometry_point_size",
"GL_EXT_geometry_shader",
"GL_EXT_gpu_shader5",
"GL_EXT_primitive_bounding_box",
"GL_EXT_shader_implicit_conversions",
"GL_EXT_shader_io_blocks",
"GL_EXT_shader_texture_lod",
"GL_EXT_shadow_samplers",
"GL_EXT_tessellation_point_size",
"GL_EXT_tessellation_shader",
"GL_EXT_texture_buffer",
"GL_EXT_texture_cube_map_array",
"GL_OES_EGL_image_external",
"GL_OES_EGL_image_external_essl3",
"GL_OES_geometry_point_size",
"GL_OES_geometry_shader",
"GL_OES_gpu_shader5",
"GL_OES_primitive_bounding_box",
"GL_OES_sample_variables",
"GL_OES_shader_image_atomic",
"GL_OES_shader_io_blocks",
"GL_OES_shader_multisample_interpolation",
"GL_OES_standard_derivatives",
"GL_OES_tessellation_point_size",
"GL_OES_tessellation_shader",
"GL_OES_texture_3D",
"GL_OES_texture_buffer",
"GL_OES_texture_cube_map_array",
"GL_OES_texture_storage_multisample_2d_array",
};
return kEsOnlyPreambleMacros.count(name) != 0;
}
// Marker recording that PreprocessShaderSource rewrote an ES-profile source to desktop AND
// that the source names at least one extension whose macro glslang's ES preamble would have
// defined. The declared ESSL version rides along because two of those macros are themselves
// version-gated in glslang.
//
// A marker rather than a "#define" block, because the macros CANNOT live in the shader text:
// glslang rejects "#define GL_..." outright (TParseContext::reservedPpErrorCheck, "names
// beginning with GL_ can't be (un)defined") for every string the application supplied - but
// deliberately NOT for the preamble strings, which is where its own ES preamble defines them
// (CPPdefine's `if (ppToken->loc.string >= 0)` gate; the two preambles sit at string index -2
// and -1). So the macros have to reach glslang through TShader::setPreamble, and this marker is
// how the decision - which needs the ORIGINAL profile and version, both gone by then - travels
// to the compiler. It rides inside the preprocessed source, so the preprocess cache and the
// translation cache both key on it for free.
constexpr const char* kEsPreambleMarkerPrefix = "/*mobilegl-es-preamble:";
// The set of macros named by an ES source that the desktop preamble will not define. Shared by
// the injector below and by CollectEsPreambleMacroDefines, which re-derives it at compile time
// from the marker - one whitelist, one version rule, no chance of the two disagreeing.
MobileGL::String BuildEsPreambleMacroList(const MobileGL::String& source, unsigned esVersion) {
MobileGL::String macros;
// std::set iteration order, so the result is deterministic for the caches and for the
// byte-exact preprocessor tests.
for (const MobileGL::String& extension : InspectShaderLanguage(source).namedExtensions) {
if (!IsEsOnlyPreambleExtensionMacro(extension, esVersion)) continue;
macros += "#define " + extension + " 1\n";
}
return macros;
}
// GetNormalizedVersionDirective rewrites every ES-profile shader to "#version 460 core", so
// glslang deduces a desktop profile and emits its DESKTOP preamble - and every ES-only
// extension macro the shader is entitled to disappears with it. A CTS shader guarded by
// `#if !GL_OES_sample_variables / this is broken / #endif` then takes the broken branch.
//
// The extension BEHAVIOUR survives the rewrite (glslang honours "#extension X : require" under
// either profile), so this is a preamble-fidelity gap and nothing more; restoring the macros is
// the whole fix.
//
// Strictly limited to extensions the source itself NAMES in an #extension directive. Any macro
// injected into a desktop parse can flip a preprocessor branch, and the ES preamble carries
// three dozen of them - defining the lot would rewrite shaders that never asked.
void MarkEsPreambleExtensionMacros(const ShaderLanguageInfo& info, MobileGL::String& source,
AfterVersionAnchor& afterVersion) {
// Only where the rewrite actually happened: a malformed directive is left for glslang to
// reject, and a desktop source already gets the preamble it is entitled to.
if (info.profile != MobileGL::ShaderProfile::ES) return;
if (!info.hasValidVersionDirective) return;
if (info.namedExtensions.empty()) return;
// namedExtensions is already closed under glslang's implication graph, so a source that
// names only GL_ANDROID_extension_pack_es31a marks its twelve members too - glslang's ES
// preamble defines all of them, and the CTS-shaped "#if !GL_OES_sample_variables" guard
// reads one of them.
//
// `#extension all : warn` is deliberately NOT honoured here, unlike in the built-in gate.
// The two answer different questions: the gate asks "would glslang have this extension
// turned on", where `all` genuinely says yes, while this asks "which preamble macros did
// the ES -> desktop rewrite take away". glslang's preamble runs BEFORE any #extension line
// and defines the ES macros regardless of behavior, so `all` adds no information - and
// emitting all thirty-five for a source that named nothing is exactly the broad rewrite
// the named-extensions-only policy exists to avoid.
const bool hasMacroToRestore =
std::any_of(info.namedExtensions.begin(), info.namedExtensions.end(),
[&info](const MobileGL::String& extension) {
return IsEsOnlyPreambleExtensionMacro(extension, info.version);
});
// Nothing the desktop preamble is missing: leave the source byte-identical.
if (!hasMacroToRestore) return;
source.insert(afterVersion.Get(source),
MobileGL::String(kEsPreambleMarkerPrefix) + std::to_string(info.version) + "*/\n");
}
// "Would glslang have this extension turned on?", mirroring TParseVersions::extensionTurnedOn.
//
// Two spellings besides the name itself reach it. The implication graph is already folded into
// enabledExtensions (AddImpliedExtensions), so only `#extension all : <behavior>` is left:
// glslang applies that behavior to EVERY registered extension at once, and rejects `all` with
// require/enable outright (Versions.cpp:1136-1141) - so the only spellings that survive are
// `all : warn`, which turns everything ON (behavior != EBhDisable), and `all : disable`.
// InspectShaderLanguage only records a name in enabledExtensions for enable/require/warn, so
// the literal "all" appearing here means `all : warn` and nothing else.
bool ExtensionTurnedOn(const ShaderLanguageInfo& info, const char* extension) {
return info.enabledExtensions.count(extension) != 0 || info.enabledExtensions.count("all") != 0;
}
// gl_NumSamples is legal in this source only where glslang would have declared it with a
// non-SPIR-V target (Initialize.cpp): desktop from 4.00 core, or from 1.30 with
// ARB_sample_shading; ESSL from 3.20, or from 3.10 with OES_sample_variables - the last of
// which GL_ANDROID_extension_pack_es31a also turns on, via the implication graph.
//
// The gate matters because the shim ends in "#define gl_NumSamples mg_NumSamples", and a
// #define is not scoped by anything: defining it for a source where the built-in does not
// exist would silently legalize a shader a conformant implementation rejects.
bool SourceMayUseSampleVariables(const ShaderLanguageInfo& info) {
if (!info.HasVersionDirective() || !info.hasValidVersionDirective) return false;
if (info.profile == MobileGL::ShaderProfile::ES) {
if (info.version >= 320) return true;
return info.version >= 310 && ExtensionTurnedOn(info, "GL_OES_sample_variables");
}
if (info.version >= 400) return true;
return info.version >= 130 && ExtensionTurnedOn(info, "GL_ARB_sample_shading");
}
// gl_NumSamples has no SPIR-V built-in to lower to, so glslang declares it only when it is NOT
// targeting SPIR-V - both the desktop branch and the ES branch of Initialize.cpp wrap the
// `uniform int gl_NumSamples;` line in `if (spvVersion.spv == 0)`. MobileGL always targets
// SPIR-V (ShaderCompiler sets EShTargetSpv on the OpenGL path as well as the Vulkan one), so
// the symbol is never in the table and every shader that reads it dies at compile time with
// "'gl_NumSamples' : undeclared identifier".
//
// Lower it to a real uniform instead. `uniform int mg_NumSamples;` is a default-block uniform,
// which the relaxed parse folds into MGL_GLOBAL_UBO - the one buffer BOTH backends already
// upload per draw - and the draw path writes the current draw framebuffer's sample count into
// it. Deliberately not a link-time constant: one program may be drawn into framebuffers of
// different sample counts, and baking the count at link would quietly hand it the wrong one.
//
// The alternative - deleting the `spvVersion.spv == 0` guard in the glslang fork - is worse,
// and not only because it is a fork change: glslang would then place a `gl_`-prefixed member
// inside MGL_GLOBAL_UBO, and ESSL reserves `gl_`, so the ES driver would reject SPIRV-Cross's
// output on the DirectGLES path.
void InjectNumSamplesBuiltinShim(MobileGL::ShaderStage stage, const ShaderLanguageInfo& info,
MobileGL::String& source, AfterVersionAnchor& afterVersion) {
// gl_NumSamples exists in the fragment stage only, in every profile.
if (stage != MobileGL::ShaderStage::Fragment) return;
if (!SourceMayUseSampleVariables(info)) return;
// Cheap reject before paying for the mask; the token cannot be there if the bytes are not.
if (source.find("gl_NumSamples") == MobileGL::String::npos) return;
const MobileGL::String masked = MaskCommentsAndQuotedText(source);
if (!MaskedSourceHasIdentifier(masked, "gl_NumSamples")) return;
// Someone already occupies the name - a re-preprocess of an already-shimmed source, or an
// application that happens to use it. Either way a second declaration would not compile.
if (MaskedSourceHasIdentifier(masked, MobileGL::MG_Util::ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)) {
return;
}
constexpr const char* shim =
"uniform int mg_NumSamples;\n"
"#define gl_NumSamples mg_NumSamples\n";
source.insert(afterVersion.Get(source), shim);
}
} // namespace } // namespace
namespace MobileGL { namespace MobileGL {
@@ -1463,6 +1841,13 @@ namespace MobileGL {
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them. // via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
NormalizeLineDirectives(source, afterVersion.Get(source)); NormalizeLineDirectives(source, afterVersion.Get(source));
// An ES source rewritten to desktop has lost glslang's ES preamble, and the macros
// it carried are what the shader's own #if guards read. Keyed off originalLanguage
// because the directive has already been rewritten by now and no longer says "es";
// the macros themselves are restored through the compiler's preamble, which is why
// this only leaves a marker behind (see kEsPreambleMarkerPrefix).
MarkEsPreambleExtensionMacros(originalLanguage, source, afterVersion);
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+) // noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders // and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
// natively and SPIRV-Cross turns into ESSL `noperspective` + the // natively and SPIRV-Cross turns into ESSL `noperspective` + the
@@ -1487,9 +1872,31 @@ namespace MobileGL {
ModernizeLegacyGLSL(stage, source, afterVersion); ModernizeLegacyGLSL(stage, source, afterVersion);
InjectDepthRangeBuiltinShim(stage, source, afterVersion); InjectDepthRangeBuiltinShim(stage, source, afterVersion);
InjectNumSamplesBuiltinShim(stage, originalLanguage, source, afterVersion);
} }
String CollectEsPreambleMacroDefines(const String& preprocessedSource) {
const SizeT markerStart = preprocessedSource.find(kEsPreambleMarkerPrefix);
if (markerStart == String::npos) return {};
SizeT probe = markerStart + std::char_traits<char>::length(kEsPreambleMarkerPrefix);
unsigned esVersion = 0;
bool hasDigits = false;
while (probe < preprocessedSource.size() && preprocessedSource[probe] >= '0' &&
preprocessedSource[probe] <= '9') {
hasDigits = true;
esVersion = esVersion * 10 + static_cast<unsigned>(preprocessedSource[probe] - '0');
if (esVersion > 1000) return {}; // absurd; not a marker this pipeline wrote
probe++;
}
// Only MobileGL's own marker, spelled exactly: a shader that happens to contain the
// prefix inside a comment of its own must not be able to steer the preamble.
if (!hasDigits || preprocessedSource.compare(probe, 2, "*/") != 0) return {};
return BuildEsPreambleMacroList(preprocessedSource, esVersion);
}
Bool RetargetLegacyVersionDirectiveTo460(String& source) { Bool RetargetLegacyVersionDirectiveTo460(String& source) {
// Re-inspect rather than searching for the literal directive: it is not necessarily at // Re-inspect rather than searching for the literal directive: it is not necessarily at
// offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere // offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere
@@ -40,6 +40,27 @@ namespace MobileGL {
// accept - can be retried instead of failing to compile. // accept - can be retried instead of failing to compile.
Bool RetargetLegacyVersionDirectiveTo460(String& source); Bool RetargetLegacyVersionDirectiveTo460(String& source);
// The "#define <EXT> 1" lines an ES-profile source needs restored after
// PreprocessShaderSource rewrote its #version to desktop, or "" for every other source.
//
// glslang defines the OES/AEP extension macros only in its ES preamble
// (TParseVersions::getPreamble), selected by the profile it deduces from the directive -
// so the rewrite silently takes them away and the shader's own
// "#if !GL_OES_sample_variables" guard flips. They cannot simply be written into the
// shader text: "#define GL_..." is a hard error for every application-supplied string
// (TParseContext::reservedPpErrorCheck). They therefore go into glslang's CUSTOM
// PREAMBLE, which sits at string index -1 and is exempt from that check by the same
// gate that exempts glslang's own preamble - hence a separate function called by the
// compiler rather than another injection pass.
//
// Deliberately narrow: only extensions the source itself NAMES in an #extension
// directive, and never GL_ES or GL_FRAGMENT_PRECISION_HIGH. The shader really is being
// compiled as desktop now, and flipping "#ifdef GL_ES" branches would break far more
// than it fixes - which is why this is a partial fix by construction. The clean
// long-term fix is to stop rewriting ES sources to desktop at all; the comment in
// GetNormalizedVersionDirective records why that has not happened.
String CollectEsPreambleMacroDefines(const String& preprocessedSource);
// GLSL reserves a few names glslang happily accepts as identifiers ("packed", // GLSL reserves a few names glslang happily accepts as identifiers ("packed",
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the // "row_major" outside a layout(...) list, the image*Shadow family). Returns the
// compile-error text for the first violation, or nullopt for a clean source. // compile-error text for the first violation, or nullopt for a clean source.
@@ -8,6 +8,8 @@
#include "SpvcSession.h" #include "SpvcSession.h"
#include <algorithm>
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
@@ -184,11 +186,29 @@ namespace MobileGL {
const SpvId* p_spirv = spirv.data(); const SpvId* p_spirv = spirv.data();
size_t word_count = spirv.size(); size_t word_count = spirv.size();
spvc_context_create(&context); // Every step is checked, and each guards the next: the C API writes its
spvc_context_parse_spirv(context, p_spirv, word_count, &ir); // out-param only on success, so passing a failed step's null handle to the
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, // step after it is a raw dereference (spvc_context_create_compiler does
&compiler); // `parsed_ir->parsed`, spvc_compiler_create_shader_resources does
spvc_compiler_create_shader_resources(compiler, &resources); // `compiler->context`). IsTranspileReady() is how a caller asks whether this
// sequence got all the way through.
if (spvc_context_create(&context) != SPVC_SUCCESS) {
context = nullptr;
return;
}
if (spvc_context_parse_spirv(context, p_spirv, word_count, &ir) != SPVC_SUCCESS) {
ir = nullptr;
return;
}
if (spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir,
SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler) != SPVC_SUCCESS) {
compiler = nullptr;
return;
}
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
resources = nullptr;
return;
}
} else if (usage & SessionUsageBit::Reflection) { } else if (usage & SessionUsageBit::Reflection) {
SpvReflectResult result = spvReflectCreateShaderModule( SpvReflectResult result = spvReflectCreateShaderModule(
spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule); spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule);
@@ -496,6 +516,271 @@ namespace MobileGL {
SPVC_CHK_RETURN SPVC_CHK_RETURN
} }
namespace {
// How many 32-bit components a captured variable occupies, which is what the
// gl_SkipComponentsN padding below is counted in. Matrices and arrays multiply.
Uint32 XfbComponentCount(spvc_compiler compiler, spvc_type_id typeId) {
const spvc_type type = spvc_compiler_get_type_handle(compiler, typeId);
if (type == nullptr) return 0;
Uint32 components = spvc_type_get_vector_size(type) * spvc_type_get_columns(type);
const unsigned dimensions = spvc_type_get_num_array_dimensions(type);
for (unsigned d = 0; d < dimensions; ++d) {
const unsigned length = spvc_type_get_array_dimension(type, d);
if (length != 0) components *= length;
}
// A double occupies two component slots per scalar (GL 4.6 core 11.1.2.1).
const spvc_basetype base = spvc_type_get_basetype(type);
if (base == SPVC_BASETYPE_FP64 || base == SPVC_BASETYPE_INT64 ||
base == SPVC_BASETYPE_UINT64) {
components *= 2;
}
return components;
}
} // namespace
namespace {
// The four gl_PerVertex members, by their GL interface names. These are the only
// built-ins GL lets transform feedback capture, and a SPIR-V module names them by
// BuiltIn decoration rather than by string - so the mapping has to live somewhere.
const char* XfbBuiltInName(SpvBuiltIn builtin) {
switch (builtin) {
case SpvBuiltInPosition:
return "gl_Position";
case SpvBuiltInPointSize:
return "gl_PointSize";
case SpvBuiltInClipDistance:
return "gl_ClipDistance";
case SpvBuiltInCullDistance:
return "gl_CullDistance";
default:
return nullptr;
}
}
} // namespace
Vector<SpirvXfbCapture> SpvcSession::ReflectTransformFeedbackCaptures() const {
Vector<SpirvXfbCapture> captures;
if (compiler == nullptr || resources == nullptr) return captures;
// XfbBuffer/XfbStride sit on the declaring VARIABLE; Offset sits on the variable
// for a plain output and on each MEMBER for a block.
auto readVariableDecorations = [this](SpvId id, Uint32& outBuffer, Uint32& outStride) {
outBuffer = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbBuffer) == SPVC_TRUE
? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbBuffer)
: 0u;
outStride = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbStride) == SPVC_TRUE
? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbStride)
: 0u;
};
// ---- application outputs: plain variables and application blocks ----
const spvc_reflected_resource* outputs = nullptr;
SizeT outputCount = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
&outputCount) == SPVC_SUCCESS) {
for (SizeT i = 0; i < outputCount; ++i) {
const spvc_reflected_resource& output = outputs[i];
Uint32 buffer = 0;
Uint32 stride = 0;
readVariableDecorations(output.id, buffer, stride);
const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id);
const unsigned memberCount =
type != nullptr && spvc_type_get_basetype(type) == SPVC_BASETYPE_STRUCT
? spvc_type_get_num_member_types(type)
: 0u;
if (memberCount == 0) {
if (spvc_compiler_has_decoration(compiler, output.id, SpvDecorationOffset) != SPVC_TRUE) {
continue;
}
SpirvXfbCapture capture;
capture.name = output.name ? output.name : "";
capture.buffer = buffer;
capture.stride = stride;
capture.offset = spvc_compiler_get_decoration(compiler, output.id, SpvDecorationOffset);
capture.componentCount = XfbComponentCount(compiler, output.type_id);
if (!capture.name.empty()) captures.push_back(Move(capture));
continue;
}
for (unsigned member = 0; member < memberCount; ++member) {
if (spvc_compiler_has_member_decoration(compiler, output.base_type_id, member,
SpvDecorationOffset) != SPVC_TRUE) {
continue;
}
const char* memberName =
spvc_compiler_get_member_name(compiler, output.base_type_id, member);
if (memberName == nullptr || *memberName == '\0') continue;
SpirvXfbCapture capture;
const String blockName = output.name ? String(output.name) : String{};
// GL's capture interface spells an application block's member
// "Block.member"; a redeclared built-in block contributes its members
// by their own names, which the built-in walk below handles.
capture.name = blockName.empty() ? String(memberName)
: blockName + "." + String(memberName);
capture.buffer = buffer;
capture.stride = stride;
capture.offset = spvc_compiler_get_member_decoration(compiler, output.base_type_id,
member, SpvDecorationOffset);
capture.componentCount =
XfbComponentCount(compiler, spvc_type_get_member_type(type, member));
captures.push_back(Move(capture));
}
}
}
// ---- the redeclared built-in block ----
// SPIRV-Cross keeps gl_PerVertex out of the STAGE_OUTPUT list and reports it here
// instead, one entry per built-in member. That is the shape the conformance suite
// feeds in first (`layout(xfb_buffer = 0, xfb_offset = 16) out gl_PerVertex { vec4
// gl_Position; }`), so walking only the list above would have found nothing at all.
const spvc_reflected_builtin_resource* builtins = nullptr;
SizeT builtinCount = 0;
if (spvc_resources_get_builtin_resource_list_for_type(
resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) ==
SPVC_SUCCESS) {
for (SizeT i = 0; i < builtinCount; ++i) {
const spvc_reflected_builtin_resource& entry = builtins[i];
const char* name = XfbBuiltInName(entry.builtin);
if (name == nullptr) continue;
Uint32 buffer = 0;
Uint32 stride = 0;
readVariableDecorations(entry.resource.id, buffer, stride);
const spvc_type blockType =
spvc_compiler_get_type_handle(compiler, entry.resource.base_type_id);
if (blockType == nullptr ||
spvc_type_get_basetype(blockType) != SPVC_BASETYPE_STRUCT) {
continue;
}
// The member index is not in the reflection entry, so it is recovered by
// matching the BuiltIn decoration - the same key the entry is keyed on.
const unsigned memberCount = spvc_type_get_num_member_types(blockType);
for (unsigned member = 0; member < memberCount; ++member) {
if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member,
SpvDecorationBuiltIn) != SPVC_TRUE) {
continue;
}
if (spvc_compiler_get_member_decoration(compiler, entry.resource.base_type_id, member,
SpvDecorationBuiltIn) !=
static_cast<unsigned>(entry.builtin)) {
continue;
}
if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member,
SpvDecorationOffset) != SPVC_TRUE) {
break; // this built-in is present but not captured
}
SpirvXfbCapture capture;
capture.name = name;
capture.buffer = buffer;
capture.stride = stride;
capture.offset = spvc_compiler_get_member_decoration(
compiler, entry.resource.base_type_id, member, SpvDecorationOffset);
capture.componentCount =
XfbComponentCount(compiler, spvc_type_get_member_type(blockType, member));
captures.push_back(Move(capture));
break;
}
}
}
// Capture order IS buffer-then-offset order: that is the order the equivalent
// glTransformFeedbackVaryings request has to name them in for the frontend's
// packer to reproduce the declared layout.
std::stable_sort(captures.begin(), captures.end(),
[](const SpirvXfbCapture& a, const SpirvXfbCapture& b) {
if (a.buffer != b.buffer) return a.buffer < b.buffer;
return a.offset < b.offset;
});
return captures;
}
void SpvcSession::StripTransformFeedbackDecorations() {
if (compiler == nullptr || resources == nullptr) return;
auto stripVariable = [this](SpvId variableId, spvc_type_id baseTypeId) {
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbBuffer);
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbStride);
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationOffset);
const spvc_type type = spvc_compiler_get_type_handle(compiler, baseTypeId);
if (type == nullptr || spvc_type_get_basetype(type) != SPVC_BASETYPE_STRUCT) return;
const unsigned memberCount = spvc_type_get_num_member_types(type);
for (unsigned member = 0; member < memberCount; ++member) {
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationOffset);
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbBuffer);
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbStride);
}
};
const spvc_reflected_resource* outputs = nullptr;
SizeT outputCount = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
&outputCount) == SPVC_SUCCESS) {
for (SizeT i = 0; i < outputCount; ++i) {
stripVariable(outputs[i].id, outputs[i].base_type_id);
}
}
const spvc_reflected_builtin_resource* builtins = nullptr;
SizeT builtinCount = 0;
if (spvc_resources_get_builtin_resource_list_for_type(
resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) ==
SPVC_SUCCESS) {
for (SizeT i = 0; i < builtinCount; ++i) {
stripVariable(builtins[i].resource.id, builtins[i].resource.base_type_id);
}
}
}
spvc_result SpvcSession::SetEntryPoint(const char* name, SpvExecutionModel model) {
// A null compiler or a null/empty name is a FAILURE, not a silent success: the
// caller is asking for a specific entry point and there is none to give it.
if (compiler == nullptr || name == nullptr || *name == '\0') return SPVC_ERROR_INVALID_ARGUMENT;
return spvc_compiler_set_entry_point(compiler, name, model);
}
Bool SpvcSession::SetSpecializationConstants(const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues,
Uint32& outUnknownConstantId) {
if (constantIds.empty()) return true;
if (compiler == nullptr) return false;
const spvc_specialization_constant* declared = nullptr;
SizeT declaredCount = 0;
if (spvc_compiler_get_specialization_constants(compiler, &declared, &declaredCount) != SPVC_SUCCESS) {
outUnknownConstantId = constantIds.front();
return false;
}
for (SizeT i = 0; i < constantIds.size(); ++i) {
const Uint32 wantedId = constantIds[i];
spvc_constant handle = nullptr;
for (SizeT j = 0; j < declaredCount; ++j) {
if (declared[j].constant_id != wantedId) continue;
handle = spvc_compiler_get_constant_handle(compiler, declared[j].id);
break;
}
if (handle == nullptr) {
// ARB_gl_spirv: "INVALID_VALUE is generated if any value in pConstantIndex
// refers to a specialization constant that does not exist in the shader
// module". Reported rather than skipped - a silently ignored id would let
// the shader specialize to something the application never asked for.
outUnknownConstantId = wantedId;
return false;
}
// The GL side hands over a flat GLuint per constant and ARB_gl_spirv says it
// is "interpreted according to the type of the specialization constant", so
// the 32-bit PATTERN is what has to be stored, not a converted number.
// spvc_constant_set_scalar_u32 writes exactly that pattern into the constant's
// scalar union, which SPIRV-Cross then reads back as whatever the constant's
// declared type is - the reinterpretation the extension asks for, for free.
spvc_constant_set_scalar_u32(handle, 0, 0, constantValues[i]);
}
return true;
}
spvc_result SpvcSession::Compile(const char** result) { spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT; if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT SPVC_CHK_INIT
@@ -56,6 +56,19 @@ namespace MobileGL {
} }
}; };
// One output a SPIR-V module asked to have captured, as its Xfb decorations describe
// it. ARB_gl_spirv makes these decorations the ONLY way a SPIR-V program declares
// transform feedback - glTransformFeedbackVaryings has no effect on such a program -
// so a module that carries them and an implementation that ignores them capture
// nothing at all.
struct SpirvXfbCapture {
String name; // the GL interface name: "gl_Position", or "Block.member"
Uint32 buffer = 0; // XfbBuffer on the declaring variable
Uint32 offset = 0; // Offset on the variable or on the member
Uint32 stride = 0; // XfbStride on the declaring variable
Uint32 componentCount = 0; // how many 32-bit components the capture occupies
};
enum class SessionUsageBit { enum class SessionUsageBit {
Reflection = 1 << 0, Reflection = 1 << 0,
Transpile = 1 << 1, Transpile = 1 << 1,
@@ -160,6 +173,43 @@ namespace MobileGL {
// A block carrying only ONE of the two is left exactly as it is: those really do // A block carrying only ONE of the two is left exactly as it is: those really do
// constrain the accesses the shader makes, and the driver is entitled to know. // constrain the accesses the shader makes, and the driver is entitled to know.
spvc_result RelaxReadWriteExclusiveStorageBuffers(); spvc_result RelaxReadWriteExclusiveStorageBuffers();
// ---- GL_ARB_gl_spirv: an APPLICATION-supplied module, not one MobileGL emitted ----
// Select which OpEntryPoint of `model` this session compiles. A module may carry
// several of the same execution model, and glSpecializeShader names the one the
// shader object stands for.
// Whether the transpile constructor actually built a compiler. Every SPIRV-Cross
// handle below is default-null and the C API leaves its out-params untouched on
// failure, so a module SPIRV-Cross cannot parse used to leave `ir` null and then
// have spvc_context_create_compiler dereference it - a raw null read that
// SPVC_BEGIN_SAFE_SCOPE cannot catch. Only glShaderBinary feeds this class bytes
// MobileGL did not generate itself, which is why the check earns its keep now.
Bool IsTranspileReady() const { return compiler != nullptr && resources != nullptr; }
// Read the module's transform-feedback layout out of its Xfb decorations, in
// (buffer, offset) order. Empty when the module declares no capture.
Vector<SpirvXfbCapture> ReflectTransformFeedbackCaptures() const;
// Remove every Xfb decoration the reflection above just read.
//
// This is not tidying: the decorations must not survive into the GLSL this session
// emits. SPIRV-Cross re-emits them as `layout(xfb_buffer = N, xfb_stride = M) out
// gl_PerVertex { layout(xfb_offset = K) ... }`, glslang re-encodes that into the
// regenerated SPIR-V, and the DirectGLES leg then transpiles THAT to ESSL - where
// the same SPIRV-Cross throws "Need GL_ARB_enhanced_layouts for xfb_stride or
// xfb_buffer" and the stage silently fails to build, leaving a program that links
// clean and draws nothing. Stripping them and re-declaring the capture through
// MobileGL's ordinary capture machinery (which both backends already implement)
// routes a SPIR-V program down exactly the path a GLSL program takes.
void StripTransformFeedbackDecorations();
spvc_result SetEntryPoint(const char* name, SpvExecutionModel model);
// Bake glSpecializeShader's values into the module's specialization constants.
// Every value is a GLuint on the GL side and is reinterpreted according to the
// constant's own scalar type, exactly as ARB_gl_spirv specifies ("the value is
// interpreted as the type of the specialization constant"). Returns false and
// sets `outUnknownConstantId` when an id the caller passed is not a
// specialization constant of this module, which the extension makes
// GL_INVALID_VALUE.
Bool SetSpecializationConstants(const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues,
Uint32& outUnknownConstantId);
spvc_result Compile(const char** result); spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const; const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const; const char* GetLastErrorString() const;
+93
View File
@@ -14,6 +14,20 @@ namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO"; inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
// The default-block uniform InjectNumSamplesBuiltinShim declares to stand in for the
// gl_NumSamples built-in, which glslang does not put in the symbol table under a
// SPIR-V target (Initialize.cpp guards both the desktop and the ES declaration on
// `spvVersion.spv == 0`, and MobileGL always targets SPIR-V). The relaxed parse folds
// it into GLOBAL_UBO_NAME like any other default-block uniform, which is what lets
// BOTH backends pick the value up from the one buffer they already upload; the link
// task keeps it out of the GL-visible uniform surface, and the draw path writes the
// current draw framebuffer's sample count into it.
//
// RESERVED, not merely conventional: a shader that declares this name itself keeps
// the shim from firing (the injector bails on it), but if it declares the name AND
// uses gl_NumSamples the link task will still hide its uniform. That is the same
// bargain every mg_-prefixed rewrite in this pipeline strikes.
inline const char* NUM_SAMPLES_UNIFORM_NAME = "mg_NumSamples";
// glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a // glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a
// synthesized storage block named "<this>_<GL atomic-counter binding>" // synthesized storage block named "<this>_<GL atomic-counter binding>"
// (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter // (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter
@@ -66,6 +80,53 @@ namespace MobileGL {
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8; inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8; inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
// ---- Tessellation per-stage resource limits ----
//
// Here for exactly the reason the atomic-counter block above is here. GL 4.6 requires
// glGetIntegerv and the matching gl_MaxTess* built-in constant to report the same
// number (KHR-GL45.limits.max_tess_* reads the query and then compiles a shader that
// writes the built-in into an SSBO and demands equality), and these numbers used to
// exist ONLY inside BuildTBuiltInResource - so gl_MaxTessControlInputComponents
// compiled fine while glGetIntegerv of the same limit had no case at all and answered
// GL_INVALID_ENUM. Never move one of these without the other.
//
// The values are the GL 4.6 core minimums (table 23.55), which is what a frontend that
// synthesizes the tessellation stages onto ES/Vulkan can honestly promise.
inline constexpr Int MAX_TESS_CONTROL_INPUT_COMPONENTS = 128;
inline constexpr Int MAX_TESS_CONTROL_OUTPUT_COMPONENTS = 128;
inline constexpr Int MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS = 16;
inline constexpr Int MAX_TESS_CONTROL_UNIFORM_COMPONENTS = 1024;
inline constexpr Int MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS = 4096;
inline constexpr Int MAX_TESS_EVALUATION_INPUT_COMPONENTS = 128;
inline constexpr Int MAX_TESS_EVALUATION_OUTPUT_COMPONENTS = 128;
inline constexpr Int MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS = 16;
inline constexpr Int MAX_TESS_EVALUATION_UNIFORM_COMPONENTS = 1024;
inline constexpr Int MAX_TESS_PATCH_COMPONENTS = 120;
// ---- Varying and default-block uniform capacities ----
//
// The *_VECTORS limits are the *_COMPONENTS ones counted in vec4s, so they are DERIVED
// rather than typed independently: GL_MAX_VARYING_COMPONENTS said 64 while
// GL_MAX_VARYING_VECTORS said 8, and GL_MAX_VERTEX_UNIFORM_COMPONENTS said 4096 while
// GL_MAX_VERTEX_UNIFORM_VECTORS said 128 - two pairs that cannot both describe the
// same capacity, and both *_VECTORS answers were below the GL 4.5 core minimum
// (15 and 256 respectively). Shared with BuildTBuiltInResource because
// gl_MaxVaryingVectors and gl_MaxVertexUniformVectors expand from the same numbers.
inline constexpr Int MAX_VARYING_COMPONENTS = 64;
inline constexpr Int MAX_VARYING_VECTORS = MAX_VARYING_COMPONENTS / 4;
inline constexpr Int MAX_VERTEX_UNIFORM_COMPONENTS = 4096;
inline constexpr Int MAX_VERTEX_UNIFORM_VECTORS = MAX_VERTEX_UNIFORM_COMPONENTS / 4;
// GL 4.6 core table 23.53 sets the GL_MAX_SAMPLES minimum at 4, and MobileGL floors
// the backend's answer at it (GL_Getter's GetAdvertisedMaxSamples). gl_MaxSamples has
// to expand to the SAME number - it is also what sizes gl_SampleMask[] /
// gl_SampleMaskIn[] and what bounds a constant index into them - so the floor lives
// here and both sides apply it. NOTE the deliberate asymmetry: only MAX_SAMPLES has a
// floor of 4. MAX_INTEGER_SAMPLES, MAX_COLOR_TEXTURE_SAMPLES and
// MAX_DEPTH_TEXTURE_SAMPLES have a minimum of ONE in the same table and are reported
// as the backend probed them.
inline constexpr Int MIN_ADVERTISED_MAX_SAMPLES = 4;
struct EmptyType {}; struct EmptyType {};
enum class ShaderCompileBits : Uint { enum class ShaderCompileBits : Uint {
@@ -84,6 +145,33 @@ namespace MobileGL {
const CompileEnv* env = nullptr; const CompileEnv* env = nullptr;
}; };
// The per-device ceilings a shader-declared `layout(binding = N)` is measured
// against - one per resource kind, because GL gives each kind its own limit and they
// differ by an order of magnitude on real hardware (a Mali-G925 reports 96 combined
// texture image units and 21 image units).
//
// These exist because glslang cannot enforce them for MobileGL. It owns ceilings for
// samplers/images and for atomic counters, and both are switched OFF by the parse
// configuration MobileGL uses everywhere - `spvVersion.vulkan == 0` gates the first
// and `!spvVersion.vulkanRelaxed` the second (ParseHelper.cpp layoutTypeCheck), and
// MobileGL always parses with setEnvClient(EShClientVulkan) +
// setEnvInputVulkanRulesRelaxed(). For uniform and storage BLOCKS glslang quotes the
// spec sentence and then checks nothing at all. Flipping to the OpenGL client to wake
// those checks is not an option (it would change the parse the whole relaxed
// lowering pipeline is built on) and would not even be correct: glslang measures
// IMAGE bindings against the SAMPLER limit and hardcodes that limit at 80, so it
// would reject legal bindings 80..95 and keep under-rejecting images.
//
// Zero or negative means "no ceiling to enforce for this kind" - a backendless
// environment, which every unit test and the pre-init preload path run in.
struct ResourceBindingLimits {
Int MaxSamplerBindings = 0; // GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS
Int MaxImageBindings = 0; // GL_MAX_IMAGE_UNITS
Int MaxUniformBufferBindings = 0; // GL_MAX_UNIFORM_BUFFER_BINDINGS
Int MaxShaderStorageBufferBindings = 0; // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
Int MaxAtomicCounterBufferBindings = 0; // GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS
};
struct ProgramAttrib { struct ProgramAttrib {
Vector<SharedPtr<glslang::TShader>> shaders; Vector<SharedPtr<glslang::TShader>> shaders;
UnorderedMap<String, Uint> explicitVertexInLocations; UnorderedMap<String, Uint> explicitVertexInLocations;
@@ -99,6 +187,11 @@ namespace MobileGL {
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr; UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
std::set<String>* storageBlocksWithoutBinding = nullptr; std::set<String>* storageBlocksWithoutBinding = nullptr;
std::set<String>* uniformBlocksWithoutBinding = nullptr; std::set<String>* uniformBlocksWithoutBinding = nullptr;
// IN: the ceilings above. OUT: the first violation the resolver found, in the
// same capture window and for the same reason - past mapIO's doMap() every
// resource carries an ASSIGNED binding and the question can no longer be asked.
ResourceBindingLimits resourceBindingLimits{};
String* resourceBindingViolation = nullptr;
}; };
struct ProgramBinaryAttrib { struct ProgramBinaryAttrib {
@@ -13,6 +13,8 @@
#include "TMglGlslIoResolver.h" #include "TMglGlslIoResolver.h"
#include <cstring> #include <cstring>
#include <cstdlib>
#include <string>
#include <MG_Util/ShaderTranspiler/Types.h> #include <MG_Util/ShaderTranspiler/Types.h>
@@ -165,6 +167,101 @@ namespace MobileGL {
// before the preprocessor's macros were expanded and therefore could not read // before the preprocessor's macros were expanded and therefore could not read
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of // `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
// its storage blocks. Asking the AST instead makes the macro case ordinary. // its storage blocks. Asking the AST instead makes the macro case ordinary.
// GLSL 4.30 4.4.5 and ES 3.1 4.4.4: `layout(binding = N)` on any opaque uniform, uniform
// block, storage block or atomic counter is a COMPILE-TIME error when N is not less than that
// resource kind's implementation limit - and, for an ARRAY of them, when base + count - 1 is
// not. MobileGL enforces it here rather than at compile because here is the last point where
// `qualifier.hasBinding()` still means "the SHADER said so" (see the comment on the caller),
// and because the per-device ceilings are deliberately not part of the compile pipeline's
// memo keys. The conformance suite accepts a link-time rejection: its predicate is
// compiledAndLinked(), which is the AND of the two.
//
// FIVE KINDS HERE, AND ONE OF THEM IS ALSO CHECKED EARLIER. Before this, exactly one kind -
// shader-storage blocks - was checked at all, by a bespoke lexical scan of the shader source,
// which is why the storage sub-family was the one that passed while sampler, image,
// uniform-block and atomic-counter bindings sailed past every ceiling.
//
// That scan is deliberately KEPT (ShaderCompileTask.cpp's MaxShaderStorageBufferBindings
// explains why: GLSL makes an over-range binding a COMPILE-time error, and the relaxed Vulkan
// parse leaves the scan as the only place MobileGL can raise one). So the storage arm has two
// enforcement points and the other four have this one. What keeps them from drifting is not
// that there is only one site but that both read the SAME numbers - ResolveResourceBindingLimits
// is the single derivation, and neither site computes a ceiling of its own.
void TMglGlslIoResolver::CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name) {
if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return;
if (!m_bindingViolation->empty()) return; // first violation wins; the link is already lost
const glslang::TQualifier& qualifier = type.getQualifier();
const char* kind = nullptr;
const char* limitName = nullptr;
Int limit = 0;
long long binding = -1;
if (type.getBasicType() == glslang::EbtSampler && qualifier.hasBinding()) {
const bool isImage = type.getSampler().isImage();
kind = isImage ? "image" : "sampler";
limitName = isImage ? "GL_MAX_IMAGE_UNITS" : "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS";
limit = isImage ? m_bindingLimits->MaxImageBindings : m_bindingLimits->MaxSamplerBindings;
binding = qualifier.layoutBinding;
} else if (type.getBasicType() == glslang::EbtBlock) {
// An atomic counter never reaches here as a counter: the relaxed parse has already
// folded it into a synthesized "gl_AtomicCounterBlock_<binding>" storage block whose
// TRAILING NUMBER is the GL binding the shader asked for (ParseContextBase::
// growAtomicCounterBlock names it from bufferBinding). That name is the only surviving
// record of the declaration, so it is what the counter ceiling is read off.
const Int counterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(
StringView(name.c_str(), name.size()));
if (counterBinding >= 0) {
kind = "atomic_uint";
limitName = "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxAtomicCounterBufferBindings;
binding = counterBinding;
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqUniform &&
name.compare(MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != 0) {
kind = "uniform block";
limitName = "GL_MAX_UNIFORM_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxUniformBufferBindings;
binding = qualifier.layoutBinding;
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqBuffer) {
kind = "buffer block";
limitName = "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxShaderStorageBufferBindings;
binding = qualifier.layoutBinding;
}
}
if (kind == nullptr || limit <= 0 || binding < 0) return;
// The ARRAYED-INSTANCE rule: an array of N takes bindings base .. base + N - 1, and every
// one of them has to fit. getCumulativeArraySize() folds a multi-dimensional array into
// the count of leaf elements, which is exactly how many consecutive bindings GL hands out.
//
// isSizedArray() is MANDATORY, not defensive. glslang's TArraySizes::getCumulativeSize()
// asserts `sizes.getDimSize(d) != UnsizedArraySize` ("this only makes sense in paths that
// have a known array size"), so calling it on a run-time-sized array - the ordinary shape
// of a storage block's trailing member, and legal on the block instance itself - aborts
// the process inside mapIO's collect callback in any build with assertions live. The
// repo defines no NDEBUG of its own, so a CMake Debug build is exactly such a build; the
// "reports 0" behaviour the previous comment relied on is only what NDEBUG happens to do.
// An unsized array occupies one binding here, which is also what GL means by it.
long long elementCount = 1;
if (type.isArray() && type.isSizedArray()) {
const int cumulative = static_cast<int>(type.getCumulativeArraySize());
if (cumulative > 1) elementCount = cumulative;
}
const long long lastBinding = binding + elementCount - 1;
if (lastBinding < static_cast<long long>(limit)) return;
String message = "Error: layout(binding = " + std::to_string(binding) + ") on " + kind + " '" +
String(name.c_str()) + "'";
if (elementCount > 1) {
message += " (an array of " + std::to_string(elementCount) + ", occupying bindings " +
std::to_string(binding) + ".." + std::to_string(lastBinding) + ")";
}
message += " is not less than " + String(limitName) + " (" + std::to_string(limit) + ").";
*m_bindingViolation = Move(message);
}
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType(); const glslang::TType& type = ent.symbol->getType();
const glslang::TQualifier& qualifier = type.getQualifier(); const glslang::TQualifier& qualifier = type.getQualifier();
@@ -207,6 +304,8 @@ namespace MobileGL {
m_uniformBlocksWithoutBinding->insert(name.c_str()); m_uniformBlocksWithoutBinding->insert(name.c_str());
} }
CheckDeclaredBindingRange(type, name);
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink); TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
} }
@@ -21,27 +21,35 @@
#include <glslang/MachineIndependent/iomapper.h> #include <glslang/MachineIndependent/iomapper.h>
#include "TVarEntryInfo.h" #include "TVarEntryInfo.h"
#include "MG_Util/Types.h" #include "MG_Util/Types.h"
#include "MG_Util/ShaderTranspiler/Types.h"
namespace MobileGL { namespace MobileGL {
class TMglGlslIoResolver : public glslang::TDefaultGlslIoResolver { class TMglGlslIoResolver : public glslang::TDefaultGlslIoResolver {
public: public:
using ExplicitVarSlotMap = UnorderedMap<String, Uint>; using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
using ResourceBindingLimits = MG_Util::ShaderTranspiler::ResourceBindingLimits;
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns, TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices, const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
ExplicitVarSlotMap* opaqueUniformBindings, ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr, std::set<String>* storageBlocksWithoutBinding = nullptr,
std::set<String>* uniformBlocksWithoutBinding = nullptr) std::set<String>* uniformBlocksWithoutBinding = nullptr,
const ResourceBindingLimits* bindingLimits = nullptr,
String* bindingViolation = nullptr)
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts), : TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings), m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding), m_storageBlocksWithoutBinding(storageBlocksWithoutBinding),
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding) {} m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding), m_bindingLimits(bindingLimits),
m_bindingViolation(bindingViolation) {}
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage, TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings, const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr, std::set<String>* storageBlocksWithoutBinding = nullptr,
std::set<String>* uniformBlocksWithoutBinding = nullptr) std::set<String>* uniformBlocksWithoutBinding = nullptr,
const ResourceBindingLimits* bindingLimits = nullptr,
String* bindingViolation = nullptr)
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices, : TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding) {} opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding,
bindingLimits, bindingViolation) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override; int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
@@ -72,6 +80,10 @@ namespace MobileGL {
// resource kind on set 0), so an unbound block declared after an unbound image lands on // resource kind on set 0), so an unbound block declared after an unbound image lands on
// 1. See ProgramLinkTask's UBO reflection loop for what is done with them. // 1. See ProgramLinkTask's UBO reflection loop for what is done with them.
std::set<String>* m_uniformBlocksWithoutBinding = nullptr; std::set<String>* m_uniformBlocksWithoutBinding = nullptr;
// The binding-range rule, IN and OUT. See RecordBindingRangeViolation.
const ResourceBindingLimits* m_bindingLimits = nullptr;
String* m_bindingViolation = nullptr;
void CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name);
std::map<glslang::TString, int> m_plainUniformLocationSizeByName; std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
std::map<glslang::TString, int> m_plainUniformLocationByName; std::map<glslang::TString, int> m_plainUniformLocationByName;
bool m_plainUniformLocationsAssigned = false; bool m_plainUniformLocationsAssigned = false;

Some files were not shown because too many files have changed in this diff Show More