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
// hand glslang a workable gl_MaxClipDistances.
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;
// 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
@@ -307,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
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,
GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0;
@@ -717,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat));
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
} else {
@@ -731,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
}
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
@@ -1465,6 +1482,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
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;
// 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
+402 -33
View File
@@ -351,9 +351,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#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.
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) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
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;
// 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
@@ -2461,9 +2495,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
// `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
// 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() !=
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) {
(twin->GetPassthroughTessControlPatchVertices() !=
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
twin->SyncToBackend(currentProgram);
}
g_currentDrawFrontendProgram = currentProgram.get();
@@ -3524,6 +3583,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
GLsizei drawcount, GLsizei stride, const char* 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();
if (useNative) {
// 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
// the reason (a fallback would silently drop restarts and corrupt geometry).
void CheckPrimitiveRestartSupported(GLenum indexType) {
// ---------------------------------------------------------------------------
// Arbitrary-index primitive restart
//
// 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) ||
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;
}
Uint32 fixedMax = 0;
switch (indexType) {
case GL_UNSIGNED_BYTE: fixedMax = 0xFFu; break;
case GL_UNSIGNED_SHORT: fixedMax = 0xFFFFu; break;
case GL_UNSIGNED_INT: fixedMax = 0xFFFFFFFFu; break;
default: return;
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
const auto& indexBuffer = BoundElementArrayBuffer();
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.
}
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
if (restartIndex != fixedMax) {
THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + std::to_string(restartIndex) +
") is not supported by the GLES backend, which only restarts on the fixed index value (" +
std::to_string(fixedMax) +
") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX or set glPrimitiveRestartIndex "
"to that value.");
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);
}
// Widen only when the source really does use the all-ones value as a vertex index -
// otherwise the sentinel is free and the copy stays the caller's width, which keeps the
// common substitution allocation-for-allocation identical to the narrow form.
GLenum destinationType = indexType;
SizeT destinationIndexSize = sourceIndexSize;
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) {
@@ -3860,9 +4210,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
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
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex);
});
SetCurrentBaseVertex(0);
}
@@ -4158,9 +4510,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* indices, GLint basevertex) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex);
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);
}
@@ -4168,8 +4523,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
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;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseInstance(baseinstance);
SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(),
restart.Indices(), instancecount,
basevertex, baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(),
instancecount, basevertex);
}
});
SetCurrentBaseVertex(0);
@@ -4209,9 +4570,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei instancecount, GLint basevertex) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseVertex(basevertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount,
basevertex);
});
SetCurrentBaseVertex(0);
}
@@ -4221,13 +4585,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
SetCurrentBaseInstance(baseinstance);
ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
baseinstance);
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(),
instancecount, baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
}
});
SetCurrentBaseInstance(0);
@@ -4236,8 +4602,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
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();
XfbImpl::OnBackendContextDestroyed();
MultiDrawImpl::OnBackendContextDestroyed();
OnRestartSubstitutionContextDestroyed();
ScratchFBOImpl::OnBackendContextDestroyed();
ReleasePackedWordScratchTexture();
FramebufferImpl::InvalidateFramebufferBindingCache();
+83 -10
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
// 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 &&
m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
(m_cacheBorderColor != stateTextureObject->GetBorderColor() ||
m_cacheBorderColorI != stateTextureObject->GetBorderColorI() ||
m_cacheBorderColorUI != stateTextureObject->GetBorderColorUI() ||
m_cacheBorderColorForm != borderColorForm)) {
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();
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(),
borderColor.w()};
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
m_cacheBorderColor = borderColor;
}
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) {
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()
: 3u;
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 ||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
@@ -6770,8 +6812,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const String outMembers =
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
const String source =
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers);
const String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
inMembers, outMembers,
m_passthroughTessControlOuterLevel,
m_passthroughTessControlInnerLevel);
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
if (backendShaderId == 0) {
@@ -6861,8 +6905,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// Re-established by AttachPassthroughTessControlStage below when this program needs
// 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_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
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
@@ -7901,15 +7948,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor) {
// Same gate as the texture-side border colour above.
if (g_GLESCapabilities.SupportsTextureBorderClamp && g_GLESFuncs.glSamplerParameterfv) {
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor ||
m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI ||
m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI ||
m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) {
// Same gate as the texture-side border colour above, and the same reason for
// branching on the form: an integer border colour must reach the driver through
// 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);
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
}
}
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
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
// the program; every GL draw entry point calls it exactly once before issuing draws.
void PrepareForDraw(DrawSyncFlags syncBits);
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
void CheckPrimitiveRestartSupported(GLenum indexType);
// What an indexed draw has to do about primitive restart before it can be issued.
//
// 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
// uniforms. All are no-ops when the program does not read the corresponding builtin.
void SetCurrentBaseInstance(Uint32 baseInstance);
@@ -961,7 +1050,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint16 m_syncedShapeParamsVersion = 0;
SamplerParameters m_cacheSamplerParameters;
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};
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,
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
// 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 {
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; }
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
// with. See GetPassthroughTessControlPatchVertices.
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_backendProgramUsable = false;
// 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
// primitive restart is in play. CheckPrimitiveRestartSupported has already
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
// restart always restarts here and nowhere else.
// The index value this batch restarts on, compared at 32 bits against the zero-extended
// source index. Normally the all-ones value of the source type, which is what
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
// 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) {
switch (type) {
case GL_UNSIGNED_BYTE: return 0xFFu;
case GL_UNSIGNED_SHORT: return 0xFFFFu;
default: return 0xFFFFFFFFu;
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
return MG_State::pGLContext->GetPrimitiveRestartIndex();
}
return MG_Util::FixedRestartIndexForGLType(type);
}
Bool RestartActive() {
@@ -275,10 +280,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer) {
Bool hasIndexBuffer, Bool arbitraryRestart) {
ResolveTierOnce();
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
// 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
@@ -488,6 +503,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Bool restartActive = RestartActive();
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);
SizeT cursor = 0;
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,
GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return;
// State-independent and possibly throwing, so it runs before any GL work.
CheckPrimitiveRestartSupported(type);
// Read before any GL work, because it decides the tier below: a desktop restart index
// 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;
@@ -889,7 +920,8 @@ void main() {
// the tier choice and the per-sub-draw feeds use those, not the guess above.
const Bool feedDrawID = CurrentProgramReadsDrawID();
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
const GLESMultiDrawMode tier =
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
Bool drawn = false;
switch (tier) {
@@ -921,8 +953,10 @@ void main() {
// 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
// rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive.
if (!drawn) {
// entry points can receive - except that the base-vertex replay hands the
// 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);
}
if (!drawn) {
+12 -7
View File
@@ -26,6 +26,7 @@
#include <cmath>
#include <cctype>
#include <cstring>
#include <format>
#include <regex>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -836,7 +837,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers) {
const String& outPerVertexMembers,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -866,12 +869,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
// 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_TessLevelOuter[0] = 1.0;\n";
source += " gl_TessLevelOuter[1] = 1.0;\n";
source += " gl_TessLevelOuter[2] = 1.0;\n";
source += " gl_TessLevelOuter[3] = 1.0;\n";
source += " gl_TessLevelInner[0] = 1.0;\n";
source += " gl_TessLevelInner[1] = 1.0;\n";
for (Uint i = 0; i < 4; ++i) {
source += " gl_TessLevelOuter[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
}
for (Uint i = 0; i < 2; ++i) {
source += " gl_TessLevelInner[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
}
source += "}\n";
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
// 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
// default and glPatchParameterfv - their only setter - is a stub in this frontend
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
// the levels a parameter here AND part of what makes a built program stale, exactly as
// PATCH_VERTICES already is; the two must move together, so they are named together.
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
//
// The same stage, for the same reason, that DirectVulkan synthesizes in
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
@@ -382,7 +382,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// VkShaderModule against a driver shader object.
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
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
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
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.
m_dynamicParameters.MaxClipDistances =
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;
// 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
@@ -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.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
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.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
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.passthroughTessControlKey,
sizeof(payload.passthroughTessControlKey)));
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.cullMode, sizeof(payload.cullMode)));
@@ -435,6 +439,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
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};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -37,11 +37,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1;
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;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
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
// 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
@@ -87,8 +100,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
// of which ComputeHash already mixes in.
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
// 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{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// 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/Types.h"
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <format>
#include <map>
#include <utility>
#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
// 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
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Those two levels default to 1.0 and are baked here as literals because
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
// making the levels a parameter of this source AND of the cache key in
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
// together here.
// Those two levels are baked in as literals - Vulkan has no equivalent dynamic state, so
// compiling them in is the only way to honour glPatchParameterfv. That makes them part of
// this module's identity: GetOrCreatePassthroughTessControlStage keys its cache on them,
// and PipelineFactory hashes them into the pipeline key. The three must move together.
//
// 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
@@ -3619,60 +3740,91 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// this from having to know the domain.
String source = "#version 450 core\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
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
// matches built-in interface blocks by their whole shape, and the two obvious spellings
// are both wrong:
// gl_in and gl_out are redeclared to the exact gl_PerVertex the NEIGHBOURING EVALUATION
// STAGE carries, because Vulkan matches built-in interface blocks by their whole shape,
// and the two obvious spellings are both wrong:
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
// zeroes (degenerate triangles, nothing rasterized), and
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
// vertex+evaluation program through this same compiler and fails if the two shapes ever
// stop agreeing, rather than letting the mismatch show up as a black frame.
// * taking glslang's DEFAULT block for a standalone control stage yields whatever THIS
// source's #version implies, which is unrelated to the evaluation stage's.
//
// The member set is a PARAMETER rather than a constant, and that is the whole point: it
// 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:
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
// 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
// this trades for not making every tessellated pipeline depend on an optional feature.
source += "in gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\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";
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
source += "void main() {\n";
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n";
source += " gl_TessLevelOuter[1] = 1.0;\n";
source += " gl_TessLevelOuter[2] = 1.0;\n";
source += " gl_TessLevelOuter[3] = 1.0;\n";
source += " gl_TessLevelInner[0] = 1.0;\n";
source += " gl_TessLevelInner[1] = 1.0;\n";
for (Uint32 i = 0; i < 4; ++i) {
source += " gl_TessLevelOuter[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
}
for (Uint32 i = 0; i < 2; ++i) {
source += " gl_TessLevelInner[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
}
source += "}\n";
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
// 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()) {
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};
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
stage.module = VK_NULL_HANDLE;
stage.pName = "main";
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
// 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).
@@ -3686,7 +3838,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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, compiled.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
m_passthroughTessControlStages.emplace(key, stage);
return stage;
}
@@ -3696,7 +3848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!linked) {
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
m_passthroughTessControlStages.emplace(key, stage);
return stage;
}
@@ -3705,7 +3857,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!binary || binary.value().empty() || binary.value().front().empty()) {
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
"for patchVertices=%u", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
m_passthroughTessControlStages.emplace(key, stage);
return stage;
}
@@ -3727,14 +3879,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (result != VK_SUCCESS) {
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
m_passthroughTessControlStages.emplace(key, stage);
return stage;
}
stage.module = module;
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);
m_passthroughTessControlStages.emplace(patchVertices, stage);
m_passthroughTessControlStages.emplace(key, stage);
return stage;
}
@@ -3744,6 +3896,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const {
entry.needsPassthroughTessControl = false;
entry.passthroughTessControlEmulatable = false;
entry.passthroughPerVertexMembers = 0;
Bool hasTessEval = false;
Bool hasTessControl = false;
@@ -3763,6 +3916,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
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{};
const SpvReflectResult createResult =
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using CompileOptionFlags = Flags<CompileOptionBit>;
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 {
Bool enabled = false;
Uint32 maxPerStageSamplers = 0;
@@ -194,6 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed.
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
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -249,6 +275,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -267,6 +294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -311,6 +339,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -329,6 +358,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0;
return *this;
}
@@ -485,18 +515,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the caller then has no control stage to inject, and CreatePipeline refuses the
// 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,
// which is draw state, not link state - the CTS case that motivated this links at the
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
// so the module a pipeline was built with is part of that pipeline's identity.
// Compiling is bounded by the number of distinct patch sizes a program draws with
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
// happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
// member set, because all three decide what the generator emits. The size comes from
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
// - draw state rather than link state, and the CTS case that motivated this links at the
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
// own SPIR-V, so two programs at different GLSL versions need different modules. The
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
// 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
// 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:
struct ProgramLookupCache {
@@ -556,11 +608,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by input patch size. Never evicted: at most
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
// ever built from one keeps referencing its module. A failed build is cached as
// Pass-through tessellation control stages by the identity of what was compiled into
// them - the input patch size and the six default tessellation levels, folded into one
// 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.
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();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,10 @@
#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_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
static Uint32 ResolveAttachmentLayerCount(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
}
return 1u;
}
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
// not a cosmetic difference: the count below is not key-only, it is written straight into
// 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
// 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(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
@@ -86,34 +86,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ToStorageArrayLayer(texture, face);
}
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a
// 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;
}
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
// 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(
const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) {
if (attachment.IsLayered()) {
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
// the image's own view type is. The cube-face upload targets always meant this; a cube map
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
// 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
// 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()) ||
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 resource.viewType;
@@ -334,47 +355,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const auto internalFormat = renderbuffer->GetInternalFormat();
// Three-channel color formats widen to their RGBA twin exactly like textures do
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
// renderbuffer and a texture of the same GL format then see one VkFormat.
const VkFormat format = [&]() -> VkFormat {
switch (internalFormat) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8:
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);
}
}();
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
//
// This used to be a hand-maintained second copy of that table, and it was missing exactly
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
// a size-incompatible vkCmdCopyImage.
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// 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),
@@ -772,7 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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,
Bool drawUsesDepthStencil) {
// 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);
if (activeIt != m_renderPasses.end()) {
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_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
return &activeIt->second;
}
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter;
return it->second;
return &it->second;
}
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -1011,8 +1003,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
if (attachmentViews.back() == VK_NULL_HANDLE) {
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;
continue;
@@ -1100,8 +1096,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
} else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
MOBILEGL_ASSERT(textureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
if (textureResource == nullptr) {
// 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);
desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format,
@@ -1122,8 +1125,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
if (attachmentViews.back() == VK_NULL_HANDLE) {
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;
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
@@ -1216,8 +1226,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture();
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(depthTextureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
if (depthTextureResource == nullptr) {
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;
depthAttachmentDescription.format = depthTextureResource->format;
depthAttachmentSampleCount = depthTextureResource->sampleCount;
@@ -1229,8 +1243,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else {
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
MOBILEGL_ASSERT(depthRenderbufferResource,
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
if (depthRenderbufferResource == nullptr) {
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;
depthAttachmentDescription.format = depthRenderbufferResource->format;
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
@@ -1304,8 +1322,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
if (attachmentViews.back() == VK_NULL_HANDLE) {
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) {
width = attachmentExtent.x();
height = attachmentExtent.y();
@@ -1327,6 +1351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
});
textureResources.emplace_back(nullptr);
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) {
width = attachmentExtent.x();
height = attachmentExtent.y();
@@ -1424,8 +1454,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.dependencyCount = 2;
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;
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
VkFramebufferCreateInfo framebufferCreateInfo;
@@ -1438,8 +1485,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
framebufferCreateInfo.width = width;
framebufferCreateInfo.height = height;
framebufferCreateInfo.layers = framebufferLayers;
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
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};
RenderPassEntry renderPassEntry {
hash,
@@ -1464,7 +1524,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second;
return &insertedIt->second;
}
void VkRenderPassManager::OnPresent() {
@@ -243,7 +243,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// draw against a depth-less active pass resolves to a new (incompatible)
// 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).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
//
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
// 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,
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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) {
switch (format) {
case TextureInternalFormat::DepthComponent:
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
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,
"VkSamplerManager::Initialize failed: invalid initialization info");
return true;
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE;
m_config = nullptr;
m_frameBoundaryCounter = 0;
m_customBorderColorSupported = false;
m_maxCustomBorderColorSamplers = 0;
m_customBorderColorSamplerCount = 0;
}
void VkSamplerManager::OnFrameBoundary() {
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
it = m_samplers.erase(it);
} else {
++it;
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const {
Bool forceNearestFiltering, Bool singleLevelView,
const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
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)));
const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
// 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);
}
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
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);
if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.borderColor = borderColor.color;
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;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.usesCustomBorderColor = borderColor.isCustom;
if (entry.usesCustomBorderColor) {
++m_customBorderColorSamplerCount;
}
m_samplers[key] = entry;
return vkSampler;
}
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
ResolvedBorderColor resolved{};
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
// with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
const auto format = 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)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
}
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) &&
NearlyEqual(borderColor.z(), 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)) {
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) &&
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
@@ -28,6 +28,13 @@ public:
Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
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);
@@ -52,6 +59,21 @@ public:
// boundaries.
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:
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
@@ -60,17 +82,18 @@ private:
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0;
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
Bool usesCustomBorderColor = false;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const;
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
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 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
@@ -82,6 +105,12 @@ private:
const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
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;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
@@ -46,13 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 {
VkImageType imageType = VK_IMAGE_TYPE_2D;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
@@ -380,7 +373,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
@@ -921,18 +914,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
// SyncTextureResource asks for and may have had refused per format.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
// 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);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
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, "
"2D-array-compatible=%d)",
// Not an error line: the render-pass builder turns the null view into one
// 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(),
mipLevel, sliceCount,
static_cast<Int>(viewType), mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE;
}
@@ -2173,12 +2176,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
// Losing 2D-array compatibility only costs framebuffer attachment of this format's
// 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
// 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 "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
"textureId=%d; creating without it (per-slice and layered framebuffer "
"attachment of 3D textures in this format will be unavailable)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
m_2dArrayCompatibleUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
@@ -10,8 +10,10 @@
#include "../VkIncludes.h"
#include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
@@ -22,6 +24,31 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan {
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
// 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
@@ -41,6 +68,37 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
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
// 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
@@ -19,6 +19,7 @@
#include "MG_State/GLState/SamplerState/SamplerObject.h"
#include "MG_State/GLState/TextureState/TextureObject.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Impl/GLImpl/Texture/GL_Texture.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
// 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.
@@ -30,6 +31,7 @@
#include "MG_Util/Texture/PixelStoreProcessor.h"
#include <Config.h>
#include <algorithm>
#include <bit>
#include <cstdlib>
#include <cstring>
#include <vulkan/utility/vk_format_utils.h>
@@ -1417,6 +1419,48 @@ void main() {
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,
Uint32 baseMipLevel) {
const Uint32 existingMipLevelCount = static_cast<Uint32>(texture.GetMipmapLevelCount());
@@ -1429,6 +1473,8 @@ void main() {
return false;
}
const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget());
for (const auto uploadTarget : uploadTargets) {
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel);
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel);
@@ -1445,13 +1491,15 @@ void main() {
}
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize);
const Uint32 requiredMipLevelCount =
baseMipLevel + ComputeFullMipLevelCountWithFixedComponents(baseTexelSize, shrinkingComponents);
if (existingMipLevelCount >= requiredMipLevelCount) {
continue;
}
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()) *
static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z());
@@ -3126,7 +3174,9 @@ void main() {
m_samplerManager = MakeUnique<VkSamplerManager>();
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
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.");
succeeded = InitializeBlitResources();
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
// 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
// a restart, so it is nudged to the next-lowest value: it can only be a real index (the
// application's restart index is a different number), and the vertex it selects is
// outside any well-defined draw anyway, whereas leaving it alone would tear the
// primitive in two.
// a restart, so it is nudged to the next-lowest value, which silently draws the wrong
// vertex. That is a real (if narrow) loss and it is reported once rather than left
// invisible; DirectGLES avoids it for 8- and 16-bit indices by widening the copy instead,
// 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,
Uint32 applicationRestartIndex, Vector<Uint8>& output) {
output.resize(sizeBytes);
@@ -3932,6 +3987,12 @@ void main() {
if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) {
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;
}
}
@@ -3984,14 +4045,13 @@ void main() {
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) {
const Uint32 restartIndex = rsp.PrimitiveRestartIndex;
Uint32 fixedMax = 0;
switch (vkIndexType) {
case VK_INDEX_TYPE_UINT8: fixedMax = 0xFFu; break;
case VK_INDEX_TYPE_UINT16: fixedMax = 0xFFFFu; break;
case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break;
default: break;
}
substituteRestart = restartIndex != fixedMax;
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
// STRICTLY less, and never truncated. Equal needs no rewrite (the driver already
// restarts there); GREATER means the index type cannot hold the application's restart
// index, so GL 4.6 core 10.3.6 says nothing matches it and the draw restarts nowhere -
// which is exactly what ResolvePrimitiveRestartEnable told the pipeline, so rewriting
// here would put restarts into a stream the pipeline was built not to restart on.
substituteRestart = restartIndex < fixedMax;
substituteRestartIndex = restartIndex;
}
@@ -4707,8 +4767,9 @@ void main() {
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op
// PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask
// - 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)
// - 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
@@ -4733,8 +4794,31 @@ void main() {
capabilityBits |= p.PrimitiveRestartEnabled ? 1ull << 6 : 0;
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
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));
// 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.CullFaceModeSetting));
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.DepthFunc));
@@ -4779,13 +4863,42 @@ void main() {
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(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry) {
const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable) {
Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip;
if (programObj.stages.empty()) {
MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages");
@@ -4827,6 +4940,7 @@ void main() {
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
entry.renderPassHash == renderPassHash &&
entry.pipelineStateHash == pipelineStateHash &&
entry.primitiveRestartEnable == primitiveRestartEnable &&
entry.transformFlags == transformFlags) {
return entry.pipeline;
}
@@ -5025,22 +5139,50 @@ void main() {
: VK_POLYGON_MODE_FILL;
const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode);
const Bool primitiveRestartEnabled =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
// Resolved by the caller (ResolvePrimitiveRestartEnable), which knows whether the draw is
// indexed and with what index type; the capability bits alone answer neither.
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;
// strip/fan restart works without it. Silently dropping restarts would corrupt geometry, so
// hard-fail here (at the draw) with the reason when the device lacks the feature.
// strip/fan restart works without it. There is no fallback - silently dropping the restarts
// 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) {
return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST ||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST ||
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)) {
THROW_EXCEPTION("Primitive restart on a list topology requires the primitiveTopologyListRestart device "
"feature (VK_EXT_primitive_topology_list_restart), which this device does not support; use "
"a strip/fan topology or a device that supports it.");
MGLOG_E_ONCE("Draw skipped: primitive restart on a list topology (0x%x) requires the "
"primitiveTopologyListRestart device feature (VK_EXT_primitive_topology_list_restart), "
"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 {
@@ -5050,6 +5192,13 @@ void main() {
.renderPass = renderPassEntry.renderPass,
.colorAttachmentCount = renderPassEntry.colorAttachmentCount,
.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,
.topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled,
@@ -5106,10 +5255,21 @@ void main() {
// 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
// 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 &&
vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
payload.passthroughTessControlStage =
m_programFactory->GetOrCreatePassthroughTessControlStage(payload.patchControlPoints);
const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();
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) {
payload.frontStencilFailOp = VK_STENCIL_OP_KEEP;
@@ -5309,8 +5469,21 @@ void main() {
colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format;
} else if (colorAttachmentRenderbuffer != nullptr) {
textureExternalIndex = static_cast<Int>(colorAttachmentRenderbuffer->GetExternalIndex());
colorAttachmentFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
colorAttachmentRenderbuffer->GetInternalFormat());
// The SAME resolver GetOrCreateRenderbufferResource backs the image with, so the
// 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 {
auto* texture = colorAttachmentTexture;
MOBILEGL_ASSERT(texture != nullptr,
@@ -5391,6 +5564,7 @@ void main() {
entry.vertexInputHash = vertexLayoutHash;
entry.renderPassHash = renderPassHash;
entry.pipelineStateHash = pipelineStateHash;
entry.primitiveRestartEnable = primitiveRestartEnable;
entry.transformFlags = transformFlags;
entry.pipeline = pipeline;
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
@@ -5775,7 +5949,11 @@ void main() {
return false;
}
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;
}
if (m_clearManager->HasAnyPendingClears()) {
@@ -6045,6 +6223,7 @@ void main() {
entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash &&
entry.renderPassHash == snap.renderPassHash &&
entry.pipelineStateHash == m_pipelineStateHash &&
entry.primitiveRestartEnable == drawPrimitiveRestartEnable &&
entry.transformFlags == memoTransformFlags) {
pipeline = entry.pipeline;
break;
@@ -6055,14 +6234,17 @@ void main() {
// index, depth/stencil participation, image epochs, no pending clears)
// was verified unchanged above, so this is a pure cache hit on the same
// 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);
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;
}
pipeline = GetOrCreatePipeline(mode, program, programObj,
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
vao, renderPassEntry);
vao, *renderPassEntry, drawPrimitiveRestartEnable);
if (pipeline == VK_NULL_HANDLE) {
return false;
}
@@ -6447,12 +6629,23 @@ void main() {
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
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)) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
activeRenderPass = nullptr;
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) {
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
// 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
@@ -6562,6 +6756,7 @@ void main() {
if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) {
snap.valid = true;
snap.aspects = aspects.GetRaw();
snap.primitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView);
snap.mode = mode;
snap.programLifetimeId = program.GetLifetimeId();
snap.programVersion = program.GetBackendStateVersion();
@@ -6796,8 +6991,10 @@ void main() {
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
if (renderPassEntry->attachmentCount == 0 ||
auto* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
// 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) {
return ScissoredClearPrep::NoOp;
}
@@ -6827,7 +7024,10 @@ void main() {
activeRenderPass = nullptr;
// Re-resolve: ending the pass updates tracked attachment layouts, which feed the
// 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
// 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
// it opens skip the depth attachment (depth-less flavor).
auto& renderPassEntry =
auto* renderPassEntryPtr =
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);
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
@@ -8944,6 +9150,12 @@ void main() {
VkExtent2D extent = {0, 0};
Uint32 depth = 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,
@@ -9063,6 +9275,7 @@ void main() {
out.extent = resource->extent;
out.depth = 1;
out.arrayLayers = 1;
out.format = resource->format;
return out.image != VK_NULL_HANDLE;
}
// 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.depth = resource->depth;
out.arrayLayers = resource->arrayLayers;
out.format = resource->format;
return true;
};
CopyImageVkImage srcImage{};
@@ -9118,6 +9332,42 @@ void main() {
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
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 =
srcImage.aspect & dstImage.aspect &
(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);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: failed to sync textureId=%u",
textureObject->GetExternalIndex());
// Two shapes end up in the same place, and for the same reason: the GL level being read has
// no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
// 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;
}
@@ -10351,15 +10630,23 @@ void main() {
void VulkanRenderer::GenerateMipmap(GLenum target) {
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// The other mipmappable targets - 1D, 1D array, cube map array - are legal GL and the front
// end lets them through, so reaching one here is a coverage gap in this backend, not a
// broken invariant. Declining leaves the mip chain unwritten; asserting took the process
// down with it.
// Whatever is left here is a coverage gap in this backend, not a broken invariant, so it
// declines (leaving the mip chain unwritten) rather than asserting the process down. What
// remains is the multisample targets, which GL 4.6 core 8.14.4 forbids to glGenerateMipmap
// 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 &&
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
// 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());
return;
}
@@ -12709,6 +12996,11 @@ void main() {
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
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
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
// 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
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
// 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.
Bool m_dualSrcBlendFeatureEnabled = 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
// (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
@@ -733,6 +749,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// values the memo already holds.
Uint64 pipelineStateHash = 0;
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;
};
static constexpr Uint32 kPipelineMemoSize = 8;
@@ -865,6 +887,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 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;
Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0;
@@ -1168,13 +1196,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CreateSwapchain();
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(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry);
const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines();
// 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;
}
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
// Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
// 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
@@ -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 {
// 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
@@ -751,6 +855,27 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
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) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
}
@@ -851,7 +976,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
// they produce for a call made with no program bound. Same precedence decision, and
// the same reason, as DispatchComputeIndirect above.
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
if (!multiDrawElementsIndirectCount) {
MG_State::pGLContext->RecordError(
@@ -872,7 +997,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// See MultiDrawElementsIndirectCount, including why this one goes last.
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
if (!multiDrawArraysIndirectCount) {
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,
const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) 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) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
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,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
baseinstance);
@@ -914,7 +1039,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) 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,
GLsizei instancecount, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) 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,
GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount);
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
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) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
@@ -981,7 +1106,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, 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) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) {
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,
GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
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,
GLsizei drawcount, const GLint* basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) 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) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices);
@@ -1459,7 +1584,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// (GL 4.6 core 10.3.7).
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(functionName)) return;
if (!PrepareCurrentProgramForDraw(functionName)) return;
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
if (instancecount < 0) {
MG_State::pGLContext->RecordError(
@@ -1482,8 +1607,13 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(id) + " is not a transform feedback object name."));
return;
}
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
if (stream != 0) {
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
// 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(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -1501,6 +1631,7 @@ namespace MobileGL::MG_Impl::GLImpl {
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);
if (vertices == 0) return;
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 DispatchComputeIndirect(GLintptr indirect);
void PatchParameteri(GLenum pname, GLint value);
void PatchParameterfv(GLenum pname, const GLfloat* values);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void TextureBarrier();
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 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, 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_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, 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)
@@ -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, 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, 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, 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)
@@ -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, 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, 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, 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)
@@ -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, 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_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, 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)
@@ -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, 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, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_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, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
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, 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(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)
@@ -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, 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, 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, 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)
@@ -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, 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, 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_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)
@@ -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, 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, 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, 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)
@@ -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,
GLuint texture, GLint level,
TextureUploadTarget textureUploadTarget, Bool layered = false) {
@@ -482,10 +551,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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,
textureUploadTarget);
textureUploadTarget, layered);
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
textureUploadTarget);
textureUploadTarget, layered);
return;
}
@@ -497,13 +580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
if (texture == 0) {
framebufferObject->Detach(attachmentType);
@@ -518,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
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_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
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
// count it actually hands the driver.
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
// integer path is genuinely one sample, and accepting four only moved the failure from an
// 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) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA;
@@ -644,13 +739,24 @@ namespace MobileGL::MG_Impl::GLImpl {
&normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_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) {
return GetMaxRenderbufferSamples_State();
}
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
// clamps to it, while the frontend honours what it advertised.
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
return GetAdvertisedIntegerMaxSamples();
}
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -682,8 +788,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
// glGetInternalformativ is backed; integer formats are handled below.
// Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
// 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);
if (samples > maxSamples) {
// 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& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
if (texture == 0) {
framebufferObject->Detach(attachmentType);
@@ -1069,6 +1171,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
if (layer < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -1191,6 +1294,13 @@ namespace MobileGL::MG_Impl::GLImpl {
"Framebuffer target is bound to no framebuffer object."));
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) {
framebufferObject->Detach(attachmentType);
@@ -1205,6 +1315,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -1241,6 +1352,12 @@ namespace MobileGL::MG_Impl::GLImpl {
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);
if (!textureObject) {
MG_State::pGLContext->RecordError(
@@ -1291,13 +1408,10 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (level < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State",
"Texture level must be non-negative."));
return;
}
// The whole level condition, not just its negative half: glNamedFramebufferTexture and
// glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
// be rejected on both or a CTS case gets two answers for one rule.
if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
Bool layered = false;
+390 -46
View File
@@ -7,7 +7,9 @@
// End of Source File Header
#include "GL_Getter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <Config.h>
#include <MGGitHash.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.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
// The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
// 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 kFrontendMaxVertexOutputComponents = 64;
constexpr GLint kFrontendMaxFragmentInputComponents = 128;
@@ -106,21 +115,61 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks +
kFrontendMaxGeometryUniformBlocks +
kFrontendMaxFragmentUniformBlocks;
constexpr GLint kFrontendMaxVaryingComponents = 64;
constexpr GLint kFrontendMaxVaryingVectors = 8;
// ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
// gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
// answer only; 32 is the GL 4.6 core minimum (table 23.57).
constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
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 kFrontendMinProgramTexelOffset = -8;
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 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 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 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
// 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;
}
// 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 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) {
@@ -304,24 +363,6 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
@@ -473,10 +514,18 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace
// 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
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
// 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.
// before it is advertised. gl_MaxSamples expands from the same floored number
// (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
//
// 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() {
if (MG_Backend::pActiveBackendObject == nullptr) {
return kFrontendMaxSamples;
@@ -484,6 +533,50 @@ namespace MobileGL::MG_Impl::GLImpl {
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@ */
const GLubyte* GetString(GLenum name) {
static String vendorString;
@@ -680,12 +773,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
case GL_MIN_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;
GetFloatv(pname, &value);
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
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:
break;
}
@@ -735,6 +846,22 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = depthRange.y();
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: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.ViewportBoundsRangeMin;
@@ -800,6 +927,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POLYGON_OFFSET_UNITS:
params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
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: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.SmoothLineWidthRangeMin;
@@ -815,6 +947,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SAMPLE_COVERAGE_VALUE:
params[0] = MG_State::pGLContext->GetSampleCoverageValue();
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:
// Float state: read it directly so the fractional part is not lost to the integer path.
params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
@@ -1186,6 +1323,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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:
if (MG_Backend::pActiveBackendObject) {
params[0] = static_cast<GLint64>(
@@ -1222,12 +1366,17 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint ints[4] = {};
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) {
case GL_BLEND_COLOR:
case GL_COLOR_CLEAR_VALUE:
case GL_COLOR_WRITEMASK:
case GL_SCISSOR_BOX:
case GL_VIEWPORT:
case GL_PATCH_DEFAULT_OUTER_LEVEL:
for (int i = 0; i < 4; ++i) {
params[i] = static_cast<GLint64>(ints[i]);
}
@@ -1237,6 +1386,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_VIEWPORT_DIMS:
case GL_POINT_SIZE_RANGE:
case GL_VIEWPORT_BOUNDS_RANGE:
case GL_PATCH_DEFAULT_INNER_LEVEL:
params[0] = static_cast<GLint64>(ints[0]);
params[1] = static_cast<GLint64>(ints[1]);
return;
@@ -1268,6 +1418,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POINT_SIZE_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
case GL_MAX_VIEWPORT_DIMS:
case GL_PATCH_DEFAULT_INNER_LEVEL:
count = 2;
break;
case GL_BLEND_COLOR:
@@ -1275,6 +1426,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VIEWPORT:
case GL_SCISSOR_BOX:
case GL_COLOR_WRITEMASK:
case GL_PATCH_DEFAULT_OUTER_LEVEL:
count = 4;
break;
default:
@@ -1314,6 +1466,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0;
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:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
return;
@@ -1369,6 +1530,16 @@ namespace MobileGL::MG_Impl::GLImpl {
// this single case serves every getter flavor.
*params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
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: {
const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
params[0] = static_cast<GLint>(clearColor.x());
@@ -1657,6 +1828,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
*params = kFrontendMaxGeometryUniformComponents;
return;
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
*params = kFrontendMaxGeometryShaderInvocations;
return;
case GL_MAX_IMAGE_SAMPLES:
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
return;
@@ -1710,6 +1884,59 @@ namespace MobileGL::MG_Impl::GLImpl {
*params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
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:
*params = 15; // TODO
return;
@@ -1755,8 +1982,21 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_NUM_PROGRAM_BINARY_FORMATS:
*params = 0;
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:
*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;
case GL_PACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
@@ -1815,6 +2055,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PRIMITIVE_RESTART_INDEX:
*params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
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:
*params = 0; // program-binary entrypoints are stubbed
return;
@@ -1900,6 +2145,13 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SAMPLE_MASK:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
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:
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
return;
@@ -2118,7 +2370,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
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;
case GL_CONTEXT_PROFILE_MASK:
// Reports the requested context profile (EGL defaults 3.x contexts to core);
@@ -2174,8 +2431,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeTextureImageUnits;
break;
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,
dynamicParameters.MaxComputeUniformBlocks,
ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
@@ -2219,16 +2480,16 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedColorTextureMaxSamples();
break;
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
kFrontendMaxFragmentUniformBlocks,
ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
kFrontendMaxGeometryUniformBlocks,
ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
@@ -2242,14 +2503,14 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
kFrontendMaxVertexUniformBlocks,
ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
*params = dynamicParameters.MaxCubeMapTextureSize;
break;
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedDepthTextureMaxSamples();
break;
case GL_MAX_FRAMEBUFFER_WIDTH:
*params = dynamicParameters.MaxFramebufferWidth;
@@ -2276,7 +2537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeImageUniforms;
break;
case GL_MAX_INTEGER_SAMPLES:
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedIntegerMaxSamples();
break;
case GL_MAX_RENDERBUFFER_SIZE:
*params = dynamicParameters.MaxRenderbufferSize;
@@ -2287,12 +2548,56 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PATCH_VERTICES:
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
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:
*params = dynamicParameters.MaxPatchVertices;
break;
case GL_MAX_TESS_GEN_LEVEL:
*params = dynamicParameters.MaxTessGenLevel;
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:
*params = dynamicParameters.MinProgramTextureGatherOffset;
break;
@@ -2343,7 +2648,25 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
break;
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;
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
@@ -2360,15 +2683,36 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TEXTURE_SIZE:
*params = dynamicParameters.MaxTextureSize;
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
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// 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
// (36), so the clamp never under-advertises.
// advertised index and expects no error. The floor is the GL 4.5 core minimum, and
// 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,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break;
}
case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize;
break;
+19 -2
View File
@@ -25,7 +25,24 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum GetError();
GLenum GetGraphicsResetStatus();
// 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
// format, otherwise MobileGL rejects a sample count it advertised itself.
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
// ceilings below have a minimum of one and are reported as probed.
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
+351 -25
View File
@@ -11,6 +11,8 @@
#include "Config.h"
#include <cmath>
#include <limits>
#include <set>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -30,10 +32,22 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckShaderNameValidity(Uint 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(
ErrorCode::InvalidValue,
error,
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 true;
@@ -245,6 +259,30 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
if (MG_Backend::pActiveBackendObject) {
@@ -307,9 +345,195 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompileShader_State(GLuint shader) {
auto& shaderObject = TryToGetShaderObject(shader);
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();
}
// ---------------------------------------------------------------------------------------
// 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,
// 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.
@@ -744,12 +968,77 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
break;
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;
// 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_INPUT_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:
MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
@@ -811,8 +1100,19 @@ namespace MobileGL::MG_Impl::GLImpl {
}
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
break;
case GL_SHADER_SOURCE_LENGTH:
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
case GL_SHADER_SOURCE_LENGTH: {
// 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;
// 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
@@ -858,13 +1158,23 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return;
auto& src = shaderObject->GetShaderSource();
auto& src = shaderObject->GetApplicationShaderSource();
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
}
GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
auto& programObject = TryToGetProgramObject(program);
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);
MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
return loc;
@@ -1277,11 +1587,13 @@ namespace MobileGL::MG_Impl::GLImpl {
template <GLsizei ItemCount, typename T>
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
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()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1289,6 +1601,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
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++) {
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,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -1712,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
"program " + std::to_string(program));
}
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -1731,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
@@ -1756,8 +2072,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -1769,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
@@ -1790,8 +2106,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -1803,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
"program " + std::to_string(program));
}
@@ -1836,6 +2152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Program object" + std::to_string(program) + " that has been linked."));
return;
}
if (!ValidateUniformBlockBinding(uniformBlockBinding)) return;
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -2083,6 +2400,15 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
CompileShader_State(shader);
}
@@ -2342,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2352,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2378,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2404,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2430,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2456,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2482,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2508,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2534,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
}
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,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2560,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
}
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
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 CreateShader(GLenum type);
void DeleteProgram(GLuint program);
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Program {} has not been linked successfully.", program));
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;
+93 -12
View File
@@ -59,6 +59,37 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
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() {
return MG_Config::Features.DisableTimerQuery;
@@ -370,6 +401,9 @@ namespace MobileGL::MG_Impl::GLImpl {
}
queryObject->active = false;
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 ||
queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false;
@@ -410,7 +444,9 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
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
// need backend support.
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.");
return;
}
GLuint& activeQueryId = isTransformFeedbackQuery
GLuint& activeQueryId = isPipelineStatisticsQuery
? g_activePipelineStatisticsQueryIds[target]
: (isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on this target.");
@@ -448,7 +486,11 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
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);
// the CPU accounting delta stays as the fallback when the backend lacks them.
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_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
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.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
GLuint& activeQueryId = isTransformFeedbackQuery
GLuint& activeQueryId = isPipelineStatisticsQuery
? g_activePipelineStatisticsQueryIds[target]
: (isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return;
@@ -494,6 +540,17 @@ namespace MobileGL::MG_Impl::GLImpl {
activeQueryId = 0; // should not happen; keep state consistent
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 (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
@@ -657,7 +714,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
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;
}
return;
@@ -668,6 +730,14 @@ namespace MobileGL::MG_Impl::GLImpl {
// entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// 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 ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
@@ -741,14 +811,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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
// 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
// single vertex stream both bounds are 1, so a valid call is always index 0 and
// forwards to the unindexed implementation.
// for the two transform feedback targets and zero for every other target. MobileGL
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
// 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) {
const Bool perStreamTarget =
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
GLint maxVertexStreams = 1;
if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
@@ -761,6 +841,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: "index must be zero for this query target.");
return false;
}
} // namespace
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);
}
// 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) {
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) {
// 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
@@ -1013,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
SampleCoverage_State(value, invert);
}
void MinSampleShading(GLfloat value) {
MinSampleShading_State(value);
}
void PolygonOffset(GLfloat factor, GLfloat 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) {
PolygonMode_State(face, mode);
}
@@ -38,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert);
void MinSampleShading(GLfloat value);
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 PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param);
+131 -39
View File
@@ -13,6 +13,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Math/FixedPointConversion.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
if (param == nullptr) return false;
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
} // 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,
bool isUnsignedInteger) {
bool isUnsignedInteger, bool isIntegerCommand) {
if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isFloat) {
const auto* values = (const GLfloat*)param;
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) {
const auto* values = (const GLuint*)param;
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,
bool isUnsignedInteger) {
bool isUnsignedInteger, bool isIntegerCommand) {
if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
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) {
case GL_TEXTURE_WRAP_S:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
break;
case GL_TEXTURE_WRAP_T:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
break;
case GL_TEXTURE_WRAP_R:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
break;
case GL_TEXTURE_MIN_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
samplerObj->GetMipmapMode()));
break;
case GL_TEXTURE_MAG_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
SamplerMipmapMode::None));
break;
case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
break;
case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
break;
case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (isFloat) {
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
} else if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
} else {
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
break;
case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
break;
case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
break;
case GL_TEXTURE_BORDER_COLOR: {
if (isFloat) {
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
out[1] = color.y();
out[2] = color.z();
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) {
const auto& color = samplerObj->GetBorderColorUI();
auto* out = (GLuint*)params;
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr);
} else {
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
// name, which is why this cannot go through the shared SamplerImpl validator.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
// GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
// name, so the shared validator answers for all of them.
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler);
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
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) {
SetSamplerParam_State(sampler, pname, param, false, true);
SetSamplerParam_State(sampler, pname, param, false, true, true);
}
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) {
SetSamplerParam_State(sampler, pname, param, false, false);
SetSamplerParam_State(sampler, pname, param, false, false, false);
}
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) {
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);
}
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);
}
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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) {
GetSamplerParam_State(sampler, pname, params, false, false);
GetSamplerParam_State(sampler, pname, params, false, false, true);
}
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) {
@@ -12,10 +12,16 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
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) {
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
std::format("Invalid sampler name {}", sampler)));
return false;
}
+525 -88
View File
@@ -27,6 +27,8 @@
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Impl/GLImpl/Framebuffer/Validators.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>
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]));
}
// 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,
const GLint* params) {
constexpr Float kSignedIntToFloat = 1.0f / 2147483647.0f;
textureObject->SetBorderColor(FloatVec4(static_cast<Float>(params[0]) * kSignedIntToFloat,
static_cast<Float>(params[1]) * kSignedIntToFloat,
static_cast<Float>(params[2]) * kSignedIntToFloat,
static_cast<Float>(params[3]) * kSignedIntToFloat));
textureObject->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(params[0]),
MG_Util::SignedNormalizedInt32ToFloat(params[1]),
MG_Util::SignedNormalizedInt32ToFloat(params[2]),
MG_Util::SignedNormalizedInt32ToFloat(params[3])));
}
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);
case GL_TEXTURE_DEPTH_TYPE:
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:
MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname);
return 0;
@@ -398,10 +407,46 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
}
// Array targets store their layer count in z; layers never participate in mip
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
// 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 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) {
return target == TextureTarget::Texture3D;
return MipShrinkingAxisCount(target) == 3;
}
// 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
// height, so unlike a 2D texture its height takes no part in the reduction.
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips);
// The longest mip chain the level-0 size admits, over the axes that actually reduce.
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);
Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
const Int mipHeight = (target == TextureTarget::Texture1DArray) ? 1 : std::max<Int>(height, 1);
return ComputeFullMipmapLevelCount({std::max<Int>(width, 1), mipHeight, std::max<Int>(depth, 1)},
DepthParticipatesInMipmapping(target));
return ComputeFullMipmapLevelCount(
{std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
MipShrinkingAxisCount(target));
}
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
Int maxDimension = std::max<Int>(
baseTexelSize.x(),
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
Int maxDimension = 1;
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
}
Uint mipLevelCount = 1;
while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -444,13 +489,13 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount;
}
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
return {
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
: std::max<Int>(baseTexelSize.z(), 1),
};
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
std::max<Int>(baseTexelSize.z(), 1)};
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
}
return size;
}
Bool EnsureGeneratedMipmapStorageAllocated(
@@ -472,10 +517,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
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()) *
static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z());
@@ -522,44 +567,55 @@ namespace MobileGL::MG_Impl::GLImpl {
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
// the driver; the shadow state keeps reporting what the application asked for.
// The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for
// this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or
// GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are
// reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning
// that an application reads GL_MAX_SAMPLES once and hands that count to every
// 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,
TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
// 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 - validating against anything else is how the two answers drifted apart.
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
if (probedMaxSamples > 0) {
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);
const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
Bool isIntegerFormat = false;
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);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
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);
}
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);
return probedMaxSamples > 0 ? probedMaxSamples : categoryMaxSamples;
}
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_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
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(
ErrorCode::InvalidOperation,
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Sampler state is invalid for multisample textures."));
"Multisample textures do not accept sampler-state pnames."));
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 ((pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T) &&
(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
// (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
@@ -1852,6 +2007,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// TexParameteriv/TexParameterfv are introduced in OpenGL 4.0, so do not support them for now.
void TexParameterf_State(GLenum target, GLenum pname, GLfloat param) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -1949,6 +2105,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(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.
// TODO: implement the missing part
void TexParameterfv_State(GLenum target, GLenum pname, const GLfloat* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) {
case GL_TEXTURE_BORDER_COLOR: {
// ======================= Converting ================================
@@ -1972,6 +2131,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
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);
break;
}
@@ -1994,6 +2158,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void TexParameteriv_State(GLenum target, GLenum pname, const GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) {
case GL_TEXTURE_BORDER_COLOR: {
// ======================= Converting ================================
@@ -2003,6 +2168,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromInts(textureObject, params);
break;
}
@@ -2023,12 +2189,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void TexParameterIiv_State(GLenum target, GLenum pname, const GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromIntegerInts(textureObject, params);
break;
}
@@ -2051,12 +2219,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void TexParameterIuiv_State(GLenum target, GLenum pname, const GLuint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
switch (pname) {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
SetTextureBorderColorFromUnsignedInts(textureObject, params);
break;
}
@@ -2203,6 +2373,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) 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::ValidateTextureInternalFormat(textureInternalFormat)) 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,
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
const char* caller) {
@@ -2714,9 +2901,21 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
// ===================== Error Checking ==============================
if (!ValidateBufferTextureTarget(target, __func__)) return;
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) 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
// 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),
@@ -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.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
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(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -2767,6 +2968,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexParameterIuiv_State(GLenum target, GLenum pname, GLuint* params) {
if (params == nullptr) return;
if (!ValidateTextureParameterTarget(target, __func__)) return;
if (pname == GL_TEXTURE_BORDER_COLOR) {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -2798,6 +3000,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexParameterIiv_State(GLenum target, GLenum pname, GLint* params) {
if (params == nullptr) return;
if (!ValidateTextureParameterTarget(target, __func__)) return;
if (pname == GL_TEXTURE_BORDER_COLOR) {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -2823,6 +3026,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return false;
// ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -2905,11 +3110,16 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_TEXTURE_BORDER_COLOR:
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();
params[0] = static_cast<GLint>(borderColor.x());
params[1] = static_cast<GLint>(borderColor.y());
params[2] = static_cast<GLint>(borderColor.z());
params[3] = static_cast<GLint>(borderColor.w());
params[0] = MG_Util::FloatToSignedNormalizedInt32(borderColor.x());
params[1] = MG_Util::FloatToSignedNormalizedInt32(borderColor.y());
params[2] = MG_Util::FloatToSignedNormalizedInt32(borderColor.z());
params[3] = MG_Util::FloatToSignedNormalizedInt32(borderColor.w());
}
break;
case GL_TEXTURE_SWIZZLE_RGBA:
@@ -3003,6 +3213,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetTexParameterfv_State(GLenum target, GLenum pname, GLfloat* params) {
if (!ValidateTextureParameterTarget(target, __func__)) return;
// ======================= Converting ================================
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -3296,6 +3508,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_ALPHA_SIZE:
case GL_TEXTURE_DEPTH_SIZE:
case GL_TEXTURE_STENCIL_SIZE:
case GL_TEXTURE_SHARED_SIZE:
if (params) {
*params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname);
}
@@ -3468,6 +3681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_ALPHA_SIZE:
case GL_TEXTURE_DEPTH_SIZE:
case GL_TEXTURE_STENCIL_SIZE:
case GL_TEXTURE_SHARED_SIZE:
if (params) {
*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,
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
@@ -4001,7 +4365,20 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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,
@@ -4457,6 +4834,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) 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::ValidateTextureBorderNumber(border)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
@@ -4901,6 +5279,72 @@ namespace MobileGL::MG_Impl::GLImpl {
"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,
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
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);
if (!ValidateShadowReadbackLayout(textureObject, textureInputFormat, texturePixelDataType, texelSize.x(),
caller)) {
return;
}
const void* src = textureMipmapObject->MapMipmapData(textureUploadTarget, level);
if (!src) return;
@@ -5195,17 +5644,11 @@ namespace MobileGL::MG_Impl::GLImpl {
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);
// 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;
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) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
TextureParameterObject_State(textureObject, pname, param, __func__);
}
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
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) {
// 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,
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
}
@@ -6617,9 +7066,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"CopyTextureSubImage1D requires a 1D texture."));
return;
}
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
}
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."));
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
// rather than a slice within one; every other layered target keeps zoffset as the slice.
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
GLint sliceOffset = zoffset;
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__);
CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
/*allowCubeFaceFromZOffset=*/true, __func__);
}
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
@@ -8,9 +8,24 @@
#pragma once
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
namespace MobileGL::MG_Impl::GLImpl {
/* @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
// clears take the same list, so it is shared rather than written out twice.
Bool IsBufferTextureInternalFormat(GLenum internalformat);
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
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) {
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
Bool ValidateTextureLevelNumber(Int level);
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 ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border);
@@ -12,15 +12,15 @@
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
Uint GetMaxVertexAttribs() {
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
if (!MG_Backend::pActiveBackendObject) return capacity;
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
if (backendLimit <= 0) return capacity;
return std::min(static_cast<Uint>(backendLimit), capacity);
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
const Int backendLimit =
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
}
Uint GetMaxVertexAttribBindings() {
@@ -63,8 +63,10 @@ add_executable(MobileGLIntegrationTest
Scenarios/PipelineFailureScenario.cpp
Scenarios/AdvertisedLimitsScenario.cpp
Scenarios/PixelStoreSweepScenario.cpp
Scenarios/PrimitiveRestartScenario.cpp
Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
@@ -101,13 +103,18 @@ add_executable(MobileGLIntegrationTest
Scenarios/TextureViewScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredAttachmentShapeScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/SpirvShaderBinaryScenario.cpp
Scenarios/RelinkStageSetScenario.cpp
Scenarios/GuiBatchScenario.cpp
Scenarios/UnboundImageDescriptorScenario.cpp
Scenarios/IntegerBorderColorScenario.cpp
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -56,7 +56,13 @@ namespace MGITest {
const std::vector<LimitBound>& BufferLimitTable() {
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_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},
@@ -133,6 +139,49 @@ namespace MGITest {
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
<< 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
@@ -199,6 +248,80 @@ namespace MGITest {
"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 -
// 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
@@ -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);
constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback,
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 {
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
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
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) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
@@ -665,9 +671,57 @@ namespace MobileGL::MG_State {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true;
compositeHasStage[stage] = true;
}
}
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
// shader is wanted here even though the separable stage programs never get one.
composite->Link(true);
@@ -812,6 +866,22 @@ namespace MobileGL::MG_State {
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 {
return m_renderState.GetPatchVertices();
}
@@ -832,6 +902,26 @@ namespace MobileGL::MG_State {
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) {
m_renderState.SetCapability(cap, enabled);
}
@@ -1009,6 +1099,14 @@ namespace MobileGL::MG_State {
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) {
m_renderState.SetPixelStoreParam(param, value);
}
+11
View File
@@ -213,9 +213,18 @@ namespace MobileGL {
Float GetPointSize() const;
void SetPatchVertices(Uint vertices);
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 SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
Float GetPolygonOffsetFactor() 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);
GLenum GetHint(GLenum target) const;
void SetPointFadeThresholdSize(Float size);
@@ -276,6 +285,8 @@ namespace MobileGL {
Bool GetSampleCoverageInvert() const;
void SetSampleMaskValue(Uint32 mask);
Uint32 GetSampleMaskValue() const;
void SetMinSampleShadingValue(Float value);
Float GetMinSampleShadingValue() const;
void SetPixelStoreParam(PixelStoreParam param, Int value);
Int GetPixelStoreParam(PixelStoreParam param) const;
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const;
@@ -10,6 +10,7 @@
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Async/ShaderCompilePool.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.
static MobileGL::Int GetReflectionVertexAttribLimit(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
constexpr MobileGL::Int capacity =
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
if (!env.HasBackend()) return capacity;
const MobileGL::Int backendLimit = env.params.MaxVertexAttribs;
if (backendLimit <= 0) return capacity;
return std::min(backendLimit, capacity);
// One shared definition with glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) and with
// BuildTBuiltInResource's gl_MaxVertexAttribs - the three used to carry three copies of
// this formula and glslang's copy was a hardcoded 64.
return MobileGL::MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(env.HasBackend(),
env.params.MaxVertexAttribs);
}
// 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
// says what the SHADER declared rather than what glslang assigned, so both captures
// 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),
.explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation,
.explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.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);
auto result = ShaderCompiler::LinkProgram(attrib);
@@ -672,9 +679,14 @@ namespace MobileGL::MG_State::GLState {
return;
}
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
// The geometry stage's link properties. GL_GEOMETRY_INPUT_TYPE is load-bearing beyond the
// 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.gsOutputPrimitive = GL_NONE;
artifacts.gsMaxVertices = 0;
artifacts.gsInvocations = 0;
if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) {
switch (gs->getInputPrimitive()) {
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;
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 ----
@@ -1104,6 +1187,20 @@ namespace MobileGL::MG_State::GLState {
in.externalIndex, uniform.name.c_str());
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)) {
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
"surface",
@@ -1595,6 +1692,25 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
artifacts.uniformBlockBinding[i] =
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,
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.)
++m_backendStateVersion;
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
// 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.
@@ -536,6 +540,33 @@ namespace MobileGL::MG_State::GLState {
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
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;
Vector<SharedPtr<ShaderCompileTask>> deps;
@@ -787,6 +787,33 @@ namespace MobileGL::MG_State::GLState {
void MarkUBOContentDirty() const {
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 ----
//
// 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,
// which is why it is plain state here rather than something Link() consults.
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) {
m_separable = separable;
// ---- arming the uniform-write tracking latch ----
@@ -1296,6 +1331,14 @@ namespace MobileGL::MG_State::GLState {
std::set<String> uniformBlocksWithoutBinding;
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;
Int uniformNameMaxLength = 0;
Int attribInNameMaxLength = 0;
@@ -1317,6 +1360,27 @@ namespace MobileGL::MG_State::GLState {
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
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;
Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false;
@@ -1344,6 +1408,11 @@ namespace MobileGL::MG_State::GLState {
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
Vector<Uint> uniformOffsets;
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
// 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
@@ -1467,6 +1536,14 @@ namespace MobileGL::MG_State::GLState {
m_requestedXfbVaryings = Move(names);
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; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
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
// program has no geometry stage. Draws must present a compatible primitive type.
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; }
// 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_binaryRetrievableHint = 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
// 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.
@@ -278,6 +278,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformOffsets.clear();
artifacts.globalUboScratch.clear();
artifacts.reservedNumSamplesOffset = ProgramObject::kInvalidUniformOffset;
// kInvalidUniformOffset marks locations that end up without global-UBO backing
// (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.
@@ -311,6 +312,18 @@ namespace MobileGL::MG_State::GLState {
artifacts.globalUboScratch.resize(size);
}
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
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
// suffix before declaring the uniform unbacked.
@@ -140,17 +140,21 @@ namespace {
return std::nullopt;
}
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
// number the application was told, not against either half of it.
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers. Derived by the shared
// ResolveResourceBindingLimits so the compile-time scan below and the link-time general check
// (TMglGlslIoResolver::CheckDeclaredBindingRange) can never disagree about the number.
//
// 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(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const MobileGL::Int frontendPoints =
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));
return MobileGL::MG_State::GLState::ResolveResourceBindingLimits(env).MaxShaderStorageBufferBindings;
}
// 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 <MG_Util/Async/JobNode.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>
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
// 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
@@ -15,7 +15,77 @@
#include <MG_Util/ShaderTranspiler/Types.h>
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) {
// 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
// byte-identical one cannot change what a compile would produce: the whole
// 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) {
if (m_hasSpirvBinary || m_specializationFailed) {
SetShaderSource(static_cast<const String&>(source));
return;
}
if (SourceMatchesCompiledState(source)) return;
ReleaseCompileNode();
m_source = MakeShared<const String>(Move(source));
@@ -65,6 +65,51 @@ namespace MobileGL {
void SetShaderSource(const String& source);
void SetShaderSource(String&& source);
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
// 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,
@@ -99,14 +144,16 @@ namespace MobileGL {
const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; }
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
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
// discarded them (see CollectExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return Compiled().explicitUniformLocations;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetCompileStatus() const { return m_specializationFailed ? false : Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; }
// 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
// changes hands (AdoptCompileNode) or goes away (DropCompileNode).
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 MobileGL
@@ -217,12 +217,43 @@ namespace MobileGL {
return m_parameters.PatchVertices;
}
void RenderState::SetPolygonOffset(Float factor, Float units) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
// BumpVersions(), not just ++m_version, for the same reason SetPatchVertices does it:
// 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.PolygonOffsetUnits = units;
++m_version;
m_parameters.PatchDefaultOuterLevel = levels;
BumpVersions();
}
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 {
@@ -233,6 +264,37 @@ namespace MobileGL {
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 --------------------
namespace {
// CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller
@@ -270,6 +332,7 @@ namespace MobileGL {
SET_CAPABILITY(SampleAlphaToOne, enabled);
SET_CAPABILITY(SampleCoverage, enabled);
SET_CAPABILITY(SampleMask, enabled);
SET_CAPABILITY(SampleShading, enabled);
SET_CAPABILITY(StencilTest, enabled);
SET_CAPABILITY(ProgramPointSize, enabled);
case CapabilityInput::Blend: {
@@ -344,6 +407,7 @@ namespace MobileGL {
RETURN_CAPABILITY(SampleAlphaToOne);
RETURN_CAPABILITY(SampleCoverage);
RETURN_CAPABILITY(SampleMask);
RETURN_CAPABILITY(SampleShading);
RETURN_CAPABILITY(StencilTest);
RETURN_CAPABILITY(ProgramPointSize);
case CapabilityInput::Blend:
@@ -737,6 +801,20 @@ namespace MobileGL {
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 --------------------
void RenderState::SetPixelStoreParam(PixelStoreParam param, Int value) {
#define SET_PIXEL_STORE_PARAM(paramNameHead, paramNameTail, val) \
@@ -240,8 +240,24 @@ namespace MobileGL {
Float PointSize = 1.0f;
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
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 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
Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> BlendStates;
@@ -271,6 +287,10 @@ namespace MobileGL {
Float SampleCoverageValue = 1.0f;
Bool SampleCoverageInvert = false;
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{};
// Cull Face
@@ -319,6 +339,7 @@ namespace MobileGL {
Bool SampleAlphaToOneEnabled = false;
Bool SampleCoverageEnabled = false;
Bool SampleMaskEnabled = false;
Bool SampleShadingEnabled = false;
Bool StencilTestEnabled = false;
Bool ProgramPointSizeEnabled = false;
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
@@ -374,9 +395,21 @@ namespace MobileGL {
Float GetPointSize() const;
void SetPatchVertices(Uint vertices);
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);
// 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 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).
void SetHint(GLenum target, GLenum mode);
GLenum GetHint(GLenum target) const;
@@ -454,6 +487,9 @@ namespace MobileGL {
Bool GetSampleCoverageInvert() const;
void SetSampleMaskValue(Uint32 mask);
Uint32 GetSampleMaskValue() const;
// glMinSampleShading. `value` is stored as given; the entry point clamps.
void SetMinSampleShadingValue(Float value);
Float GetMinSampleShadingValue() const;
// Pixel Store
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
// what glTexParameterIiv/Iuiv mean: those forms are for integer texture formats, whose
// 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) {
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.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
@@ -169,8 +181,12 @@ namespace MobileGL {
}
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.borderColorUI =
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
@@ -182,8 +198,12 @@ namespace MobileGL {
}
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.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
@@ -206,6 +226,10 @@ namespace MobileGL {
return m_samplerParameters.borderColorUI;
}
BorderColorForm SamplerObject::GetBorderColorForm() const {
return m_samplerParameters.borderColorForm;
}
SamplerCompareMode SamplerObject::GetCompareMode() const {
return m_samplerParameters.compareMode;
}
@@ -56,6 +56,19 @@ namespace MobileGL {
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 {
SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
@@ -79,6 +92,7 @@ namespace MobileGL {
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 borderColorI = {0, 0, 0, 0};
UintVec4 borderColorUI = {0, 0, 0, 0};
BorderColorForm borderColorForm = BorderColorForm::Float;
};
namespace MG_State {
@@ -117,6 +131,7 @@ namespace MobileGL {
const FloatVec4& GetBorderColor() const;
const IntVec4& GetBorderColorI() const;
const UintVec4& GetBorderColorUI() const;
BorderColorForm GetBorderColorForm() const;
Uint GetExternalIndex() const;
Uint16 GetVersion() const;
// Globally-unique, never-reused id for this sampler object's lifetime. Lets a
@@ -113,8 +113,14 @@ namespace MobileGL {
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) {
if (color == m_sampler->GetBorderColor()) return;
if (color == m_sampler->GetBorderColor() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Float) {
return;
}
m_sampler->SetBorderColor(color);
++m_textureParamsVersion;
@@ -125,7 +131,10 @@ namespace MobileGL {
}
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_textureParamsVersion;
@@ -136,12 +145,19 @@ namespace MobileGL {
}
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_textureParamsVersion;
}
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
return m_sampler->GetBorderColorForm();
}
TextureSwizzleParam TextureObjectBase::GetSwizzleParam(TextureSwizzleParam param) const {
switch (param) {
case TextureSwizzleParam::Red:
@@ -40,6 +40,8 @@ namespace MobileGL::MG_State::GLState {
virtual void SetBorderColorI(const IntVec4& color) = 0;
virtual const UintVec4& GetBorderColorUI() const = 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 void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 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 Bool IsImmutable() 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 Uint16 GetTextureParamsVersion() const = 0;
// 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;
const UintVec4& GetBorderColorUI() const override;
void SetBorderColorUI(const UintVec4& color) override;
BorderColorForm GetBorderColorForm() const override;
TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override;
const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const override;
void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override;
@@ -132,6 +141,10 @@ namespace MobileGL::MG_State::GLState {
void SetMaxLevel(Uint maxLevel) override;
Bool IsImmutable() 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;
Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override;
@@ -15,6 +15,8 @@
#include <MG_Backend/DirectGLES/Utils.h>
#include <limits>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
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
// 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) {
const String out = BuildPassthroughTessControlEssl(320, 4, "", "");
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
EXPECT_TRUE(Contains(out,
@@ -1314,10 +1321,63 @@ TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel
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
// that the driver runs the evaluation stage at all, so the only question is the spelling.
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_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
}
@@ -1325,7 +1385,7 @@ TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
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, "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(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;
}
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;
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices, kDefaultOuter,
kDefaultInner, perVertexMembers);
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
@@ -162,11 +167,85 @@ TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
// the right gate and both have to move together.
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_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
// 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
@@ -179,13 +258,33 @@ TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
using namespace MG_Util::ShaderTranspiler;
// 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.
static const char* kVs = R"(#version 430 core
// MORE THAN ONE VERSION, because the shape is a function of the neighbour's GLSL version and
// a single-version case cannot see that. glslang gates gl_PerVertex's gl_CullDistance member
// 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;
void main() { gl_Position = g_in_position; }
)";
static const char* kTes = R"(#version 430 core
const String tes = String(version) + R"(
layout(quads) in;
void main() {
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
@@ -193,41 +292,47 @@ void main() {
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;
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<const char*> sources{kVs, kTes, kFs};
const Vector<const String*> sources{&vs, &tes, &fs};
Vector<SharedPtr<glslang::TShader>> shaders;
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);
ASSERT_TRUE(compiled) << compiled.error().log;
ASSERT_TRUE(compiled) << (compiled ? String{} : compiled.error().log);
shaders.push_back(compiled.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
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()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binary);
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
// built-in block that stage declares as an input, so the module holds exactly one such shape
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
// built-in block that stage declares as an input, so the module holds exactly one such
// shape besides its own gl_PerVertex output - and both are the same shape, which is the
// point.
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
ASSERT_FALSE(tesShapes.empty());
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
EXPECT_GE(frontendMembers, 1u) << "a gl_PerVertex block always carries at least gl_Position";
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
// evaluation stage's own module, not from a constant the test picked.
const Uint32 reflectedMembers = ProgramFactory::ReflectPerVertexInputMembers((*binary)[1]);
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());
@@ -245,3 +350,4 @@ void main() { g_fs_out = vec4(0, 1, 0, 1); }
}
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
}
}
@@ -31,6 +31,7 @@
#include "Init.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.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_State/GLState/Core.h"
@@ -243,11 +244,17 @@ TEST_F(ProgramPipelineCompositeTest, AValueIdenticalWriteStillTakesTheSlotForIts
DeleteProgramPipelines(1, &pipeline);
}
// glUseProgramStages here accepts a program that was never linked as separable (GL 4.6 core 7.4
// says it should not, and MobileGL validates only LINK_STATUS). Such a program has recorded
// none of its writes, because nothing ever armed its tracking latch - so the mirror has to fall
// back to carrying everything rather than carrying nothing. Mirroring nothing would have been a
// fresh regression on a shape that worked before the dirty set existed.
// A stage program that recorded NONE of its writes, because nothing ever armed its tracking
// latch: the mirror has to fall back to carrying everything rather than carrying nothing.
// Mirroring nothing would have been a 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) {
const char* vsSource = R"(#version 430 core
uniform vec4 u_vsOnly;
@@ -270,9 +277,19 @@ void main() { gl_Position = u_vsOnly; }
GLuint pipeline = 0;
GenProgramPipelines(1, &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);
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};
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
@@ -503,3 +520,205 @@ void main() { o_color = u_shared; }
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 <algorithm>
#include <cstring>
#include <map>
#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";
}
// ---------------------------------------------------------------------------------------------
// 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 <cctype>
#include <cstdlib>
#include <ios>
#include "Includes.h"
#include "Init.h"
@@ -814,3 +815,113 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
}
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/Debug/Log.h>
#include <MG_Util/Types.h>
#include <limits>
#include <set>
namespace {
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
public:
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params):
m_params(params) {}
// `type` defaults to Unknown, which is what every existing case wanted: a limits-only
// 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 {}
MobileGL::Bool InitCapabilities() override { return true; }
@@ -55,10 +63,11 @@ namespace {
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
return m_params;
}
MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; }
MobileGL::BackendType GetBackendType() const override { return m_type; }
private:
MobileGL::MG_Backend::DynamicBackendParameters m_params;
MobileGL::BackendType m_type = MobileGL::BackendType::Unknown;
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{};
MobileGL::RendererInfo m_info{
.RendererName = "Test",
@@ -722,6 +731,62 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
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_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
@@ -1234,6 +1299,256 @@ void main() {
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) {
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, 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.
a.maxComputeWorkGroupInvocations = 128;
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;
b.params.MaxColorTextureSamples = 8;
a.params.MaxTextureSize = 4096;
@@ -572,8 +577,8 @@ namespace {
b.params.MaxViewports = 16;
a.params.MaxUniformBufferBindings = 24;
b.params.MaxUniformBufferBindings = 84;
a.params.MaxTextureImageUnits = 16;
b.params.MaxTextureImageUnits = 32;
a.params.MaxRenderbufferSize = 4096;
b.params.MaxRenderbufferSize = 16384;
return {a, b};
}
} // namespace
@@ -677,6 +682,20 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
// 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.
{"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[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 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},
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
GL_INVALID_ENUM},
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION
// that used to win is still raised for the calls it is actually about.
// A mode the enum check accepts falls through to the no-program path, which is now
// 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); },
GL_INVALID_OPERATION},
GL_NO_ERROR},
});
}
+394
View File
@@ -26,6 +26,9 @@
#include "Includes.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/RenderState/GL_RenderState.h>
#include <MG_State/GLState/Core.h>
@@ -632,3 +635,394 @@ TEST_F(RenderStateTest, TheFirstScissorWriteBumpsTheVersionEvenWhenTheValueDoesN
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
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 <MG_Backend/BackendObjects.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_State/GLState/Core.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);
}
// ...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) {
const GLuint storage = MakeImmutable2D(3, 16, 16);
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
// clipping program silently rendered nothing. The guarded probe below only ever widens it.
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;
// 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
@@ -1333,6 +1338,23 @@ namespace MobileGL::MG_Util::BackendLoader {
"rejected; reporting no clip distances");
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);
// 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
// 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.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.LayerProvokingVertex = layerProvokingVertex;
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
@@ -1651,6 +1681,8 @@ namespace MobileGL::MG_Util::BackendLoader {
// and "this driver has no clip distances".
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances,
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_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
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
// in FillInGLESCapabilities.
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;
// 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
@@ -199,6 +199,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments);
caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments);
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.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]);
caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]);
@@ -239,6 +241,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE;
caps.SupportsShaderCullDistance = supportedFeatures.shaderCullDistance == VK_TRUE;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
HasUsableShaderSubgroupSupport(subgroupProps);
@@ -319,6 +322,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments);
caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments);
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.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]);
caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]);
@@ -336,6 +341,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsFragmentStoresAndAtomics = false;
caps.SupportsGeometryShader = false;
caps.SupportsShaderClipDistance = false;
caps.SupportsShaderCullDistance = false;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
caps.SupportsShaderSubgroup = false;
caps.SubgroupSize = 0;
@@ -68,6 +68,11 @@ namespace MobileGL {
Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 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 MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
@@ -106,6 +111,11 @@ namespace MobileGL {
// 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.
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;
Bool SupportsShaderSubgroup = false;
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 <cstring>
namespace MobileGL {
template <typename Derived, typename T, SizeT N>
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>
struct Vec2 : public VecBase<Vec2<T>, T, 2> {
using Base = VecBase<Vec2<T>, T, 2>;
@@ -10,6 +10,15 @@
namespace MobileGL {
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) {
switch (type) {
// Scalars
+14
View File
@@ -12,5 +12,19 @@
namespace MobileGL {
namespace MG_Util {
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 MobileGL
@@ -23,6 +23,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
}
} // 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 state = 0x9e3779b97f4a7c15ull;
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
// shader compiles at all and what a module that reads the constant generates.
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
// 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 -
+19 -4
View File
@@ -84,11 +84,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// 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
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d),
// the third time in three waves that a hardcoded TBuiltInResource field became
// env-derived - assume the next wave does it again and re-audit.
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d)
// and the six after it at the GL 4.6 API-surface wave - the fourth time in four
// 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.
// 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
@@ -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
// different envs really do produce different P0b cache keys.
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
@@ -12,6 +12,11 @@
#include "ShaderCompiler.h"
#include <algorithm>
#include <format>
#include <cmath>
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
#include "SpirvPasses/FlattenInterfaceStructPass.h"
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
@@ -64,6 +69,30 @@
namespace MobileGL {
namespace MG_Util {
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
// backend", which is what the standalone/test entry points do. The pipeline always
// passes one, so a worker never reaches pActiveBackendObject through here.
@@ -73,16 +102,11 @@ namespace MobileGL {
Resources.maxClipPlanes = 6;
Resources.maxTextureUnits = 32;
Resources.maxTextureCoords = 32;
Resources.maxVertexAttribs = 64;
Resources.maxVertexUniformComponents = 4096;
Resources.maxVaryingFloats = 64;
Resources.maxVertexTextureImageUnits = 32;
Resources.maxCombinedTextureImageUnits = 80;
Resources.maxTextureImageUnits = 32;
Resources.maxVertexUniformComponents = MAX_VERTEX_UNIFORM_COMPONENTS;
Resources.maxVaryingFloats = MAX_VARYING_COMPONENTS;
Resources.maxFragmentUniformComponents = 4096;
Resources.maxDrawBuffers = 32;
Resources.maxVertexUniformVectors = 128;
Resources.maxVaryingVectors = 8;
Resources.maxVertexUniformVectors = MAX_VERTEX_UNIFORM_VECTORS;
Resources.maxVaryingVectors = MAX_VARYING_VECTORS;
Resources.maxFragmentUniformVectors = 256;
Resources.maxVertexOutputVectors = 16;
Resources.maxFragmentInputVectors = 15;
@@ -93,14 +117,12 @@ namespace MobileGL {
Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = 60;
Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS;
Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64;
Resources.maxGeometryOutputComponents = 128;
Resources.maxFragmentInputComponents = 128;
Resources.maxImageUnits = 8;
Resources.maxCombinedImageUnitsAndFragmentOutputs = 8;
Resources.maxCombinedShaderOutputResources = 8;
Resources.maxImageSamples = 0;
Resources.maxVertexImageUniforms = 0;
Resources.maxTessControlImageUniforms = 0;
@@ -113,16 +135,18 @@ namespace MobileGL {
Resources.maxGeometryTotalOutputComponents = 1024;
Resources.maxGeometryUniformComponents = 1024;
Resources.maxGeometryVaryingComponents = 64;
Resources.maxTessControlInputComponents = 128;
Resources.maxTessControlOutputComponents = 128;
Resources.maxTessControlTextureImageUnits = 16;
Resources.maxTessControlUniformComponents = 1024;
Resources.maxTessControlTotalOutputComponents = 4096;
Resources.maxTessEvaluationInputComponents = 128;
Resources.maxTessEvaluationOutputComponents = 128;
Resources.maxTessEvaluationTextureImageUnits = 16;
Resources.maxTessEvaluationUniformComponents = 1024;
Resources.maxTessPatchComponents = 120;
// The tessellation block is shared with glGetIntegerv through Types.h; see the
// "Never move one of these without the other" note there.
Resources.maxTessControlInputComponents = MAX_TESS_CONTROL_INPUT_COMPONENTS;
Resources.maxTessControlOutputComponents = MAX_TESS_CONTROL_OUTPUT_COMPONENTS;
Resources.maxTessControlTextureImageUnits = MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS;
Resources.maxTessControlUniformComponents = MAX_TESS_CONTROL_UNIFORM_COMPONENTS;
Resources.maxTessControlTotalOutputComponents = MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS;
Resources.maxTessEvaluationInputComponents = MAX_TESS_EVALUATION_INPUT_COMPONENTS;
Resources.maxTessEvaluationOutputComponents = MAX_TESS_EVALUATION_OUTPUT_COMPONENTS;
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.maxTessGenLevel = 64;
Resources.maxViewports = 16;
@@ -148,9 +172,6 @@ namespace MobileGL {
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64;
Resources.maxCullDistances = 8;
Resources.maxCombinedClipAndCullDistances = 8;
Resources.maxSamples = 4;
Resources.maxMeshOutputVerticesNV = 256;
Resources.maxMeshOutputPrimitivesNV = 512;
Resources.maxMeshWorkGroupSizeX_NV = 32;
@@ -180,12 +201,37 @@ namespace MobileGL {
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs =
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.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
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
// i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the
// 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
// agree, which KHR-GLxx.clip_distance.coverage compares directly.
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
// are the only ones that come from a REAL indexed driver query, which
@@ -242,7 +296,15 @@ namespace MobileGL {
tshader->setStrings(src, 1);
tshader->setNanMinMaxClamp(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) {
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
@@ -487,12 +549,28 @@ namespace MobileGL {
attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings,
attrib.storageBlocksWithoutBinding,
attrib.uniformBlocksWithoutBinding);
attrib.uniformBlocksWithoutBinding,
&attrib.resourceBindingLimits,
attrib.resourceBindingViolation);
break;
}
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;
r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog());
r.errc = -4;
@@ -1604,6 +1682,197 @@ namespace MobileGL {
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) {
spvc_compiler_options options;
session.CreateOptions(&options);
@@ -18,6 +18,18 @@
namespace MobileGL {
namespace MG_Util {
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 {
public:
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
@@ -393,6 +405,78 @@ namespace MobileGL {
bool enableSpirvValidation = false);
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
// reach, on the calling thread, so the built-in symbol tables those
// configurations need are already cached before any worker asks for one.
@@ -13,6 +13,7 @@
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#include <utility>
#include <Config.h>
@@ -234,15 +235,132 @@ namespace {
// 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.
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; }
};
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) {
const MobileGL::String code = MaskCommentsAndQuotedText(source);
ShaderLanguageInfo info;
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;
while (lineStart < code.size()) {
SizeT lineEnd = code.find('\n', lineStart);
@@ -256,46 +374,49 @@ namespace {
probe = 3;
}
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;
probe++;
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd);
if (directive == "version" && !info.HasVersionDirective()) {
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) {
info.version = version;
if (directive == "version") {
ParsedVersionDirective parsed;
if (ParseVersionDirectiveBody(code, probe, lineEnd, parsed)) {
if (!info.HasVersionDirective()) {
info.version = parsed.version;
info.profile = parsed.profile;
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;
info.hasValidVersionDirective = parsed.isValid;
} else {
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any
// downstream routing, but mark the directive malformed.
info.profile = MobileGL::ShaderProfile::Core;
profileTokenValid = false;
recordIfRedundant(directiveStart, lineEnd);
}
// 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") {
SkipDirectiveWhitespace(code, probe, lineEnd);
@@ -309,6 +430,10 @@ namespace {
extension == "GL_NV_gpu_shader5";
const bool enablesExtension = behavior == "enable" || behavior == "require" ||
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
// conservative around conditional directives and keeps legal sample qualifiers intact.
info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension);
@@ -319,6 +444,19 @@ namespace {
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;
}
@@ -367,9 +505,40 @@ namespace {
// (FindAfterVersionDirective -> InspectShaderLanguage). Each branch below leaves the bytes
// 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.
// 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) {
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
// 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
@@ -1435,6 +1604,215 @@ namespace {
"#define gl_DepthRange mg_DepthRange\n";
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 MobileGL {
@@ -1463,6 +1841,13 @@ namespace MobileGL {
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
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+)
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
@@ -1487,9 +1872,31 @@ namespace MobileGL {
ModernizeLegacyGLSL(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) {
// 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
@@ -40,6 +40,27 @@ namespace MobileGL {
// accept - can be retried instead of failing to compile.
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",
// "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.
@@ -8,6 +8,8 @@
#include "SpvcSession.h"
#include <algorithm>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
@@ -184,11 +186,29 @@ namespace MobileGL {
const SpvId* p_spirv = spirv.data();
size_t word_count = spirv.size();
spvc_context_create(&context);
spvc_context_parse_spirv(context, p_spirv, word_count, &ir);
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
&compiler);
spvc_compiler_create_shader_resources(compiler, &resources);
// Every step is checked, and each guards the next: the C API writes its
// out-param only on success, so passing a failed step's null handle to the
// step after it is a raw dereference (spvc_context_create_compiler does
// `parsed_ir->parsed`, spvc_compiler_create_shader_resources does
// `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) {
SpvReflectResult result = spvReflectCreateShaderModule(
spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule);
@@ -496,6 +516,271 @@ namespace MobileGL {
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) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
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 {
Reflection = 1 << 0,
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
// constrain the accesses the shader makes, and the driver is entitled to know.
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);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;
+93
View File
@@ -14,6 +14,20 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
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
// synthesized storage block named "<this>_<GL atomic-counter binding>"
// (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_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 {};
enum class ShaderCompileBits : Uint {
@@ -84,6 +145,33 @@ namespace MobileGL {
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 {
Vector<SharedPtr<glslang::TShader>> shaders;
UnorderedMap<String, Uint> explicitVertexInLocations;
@@ -99,6 +187,11 @@ namespace MobileGL {
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
std::set<String>* storageBlocksWithoutBinding = 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 {
@@ -13,6 +13,8 @@
#include "TMglGlslIoResolver.h"
#include <cstring>
#include <cstdlib>
#include <string>
#include <MG_Util/ShaderTranspiler/Types.h>
@@ -165,6 +167,101 @@ namespace MobileGL {
// 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
// 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) {
const glslang::TType& type = ent.symbol->getType();
const glslang::TQualifier& qualifier = type.getQualifier();
@@ -207,6 +304,8 @@ namespace MobileGL {
m_uniformBlocksWithoutBinding->insert(name.c_str());
}
CheckDeclaredBindingRange(type, name);
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
}
@@ -21,27 +21,35 @@
#include <glslang/MachineIndependent/iomapper.h>
#include "TVarEntryInfo.h"
#include "MG_Util/Types.h"
#include "MG_Util/ShaderTranspiler/Types.h"
namespace MobileGL {
class TMglGlslIoResolver : public glslang::TDefaultGlslIoResolver {
public:
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
using ResourceBindingLimits = MG_Util::ShaderTranspiler::ResourceBindingLimits;
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
ExplicitVarSlotMap* opaqueUniformBindings,
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),
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding),
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding) {}
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding), m_bindingLimits(bindingLimits),
m_bindingViolation(bindingViolation) {}
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
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,
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding) {}
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding,
bindingLimits, bindingViolation) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) 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
// 1. See ProgramLinkTask's UBO reflection loop for what is done with them.
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_plainUniformLocationByName;
bool m_plainUniformLocationsAssigned = false;

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