Compare commits

...
27 Commits
Author SHA1 Message Date
swung0x48 666f150202 [Fix] (SelfTest): stop optional-capability failures from declaring the whole backend unsupported, and log the POST report in chunks 2026-08-22 09:08:13 -04:00
swung0x48 2f62970dd5 [Refactor] (SelfTest): give every POST capability row a PASS/WARN/FAIL verdict and keep INFO for identity 2026-08-22 08:59:03 -04:00
swung0x48 dcb568d445 [Feature, Test] (SelfTest): probe the four remaining known driver bugs from the POST 2026-08-22 08:45:05 -04:00
swung0x48 a5f36c8f8d [Feature, Test] (SelfTest): add a Known Driver Bugs POST section and probe the geometry write-after-emit drop 2026-08-22 07:49:17 -04:00
swung0x48 1ebe9d11c5 [Fix, Test] (GLState, DirectGLES): stop calling a multisample texture filter-incomplete so it still binds 2026-08-22 07:18:11 -04:00
swung0x48 a8bb63950d [Fix, Test] (ShaderTranspiler): relocate a late length constant so an offset atomic-counter block still flattens 2026-08-22 07:18:11 -04:00
swung0x48 7d68a17774 [Fix, Test] (DirectGLES): give a split buffer image its own view so the sampler still sees whole texels 2026-08-22 06:16:52 -04:00
swung0x48 415645ccdd [Merge] (ShaderTranspiler, DirectGLES): land the normalized-format carriers and the buffer-image split 2026-08-22 04:47:39 -04:00
swung0x48 e6d03eb2a1 [Merge] (DirectGLES): take the image-alias rename under the format carriers 2026-08-22 04:44:41 -04:00
swung0x48 5e29e7d266 [Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage 2026-08-22 04:30:06 -04:00
swung0x48 7eac33d17b [Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui 2026-08-22 04:17:46 -04:00
Swung0x48 8f19ce6fa7 [Fix, Test] (DirectGLES): rename an image SPIRV-Cross already qualified so two stages cannot merge it 2026-08-22 03:46:35 -04:00
swung0x48 d0f7fb99db [Feat, Test] (ShaderTranspiler, DirectGLES): carry rgb10_a2ui storage images in rgba16ui and split its packed upload 2026-08-22 03:21:48 -04:00
swung0x48 d4247db6c3 [Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively 2026-08-22 00:57:06 -04:00
swung0x48 e4f41e0fd3 [Merge] (ShaderTranspiler, DirectGLES): land the fp64 block layout, colour-index and readonly-writeonly repairs 2026-08-21 22:24:38 -04:00
swung0x48 9cf340cbef [Merge] (DirectGLES, ShaderTranspiler): take the viewport routing and image repairs under the fp64 and qualifier fixes 2026-08-21 22:21:17 -04:00
swung0x48 348a30a816 [Fix, Test] (ShaderTranspiler): keep a storage block with doubles at the byte layout it was bound with 2026-08-21 22:15:05 -04:00
swung0x48 b5e0ada97e [Merge] (DirectGLES, ShaderTranspiler, Config): land the viewport-array routing emulation 2026-08-21 22:13:50 -04:00
swung0x48 cb27ac7761 [Merge] (DirectGLES): take the image-widening repairs under the viewport routing 2026-08-21 22:10:53 -04:00
swung0x48 38c56a3d38 [Test] (DirectGLES, IntegrationTest): run the viewport-array scenarios on Espryt and pin the routing rewrites against a negative control 2026-08-21 22:08:33 -04:00
swung0x48 908172ba0f [Feat] (DirectGLES, Config, ShaderTranspiler): route gl_ViewportIndex on Espryt by replaying a draw per distinct viewport state 2026-08-21 22:08:25 -04:00
swung0x48 e18bac8cb2 [Fix, Test] (ShaderTranspiler, DirectGLES): never widen a buffer image - its texels are the application buffer, not storage we can reallocate 2026-08-21 22:02:23 -04:00
swung0x48 7b0f443d3a [Fix, Test] (ShaderTranspiler, DirectGLES): drop the inert readonly+writeonly pair a storage block cannot carry in ESSL 2026-08-21 21:43:42 -04:00
swung0x48 f1b4a5e07f [Fix, Test] (ShaderTranspiler, DirectGLES): stop printing the default fragment-output colour index into ESSL 2026-08-21 21:42:31 -04:00
swung0x48 f3cd4091bf [Fix, Test] (DirectGLES): decode the packed r11f_g11f_b10f shadow into the float level its rgba16f carrier is uploaded as 2026-08-21 21:38:45 -04:00
swung0x48 529d26f38f [Fix] (DirectGLES): arm the image-format widening for r11f_g11f_b10f in the reflection gate too 2026-08-21 21:27:14 -04:00
swung0x48 9bd125aeec [Fix, Test] (ShaderTranspiler): carry r11f_g11f_b10f storage images in rgba16f instead of losing the stage 2026-08-21 21:21:54 -04:00
59 changed files with 9789 additions and 579 deletions
+2
View File
@@ -279,6 +279,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
@@ -309,6 +310,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
+13
View File
@@ -212,6 +212,19 @@ namespace MobileGL::MG_Config {
// miscompiled shader: if a device ever renders differently with the cache
// on, one run with this falsy says so.
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// emulation - the builtin becomes a flat varying, the fragment stage gets a
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
// it is ON even where the driver advertises GL_OES_viewport_array, because that
// extension only ever gave the SHADER a compilable name: MobileGL has never
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
// and nothing else), so on an extension-capable driver every index rasterized as
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
// the pre-emulation path, extension passthrough where it exists and
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
// the negative control the emulation is measured against.
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+2
View File
@@ -195,6 +195,8 @@ namespace MobileGL::MG_ConfigLoader {
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.ViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
}
inline void InitBackendType() {
+24
View File
@@ -445,12 +445,36 @@ namespace MobileGL {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
@@ -1331,9 +1331,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
// land on this backend regardless of what the driver underneath happens to support.
// Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
// module that still declared Float64 would never reach the driver at all. The demotion is
// mathematically mandatory here, on every device, forever - which is why this stays false
// regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsShaderFloat64 = false;
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
+360 -42
View File
@@ -1088,7 +1088,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendObj = MakeShared<BackendTextureObject>();
}
if (imageBindableStorageRequired) {
backendObj->RequireImageBindableStorage();
backendObj->RequireImageBindableStorage(textureObject);
}
backendObj->SyncTextureParamsToBackend(textureObject);
backendObj->SyncBuiltinSamplerToBackend(textureObject);
@@ -1482,15 +1482,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
// a bind format that names a class the storage does not have is left alone: GL
// already calls that undefined, and inventing a carrier for it would only make the
// out-of-class read wider.
//
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
// its own to widen (its texels are the application's buffer object), so
// WidenImageFormatsPass declines to widen every buffer image and the bind must decline
// with it, or the driver would be handed a carrier the shader never addressed. See the
// Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it.
//
// What a buffer image takes instead is the SPLIT, which is the same three-layer move
// through a different door: a private glTexBuffer view names the single-channel base
// format, the bind below names it too, and the shader subscripts it two components per
// original texel. Same gate on all three, so they cannot disagree.
//
// The split view is a SEPARATE texture name over the same buffer, and the bind has to
// name it rather than the application's own: the application's texture keeps the
// format it asked for so that a samplerBuffer reading the same buffer texture - which
// is NOT subscript-rewritten - still sees whole texels. See
// BackendTextureObject::m_bufferImageSplitViewId.
GLenum bindFormat = imageBinding.Format;
if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
GLuint bindTextureId = backendTexture->GetBackendTextureId();
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
GL_UNKNOWN_MGL) {
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
boundFormatSplit != GL_UNKNOWN_MGL) {
bindFormat = boundFormatSplit;
if (const Uint splitViewId = backendTexture->GetBufferImageSplitViewId();
splitViewId != 0) {
bindTextureId = splitViewId;
}
}
}
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
if (boundFormatWidening) {
bindFormat = boundFormatWidening.InternalFormat;
}
}
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
g_GLESFuncs.glBindImageTexture(unit, bindTextureId, imageBinding.Level,
layered, layer, imageBinding.Access, bindFormat);
}
@@ -3260,6 +3291,188 @@ namespace MobileGL::MG_Backend::DirectGLES {
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
}
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
// See the block comment in Managers.h for what this is and why. Here is the state half: one
// replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one
// viewport / one scissor / one depth range and telling the fragment gate which indices it
// serves.
namespace ViewportRoutingImpl {
// One replay pass: the state to push, and the set of gl_ViewportIndex values whose
// fragments this pass is allowed to keep.
struct RoutingPass {
IntVec4 viewport{};
IntVec4 scissorBox{};
FloatVec2 depthRange{};
Bool scissorTest = false;
Uint32 indexMask = 0;
};
static constexpr Uint32 kAllViewportsMask =
(RenderStateParameters::MAX_VIEWPORTS >= 32)
? 0xFFFFFFFFu
: ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u);
// The plan for the draw currently being issued. A file-scope buffer rather than a return
// value because Begin/Apply/End are three calls around a draw the caller writes, and a
// fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant,
// which is a property of the call sites and not an accident: every wrap in this file and
// in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside
// another - and a multi-draw tier that replayed its whole loop would be nesting.
static Array<RoutingPass, RenderStateParameters::MAX_VIEWPORTS> g_passes{};
static Uint g_passCount = 0;
static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr;
// What index `i` actually rasterizes against, resolved exactly the way SyncRenderState
// resolves index 0 - including both substitutions it makes, which are not cosmetic:
//
// * a viewport of zero extent means "the application has never called glViewport", and
// GL's initial viewport is the whole surface, which the frontend cannot spell before
// a surface exists;
// * a scissor rectangle is read through the WRITTEN flag and not through its extent,
// because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment"
// and is byte-identical to the never-written default that means the opposite.
//
// Resolving them here rather than deferring to SyncRenderState is what makes the grouping
// below correct: two indices that differ only in a field that resolves to the same
// rectangle really do rasterize identically and must share one pass.
static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index,
Int surfaceWidth, Int surfaceHeight) {
RoutingPass pass;
const FloatVec4& viewport = parameters.Viewports[index];
pass.viewport = IntVec4(static_cast<Int>(std::lround(viewport.x())),
static_cast<Int>(std::lround(viewport.y())),
static_cast<Int>(std::lround(viewport.z())),
static_cast<Int>(std::lround(viewport.w())));
if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) {
pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
}
pass.scissorBox = parameters.ScissorBoxes[index];
if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 &&
surfaceHeight > 0) {
pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
}
pass.depthRange = parameters.DepthRanges[index];
pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0;
return pass;
}
static Bool SameState(const RoutingPass& a, const RoutingPass& b) {
return a.viewport == b.viewport && a.scissorBox == b.scissorBox &&
a.depthRange == b.depthRange && a.scissorTest == b.scissorTest;
}
} // namespace ViewportRoutingImpl
Uint BeginViewportRoutingPasses() {
using namespace ViewportRoutingImpl;
g_passCount = 1;
g_routedProgram = nullptr;
// The whole emulation behind one static load, for every application that has never built
// a program writing gl_ViewportIndex - which is all of them but the conformance suite.
// Without it every draw in the process would pay GetCurrentBackendProgram's chain of
// frontend lookups for an answer that cannot change.
if (!g_anyProgramRoutesViewportIndex) {
return 1;
}
auto* program = GetCurrentBackendProgram();
if (program == nullptr || !program->RoutesViewportIndex()) {
return 1;
}
g_routedProgram = program;
// The gate reads zero until something writes it, and a zero mask discards every fragment.
// So this is not an optimization that can be skipped in the one-pass case - it is what
// keeps a routing program drawing at all.
program->SetViewportPassMask(kAllViewportsMask);
// Replaying multiplies every side effect the vertex and geometry stages have, and the
// fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform
// feedback records per emitted primitive, so a replayed draw would write its vertices N
// times; rasterizer discard means there are no fragments to gate at all, so replaying
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
// open gate, i.e. to the pre-emulation behaviour, rather than to wrong data.
if (MG_State::pGLContext->IsTransformFeedbackActive() ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return 1;
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
Int surfaceWidth = 0;
Int surfaceHeight = 0;
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
surfaceWidth = 0;
surfaceHeight = 0;
}
Uint count = 0;
for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) {
const RoutingPass resolved =
ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight);
Uint existing = 0;
for (; existing < count; ++existing) {
if (SameState(g_passes[existing], resolved)) break;
}
if (existing == count) {
g_passes[count] = resolved;
++count;
}
g_passes[existing].indexMask |= (1u << index);
}
// One group is the overwhelmingly common case - it is what glViewport, glScissor and
// glDepthRange leave behind, because ARB_viewport_array defines all three as writing
// EVERY index. The mask is already open and index 0's state is what SyncRenderState
// pushed, so there is nothing to replay and nothing to restore.
if (count <= 1) {
g_passCount = 1;
return 1;
}
g_passCount = count;
return count;
}
void ApplyViewportRoutingPass(Uint pass) {
using namespace ViewportRoutingImpl;
if (pass >= g_passCount || g_routedProgram == nullptr) {
return;
}
const RoutingPass& entry = g_passes[pass];
g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(),
entry.viewport.w());
g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(),
entry.scissorBox.w());
// ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a
// per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has
// no per-viewport scissor toggle either and has to widen a disabled index's rectangle to
// the whole framebuffer instead); here the rectangle stays honest.
entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y());
g_routedProgram->SetViewportPassMask(entry.indexMask);
}
void EndViewportRoutingPasses(Uint passCount) {
using namespace ViewportRoutingImpl;
if (passCount <= 1) {
// Nothing was pushed and the mask is already open; leaving the shadow alone here is
// what keeps a non-routing draw at exactly its previous cost.
g_routedProgram = nullptr;
return;
}
if (g_routedProgram != nullptr) {
// Any draw that reaches the driver without going through a replay - an internal blit,
// or a path this emulation has not been taught about - must not inherit the last
// pass's mask and paint nothing.
g_routedProgram->SetViewportPassMask(kAllViewportsMask);
}
g_routedProgram = nullptr;
g_passCount = 0;
// The viewport, scissor, scissor-test enable and depth range now on the ES context belong
// to the last replay pass, and the shadow SyncRenderState diffs against does not know it.
// A full resync is the honest repair and costs one state push on the next draw, which
// only a viewport-routing workload ever pays.
RenderStateImpl::InvalidateSyncedRenderState();
}
static Bool SupportsNativeIndirectDraws() {
return g_GLESCapabilities.SupportsDrawIndirect;
}
@@ -3317,7 +3530,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
SetCurrentBaseInstance(cmd.baseInstance);
SetCurrentBaseVertex(cmd.baseVertex);
}
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
});
}
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
@@ -3330,9 +3545,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
SetCurrentBaseInstance(cmd.baseInstance);
SetCurrentBaseVertex(cmd.baseVertex);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
});
}
}
SetCurrentDrawID(0);
@@ -3371,7 +3588,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
SetCurrentBaseInstance(cmd.baseInstance);
}
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
});
}
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
@@ -3382,9 +3601,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount));
});
}
}
SetCurrentDrawID(0);
@@ -3610,7 +3831,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type);
g_GLESFuncs.glDrawElements(mode, count, type, indices);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, count, type, indices);
});
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
@@ -3626,7 +3849,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first, count);
}
}
g_GLESFuncs.glDrawArrays(mode, first, count);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawArrays(mode, first, count);
});
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
@@ -3637,7 +3862,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
PrepareForDraw(syncBit);
CheckPrimitiveRestartSupported(type);
SetCurrentBaseVertex(basevertex);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
});
SetCurrentBaseVertex(0);
}
@@ -3663,7 +3890,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
});
}
if (feedDrawID) SetCurrentDrawID(0);
}
@@ -3900,14 +4129,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
SetCurrentBaseVertex(basevertex);
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
});
SetCurrentBaseVertex(0);
}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
PrepareForDraw(syncBit);
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
});
}
// True when the driver will apply baseInstance to the vertex fetch itself, in which case the
@@ -3930,12 +4163,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
PrepareForDraw(syncBit);
SetCurrentBaseInstance(baseinstance);
SetCurrentBaseVertex(basevertex);
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
basevertex, baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
}
ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
basevertex, baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
}
});
SetCurrentBaseVertex(0);
SetCurrentBaseInstance(0);
}
@@ -3945,7 +4180,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
SetCurrentBaseVertex(basevertex);
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
});
SetCurrentBaseVertex(0);
}
@@ -3955,19 +4192,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
SetCurrentBaseInstance(baseinstance);
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
}
ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
baseinstance);
} else {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
}
});
SetCurrentBaseInstance(0);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
});
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
@@ -3999,18 +4240,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
SetCurrentBaseInstance(baseinstance);
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
} else {
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
}
ForEachViewportRoutingPass([&] {
if (UseNativeBaseInstance()) {
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
} else {
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
}
});
SetCurrentBaseInstance(0);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
});
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
@@ -7466,9 +7711,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
// scratch framebuffer, so the frontend's READ binding describes a different image entirely -
// consulting it there would both miss real widenings and corrupt readbacks of ordinary
// textures taken while some unrelated widened attachment happened to be bound.
// The image-format widening's READ half, for the seven normalized formats whose carrier holds
// their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see
// TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those
// right: the attachment is an integer one while the application's format is normalized, so the
// class check below would refuse the read outright, and a repack that got past it would hand
// back 65535.0 where GL owes 1.0.
//
// Inactive (ChannelMax all zero) for every other read, which is all but a handful.
struct NormalizedImageCarrierRead {
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
Bool SignedNormalized = false;
Bool Active() const { return ChannelMax[0] != 0u; }
};
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
GLenum type, void* pixels, Bool honorPackImageParams,
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha) {
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha,
const NormalizedImageCarrierRead& normalizedCarrier = {}) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) {
return false;
@@ -7491,7 +7752,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
const GLenum attachmentComponentType = QueryReadAttachmentComponentType();
const Bool integerAttachment =
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
if (mapping.isInteger != integerAttachment) {
// A normalized image carrier is EXACTLY the case where the two disagree on purpose, and
// it is the caller - which knows the TEXTURE being read, not just the attachment - that
// says so. An integer client format through such a carrier is not a shape GL can ask for
// (the frontend format is normalized), so it is refused here rather than converted.
if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) {
MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a "
"normalized client format; skipping",
MG_Util::ConvertGLEnumToString(format).c_str());
return true;
}
if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) {
MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
MG_Util::ConvertGLEnumToString(format).c_str());
return true;
@@ -7511,7 +7782,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
WideReadCandidate candidates[4];
Int candidateCount = 0;
if (mapping.isInteger) {
if (normalizedCarrier.Active()) {
// The storage IS an integer texture, whatever the application's format says, so the
// only read that can answer is the integer one. The codes it hands back are turned
// into the floats the client asked for below.
candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT};
} else if (mapping.isInteger) {
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && IsIntegerReadFormat(implFormat) &&
(implType == GL_INT || implType == GL_UNSIGNED_INT)) {
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
@@ -7573,6 +7849,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
if (normalizedCarrier.Active()) {
// GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same
// denominators, so a texel an imageStore wrote and a texel the upload seeded read back
// identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with
// the signed code recovered from the low sixteen bits of the unsigned channel.
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(floatWide.data());
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
for (SizeT channel = 0; channel < 4; ++channel) {
const Uint32 code = src[i * 4 + channel];
const auto denominator = static_cast<Float>(normalizedCarrier.ChannelMax[channel]);
if (normalizedCarrier.SignedNormalized) {
const auto signedCode = static_cast<Int16>(static_cast<Uint16>(code));
dst[i * 4 + channel] =
std::max(static_cast<Float>(signedCode) / denominator, -1.0f);
} else {
dst[i * 4 + channel] = static_cast<Float>(code) / denominator;
}
}
}
wide = Move(floatWide);
wideType = GL_FLOAT;
readChannels = 4;
}
if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
// Unpack the packed words into a float wide buffer (full 10-bit precision on e.g.
// GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV).
@@ -8220,6 +8522,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GL_READ_FRAMEBUFFER, so the widening question has to be asked of the texture.
const Bool forceOpaqueAlpha =
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
// An image-bindable texture in one of the seven normalized formats has its ES storage
// in a GL_RGBA16UI, holding the format's own channel CODES. glGetTexImage still owes
// the application the NORMALIZED value, so the conversion has to be undone here - and
// it can only be asked of the TEXTURE, which is why it is not derived from the
// attachment the scratch framebuffer happens to hold.
NormalizedImageCarrierRead normalizedCarrier;
if (const auto imageWidening =
TextureImpl::GetImageBindableStorageWidening(textureObject->GetFormat());
imageWidening && imageWidening.CarriesNormalizedCodes() &&
(*backendTextureSlot)->RequiresImageBindableStorage()) {
for (SizeT channel = 0; channel < 4; ++channel) {
normalizedCarrier.ChannelMax[channel] = imageWidening.ChannelMax[channel];
}
normalizedCarrier.SignedNormalized = imageWidening.SignedNormalized;
}
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
// them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
@@ -8329,7 +8646,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
/*honorPackImageParams=*/false,
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) {
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha,
normalizedCarrier)) {
allSlicesRead = false;
break;
}
@@ -8352,7 +8670,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
applyPackImageParams,
/*applyFixedPointReadClamp=*/false,
forceOpaqueAlpha)) {
forceOpaqueAlpha, normalizedCarrier)) {
MGLOG_D("GetTexImage: finished via client-format conversion");
return;
}
+529 -26
View File
@@ -29,7 +29,9 @@
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <mutex>
#include <cstring>
@@ -47,6 +49,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
// See the block comment on ForEachViewportRoutingPass in Managers.h. Auto is ON, including on
// a driver that advertises GL_OES_viewport_array: that extension gives the shader a name, not
// the driver fifteen more rectangles to rasterize against, and nothing in MobileGL has ever
// programmed the indexed state it would need.
Bool ViewportArrayEmulationEnabled() {
return MG_Config::Features.ViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
}
Bool g_anyProgramRoutesViewportIndex = false;
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
// points. Those slots are taken from the TOP of the range downwards - below the one
@@ -300,6 +312,126 @@ namespace MobileGL::MG_Backend::DirectGLES {
return source;
}
// ---- gl_ViewportIndex routing emulation, ESSL half ---------------------------------------
//
// LowerViewportIndexPass has already turned the BuiltIn ViewportIndex OUTPUT into a plain
// Private global, so SPIRV-Cross printed `int mg_ViewportIndex;` at file scope and the stage
// still stores the index the application asked for - it just goes nowhere. The two passes
// below give it somewhere to go WITHOUT naming a builtin the language does not have: the
// producing stage's global becomes an ordinary flat varying, and the fragment stage gets a
// gate that discards every fragment whose primitive routed to a viewport the current replay
// pass is not drawing. DirectGLES.cpp's ForEachViewportRoutingPass is the other half - it
// re-issues the draw once per distinct viewport state with the real
// glViewport/glScissor/glDepthRangef pushed for it and this uniform set to the set of
// indices that state serves.
//
// FLAT is semantics, not performance: GL takes a primitive's viewport index from its
// PROVOKING VERTEX, which is exactly what flat interpolation delivers, so a primitive whose
// vertices carry different indices routes the way the spec says with no extra machinery.
//
// NO layout(location = N) on either side, deliberately. The two stages are transpiled
// independently and neither can see the other's location assignment: the producing stage
// knows its own outputs, the fragment stage only the subset it consumes, and a number derived
// from either can disagree with the other. Leaving both unqualified hands the assignment to
// the driver's linker, which then matches them BY NAME - the ordinary GLSL rule, and the only
// one that needs no cross-stage channel. The cost is one varying slot, which a program
// already at GL_MAX_VARYING_VECTORS cannot spare.
constexpr const char* VIEWPORT_INDEX_VARYING_NAME = "mg_ViewportIndex";
constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask";
constexpr const char* VIEWPORT_GATED_ENTRY_POINT_NAME = "mg_ViewportGatedMain";
constexpr const char* ESSL_ENTRY_POINT_SIGNATURE = "void main()";
static_assert(RenderStateParameters::MAX_VIEWPORTS == 16,
"the fragment gate below spells the index clamp as `& 15` and the pass mask as a "
"16-bit int; both follow MAX_VIEWPORTS and have to be respelled with it");
// Producing stage (vertex / tessellation evaluation / geometry - the three GL lets write the
// builtin). Returns whether the demoted global was found and promoted, which is also the
// answer to "does this program route viewports at all".
Bool PromoteViewportIndexGlobalToVarying(String& source) {
// The same shape PromoteDrawParameterGlobalsToUniforms matches, and for the same reason:
// SPIRV-Cross prints the demoted global with or without a precision qualifier depending
// on what the module carried. Only a declaration that starts its own line may be
// rewritten - `mg_ViewportIndex = gl_InvocationID;` in the body contains the name too and
// has to be left exactly as it is.
const String declared = String(VIEWPORT_INDEX_VARYING_NAME) + ";";
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
const String declaration = String(declPrefix) + declared;
const SizeT pos = source.find(declaration);
if (pos == String::npos) {
continue;
}
// Column 0 of its own line is what separates the declaration from the tail of any
// other declaration or expression that ends in the same name.
if (pos != 0 && source[pos - 1] != '\n') {
continue;
}
source.replace(pos, declaration.size(),
String("flat out highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";");
return true;
}
return false;
}
// Fragment stage. Returns false when the stage has no entry point to gate onto, which the
// caller reports: the program still links and still renders, it just renders every index
// with the first replay pass's state - i.e. it degrades to the pre-emulation behaviour
// rather than to a black screen.
Bool InjectViewportIndexPassGate(String& source) {
// Built beside the input and swapped in only on success, so a stage this pass declines
// reaches the driver exactly as it arrived rather than half-rewritten.
// A fragment stage that READS gl_ViewportIndex has no ESSL spelling for it either -
// LowerViewportIndexPass deliberately demotes only OUTPUTS, because a demoted INPUT would
// answer from an undefined Private global. Now that the routing varying exists and
// carries the real per-primitive value, that read has somewhere honest to go.
String gated = ReplaceIdentifier(source, "gl_ViewportIndex", VIEWPORT_INDEX_VARYING_NAME);
const SizeT entryPos = gated.find(ESSL_ENTRY_POINT_SIGNATURE);
if (entryPos == String::npos) {
return false;
}
// Declarations go immediately before the entry point rather than after #version: that
// position is already past every #extension directive (which must precede any other
// token) and past everything the body can name, so it can invalidate neither.
//
// Renaming the entry point rather than splicing a prologue into its body keeps the
// application's code byte-identical, including an early `return`.
String preamble = String("flat in highp int ") + VIEWPORT_INDEX_VARYING_NAME + ";\n";
preamble += String("uniform highp int ") + VIEWPORT_PASS_MASK_UNIFORM_NAME + ";\n";
preamble += String("void ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "()";
gated.replace(entryPos, std::strlen(ESSL_ENTRY_POINT_SIGNATURE), preamble);
// `& 15` clamps the shift operand into range for MAX_VIEWPORTS = 16. GL leaves an index
// outside [0, MAX_VIEWPORTS) undefined, but an ESSL shift by >= 32 is undefined in a way
// that can take the whole draw with it, so the emulation picks a defined answer instead.
//
// The mask, not an equality test against a pass number: viewport indices whose whole
// state tuple is identical share ONE replay pass (see BeginViewportRoutingPasses), and
// the overwhelmingly common case - every index still holding what glViewport broadcast -
// is then a single pass with every bit set, i.e. a gate that discards nothing and a draw
// that is issued exactly once.
//
// PERFORMANCE NOTE: a fragment shader containing `discard` cannot take the early-Z fast
// path on a tiler, so a routed draw pays late-Z on top of its N replay passes. Accepted
// deliberately: this runs only for a program that writes gl_ViewportIndex, and that is
// why the gate is injected per program rather than into every fragment shader.
gated += "\n";
gated += String(ESSL_ENTRY_POINT_SIGNATURE) + "\n";
gated += "{\n";
gated += String(" if (((") + VIEWPORT_PASS_MASK_UNIFORM_NAME + " >> (" +
VIEWPORT_INDEX_VARYING_NAME + " & 15)) & 1) == 0)\n";
gated += " {\n";
gated += " discard;\n";
gated += " }\n";
gated += " else\n";
gated += " {\n";
gated += String(" ") + VIEWPORT_GATED_ENTRY_POINT_NAME + "();\n";
gated += " }\n";
gated += "}\n";
source = std::move(gated);
return true;
}
// The transpile pipeline invents image binding numbers: when the GL source declares
// an image uniform without layout(binding), glslang auto-assigns one (desktop GL
// allows that and lets the app pick the unit with glUniform1i, which ES forbids on
@@ -2250,8 +2382,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
if (m_bufferImageSplitViewId != 0) {
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
}
}
m_backendTextureId = 0;
m_bufferImageSplitViewId = 0;
}
void BackendTextureObject::Bind(GLenum target, Uint unit) {
@@ -2276,12 +2412,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_backendTextureId;
}
void BackendTextureObject::RequireImageBindableStorage() {
void BackendTextureObject::RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (m_imageBindableStorageRequired) {
return;
}
m_imageBindableStorageRequired = true;
m_isInitialized = false;
// Every level this object has ALREADY uploaded has to be replayed, because the
// regeneration this transition schedules re-mints the storage in the image carrier and
// only uploads levels the shadow still calls dirty - which, for a texture that was
// synced before its first glBindImageTexture, is none of them. The new storage would
// come out ALLOCATED AND EMPTY, and every texel the application defined before that
// bind would be gone: the shader reads zeroes and the shadow still holds the data, so
// glGetTexImage (which falls back to the shadow) keeps answering correctly and only
// the image loads are wrong. Reached whenever anything syncs the texture first - a
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
// long: the scenario that binds the image immediately after uploading never sees it.
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
}
}
}
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
// and a widened texture's sampled view has to answer the channels the logical format
@@ -2584,7 +2742,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// runs after any type conversion (which keeps the component count) has already happened.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum uploadType,
Vector<Uint8>& widenedData, Bool integerData) {
Vector<Uint8>& widenedData, Bool integerData,
Uint32 alphaOneCodeOverride) {
Uint8 oneBits[8] = {};
SizeT componentSize = 0;
// One and two source components as well as three: the image-format widening carries
@@ -2596,6 +2755,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
return data;
}
// ...except where the carrier holds CODES of a normalized value (GL_R16 in a
// GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that
// type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated
// 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose
// saturated code is 0x7FFF. The caller passes the channel's own maximum instead.
// Written through a value of the component's own width rather than as the low
// `componentSize` bytes of the Uint32, so the encoding does not turn on the host's
// byte order.
if (alphaOneCodeOverride != 0u) {
if (componentSize == sizeof(Uint16)) {
const auto one = static_cast<Uint16>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
} else if (componentSize == sizeof(Uint32)) {
Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride));
} else if (componentSize == sizeof(Uint8)) {
const auto one = static_cast<Uint8>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
}
}
const SizeT srcTexelBytes = componentSize * componentCount;
// Sized from the level, never from the source: the driver reads a full
@@ -2706,25 +2884,152 @@ namespace MobileGL::MG_Backend::DirectGLES {
widenedData, IsIntegerWidenableFormat(format));
}
// One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are
// e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to
// read and the exponent bias is the 15 a 5-bit exponent always carries.
static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) {
const Uint32 mantissaScale = 1u << mantissaBits;
const Uint32 mantissa = bits & (mantissaScale - 1u);
const Uint32 exponent = bits >> mantissaBits;
if (exponent == 0u) {
// Subnormal, and zero with it: no implied leading 1, and the exponent is the
// smallest NORMAL one rather than the encoded 0.
return std::ldexp(static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale), -14);
}
if (exponent == 31u) {
return mantissa == 0u ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return std::ldexp(1.0f + static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale),
static_cast<Int>(exponent) - 15);
}
// The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F
// carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha
// channel, which is the same constant the shader-side mask writes, so a texel this
// function produced and a texel an imageStore produced are indistinguishable.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is:
// the driver reads a full width*height*depth*4 floats for the transfer it was handed.
static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data,
SizeT byteSize, Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Float), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Float*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Float rgb[3] = {0.0f, 0.0f, 0.0f};
if (i < copyTexelCount) {
Uint32 packed = 0;
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u);
rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u);
rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u);
}
dst[0] = rgb[0];
dst[1] = rgb[1];
dst[2] = rgb[2];
dst[3] = 1.0f;
}
return widenedData.data();
}
// The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its
// GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST
// component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19,
// blue 20-29 and alpha 30-31.
//
// The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an
// rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side
// unpack divides out. Neither is scaled here - the carrier holds the format's own bits.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the
// driver reads a full width*height*depth*4 shorts for the transfer it was handed.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data,
SizeT byteSize, Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Uint16), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Uint32 packed = 0;
if (i < copyTexelCount) {
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
}
dst[0] = static_cast<Uint16>(packed & 0x3FFu);
dst[1] = static_cast<Uint16>((packed >> 10u) & 0x3FFu);
dst[2] = static_cast<Uint16>((packed >> 20u) & 0x3FFu);
dst[3] = static_cast<Uint16>((packed >> 30u) & 0x3u);
}
return widenedData.data();
}
// The transfer half of the image-format widening: an image-bindable texture whose ES
// storage was widened to a core carrier is described to the driver as a four-component
// transfer, so its one- or two-component client data has to be repacked the same way the
// three-channel colour-renderable widening repacks its own.
// transfer, so its narrower client data has to be repacked the same way the three-channel
// colour-renderable widening repacks its own.
//
// Three shapes, because the carriers come in three kinds. Most of them keep the frontend
// format's component TYPE and only add channels, so padding the shadow out to four
// components is the whole conversion. The two PACKED formats do not: their shadow is one
// 32-bit word per texel, so the word has to be split - into four floats for
// r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such
// a word as components of the carrier's type - what the repack below would do - takes
// twelve or sixteen bytes from a four-byte texel and shears the level, which is what the
// allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel
// the upload got wrong).
//
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
// no-op by construction: none of the seventeen widened formats is a three-channel one
// (GetWidenableClientComponentCount reports 0 for every one of them), and the SNORM
// shadow-to-float conversion only fires for a GL_FLOAT transfer type, which the widened
// triple never picks for the two SNORM8 formats. So the shadow reaches this untouched and
// one repack is all that runs.
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
// reports a count for, and the SNORM shadow-to-float conversion only fires for a GL_FLOAT
// transfer type, which the widened triple never picks for the two SNORM8 formats. So the
// shadow reaches this untouched and one conversion is all that runs.
static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening,
const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData) {
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels >= 4) {
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
return data;
}
switch (widening.SourceEncoding) {
case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f:
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev:
return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::Components:
break;
}
if (widening.SourceChannels == 4) {
return data;
}
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
widenedData, widening.IntegerData);
widenedData, widening.IntegerData,
widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u);
}
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
@@ -3521,6 +3826,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, TextureTarget::TextureBuffer);
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
// own to widen, but the VIEW its format describes can be re-described one
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
// the format the application asked for (see GetImageBindableBufferSplitFormat).
//
// The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one.
// Re-describing the application's own texture also re-describes what a
// samplerBuffer reading it sees, and the sampler side is not subscript-rewritten -
// so texelFetch(s, i) started returning component 2i of the base view instead of
// texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the
// IMAGE binding needs the split, so only the image binding's name carries it.
const GLenum bufferImageSplitFormat =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableBufferSplitFormat(textureBufferObject->GetFormat())
: GL_UNKNOWN_MGL;
if (needsRegeneration) {
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
@@ -3576,6 +3898,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
func, file, line, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
backendId, MG_Util::ConvertGLEnumToString(err).c_str());
});
// The image half of the SPLIT, on its own name over the same buffer. Minted
// lazily - only a texture that is both image-bound AND holds a format with no
// ESSL image spelling ever gets one - and re-pointed here, in the same
// regeneration gate as the view above, so the two never describe different
// buffers or different windows of one.
if (bufferImageSplitFormat != GL_UNKNOWN_MGL) {
if (m_bufferImageSplitViewId == 0) {
g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId);
}
if (m_bufferImageSplitViewId == 0) {
MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; "
"its image binding will read the unsplit view.",
stateTextureObject->GetExternalIndex());
} else {
g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId);
if (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
}
// The raw bind above went behind Bind()'s shadow, which tracks objects
// rather than names: leaving it claiming THIS object is bound would
// make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split
// view bound in the application texture's place.
g_boundTexturesCache[g_activeTextureUnit][static_cast<SizeT>(
TextureTarget::TextureBuffer)] = nullptr;
}
}
}
break;
}
@@ -5150,12 +5503,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// The GL internal format a glslang layout format names, for the seventeen non-core
// formats WidenImageFormatsForEssl carries exactly plus nothing else: the only
// The GL internal format a glslang layout format names, for the eighteen non-core
// formats WidenImageFormatsForEssl carries losslessly plus nothing else: the only
// question asked of it is "does this DECLARED format widen", and answering 0 for
// everything else is the same "no" a non-widenable format gets. Kept as its own
// switch rather than routed through the frontend's enum converters because a
// TLayoutFormat is a glslang value and the reflection snapshot stores it raw.
//
// IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms
// the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the
// pass would carry but this switch answers 0 for never gets the chance: the module
// reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the
// only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has
// one. That is exactly what r11f_g11f_b10f did until it was added here.
Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRg32f: return 0x8230; // GL_RG32F
@@ -5175,6 +5535,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
case glslang::ElfR16ui: return 0x8234; // GL_R16UI
case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI
case glslang::ElfR8ui: return 0x8232; // GL_R8UI
// Not a channel widening but a lossless re-encoding into rgba16f - the one entry
// here whose carrier has a different per-channel layout. See
// WidenImageFormatsPass.h.
case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F
// 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same
// channel count, every value representable. Only the transfer is re-encoded.
case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI
// The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES.
// These are the entries whose carrier changes the shader-visible type as well as
// the qualifier (image2D becomes uimage2D), so every access through them is
// wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h.
case glslang::ElfRgba16: return 0x805B; // GL_RGBA16
case glslang::ElfRg16: return 0x822C; // GL_RG16
case glslang::ElfR16: return 0x822A; // GL_R16
case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2
case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM
case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM
case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM
default:
return 0;
}
@@ -5235,11 +5613,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// spelling still has to become legal ESSL somehow.
const auto declaredFormat = static_cast<glslang::TLayoutFormat>(type.layoutFormat);
if (!IsCoreEsslLayoutFormat(declaredFormat)) {
// Seventeen of the twenty-six non-core formats are re-declared in the core
// format that carries them exactly, with every access masked back to the
// channels GL says they have (WidenImageFormatsForEssl, and the matching
// storage/bind widening in TextureImpl). Those need neither the extension
// nor the diagnostic: there IS a legal spelling for them now.
// Eighteen of the twenty-six non-core formats are re-declared in a core
// format that carries them losslessly, with every access masked back to
// the channels GL says they have (WidenImageFormatsForEssl, and the
// matching storage/bind widening in TextureImpl). Those need neither the
// extension nor the diagnostic: there IS a legal spelling for them now.
if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) {
inputs.declaresWidenableImageFormat = true;
} else {
@@ -5493,7 +5871,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// keep the two in step.
const Int advertisedMaxSamples =
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
// by default (MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
// reason to keep the builtin: it only ever gave the SHADER a compilable name, while
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
// rasterized every index as index 0 exactly like a driver without it. Lowering here
// is what lets the ESSL passes downstream turn the builtin into the flat varying the
// replay gates on.
//
// Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the
// geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation
// evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass
// declines anyway, and a compute stage has none - so arming those two only ever
// bought them the shared probe's BuildModule for nothing.
const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER ||
glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER;
const Bool viewportLoweringArmed =
stageCanWriteViewportIndex &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
const Bool sampleClampArmed =
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
@@ -5517,11 +5914,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
!loweredViewportSpirv.empty()) {
effectiveSpirv = &loweredViewportSpirv;
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
"global; every invocation renders into viewport 0.",
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core "
"spelling for. The builtin was demoted to a plain global; %s.",
m_backendProgramId,
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
ViewportArrayEmulationEnabled()
? "the ESSL passes below promote it to a routing varying"
: "every invocation renders into viewport 0");
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
@@ -5865,6 +6264,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop,
outAtomicCounterGlBindings);
// `layout(index = 0)` is the GL default spelled out loud, and GLSL ES has no such
// qualifier in core - a stage that prints it is refused with "index layout
// qualifier requires EXT_blend_func_extended" and the whole program then draws
// nothing. Drop the decoration when it carries the default; a REAL dual-source
// index (1) is left alone, because that one genuinely needs the extension and the
// driver has to see it. Fragment stage only: no other stage can carry it.
if (glShaderType == GL_FRAGMENT_SHADER) {
spvcSession.DropDefaultFragmentOutputColorIndex();
}
// `readonly writeonly` together says the buffer variable can only be asked its
// .length(), which the frontend has already enforced - so the pair is inert, and
// printing it is not. Mesa's ES compiler refuses a block spelled that way and the
// stage never reaches the program.
spvcSession.RelaxReadWriteExclusiveStorageBuffers();
const char* result = nullptr;
spvcSession.Compile(&result);
@@ -6233,7 +6648,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
// The stage order the loop below walks, with every FRAGMENT stage moved to the end.
// The viewport-routing gate is the reason: whether a fragment stage needs one is a
// question about the OTHER stages ("does any of them still write gl_ViewportIndex?"),
// and the honest, free answer to it is the promotion the producing stage's own text
// pass just performed. Answering it any other way costs a BuildModule per
// pre-rasterization stage of every program - the parse the shared SpirvGateFeatures
// probe exists to avoid. Nothing else in the loop is order-sensitive: the two
// passthrough-tessellation sources it captures are a vertex and an evaluation stage,
// and the three sets it accumulates are unions.
Vector<SizeT> stageOrder;
stageOrder.reserve(linkedStages.size());
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index);
}
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index);
}
// Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text
// pass turned into a varying; read by the fragment stage to decide whether to inject
// the gate that consumes it.
Bool programRoutesViewportIndex = false;
// No fragment stage, no gate - and without a gate the promotion below would only add
// an output nothing can read. That is not merely useless: in a separable program
// pipeline the fragment stage lives in a DIFFERENT program, which never saw this
// build and cannot be given a gate, so promoting there would hang an unmatched
// varying off a program to buy nothing. Both cases keep the pre-emulation behaviour,
// which is what a program with no fragment stage had anyway.
const Bool programHasFragmentStage =
std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) !=
linkedStages.end();
const Bool viewportEmulationForThisProgram =
ViewportArrayEmulationEnabled() && programHasFragmentStage;
for (const SizeT index : stageOrder) {
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]);
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
@@ -6272,7 +6720,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
esslKeyInputs.spirv = &spirvCode;
esslKeyInputs.shaderType = glShaderType;
esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray;
// The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it.
// Duplicated rather than shared because the two live on opposite sides of the
// memo boundary - and a key that disagrees with the pass it is keying is the one
// failure mode of this cache that renders wrong pixels instead of being slow.
esslKeyInputs.viewportIndexLoweringArmed =
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER) &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
esslKeyInputs.supportsNoperspectiveInterpolation =
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
esslKeyInputs.supportsExtendedImageFormats =
@@ -6427,8 +6882,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// name; a driver without the extension took the LowerViewportIndexPass fallback
// above and its source no longer names the builtin at all, so the two are mutually
// exclusive by construction. Read `source` BEFORE it is moved from.
const Bool needsViewportArrayExtension = g_GLESCapabilities.SupportsViewportArray &&
source.find("gl_ViewportIndex") != String::npos;
// The routing emulation is the third way this can be reached and the only one
// that needs no directive: it renames the fragment stage's read onto the varying
// the producing stage now writes, a few passes below.
const Bool needsViewportArrayExtension =
g_GLESCapabilities.SupportsViewportArray &&
!(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) &&
source.find("gl_ViewportIndex") != String::npos;
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
@@ -6489,6 +6949,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe());
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
// The two halves of the gl_ViewportIndex routing emulation, next to the draw-
// parameter promotion because they are the same shape: a builtin ESSL cannot
// spell, demoted to a plain global by a SPIR-V pass, given a real interface here.
// BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to
// the top land above the declarations these inject; AFTER
// ForceFlatIntegerVaryings, which matches only declarations carrying a
// layout(...) qualifier and so cannot touch either of them.
if (viewportEmulationForThisProgram) {
if (glShaderType == GL_FRAGMENT_SHADER) {
if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) {
// MGLOG_E, unlatched, like the transpile- and compile-failure
// diagnostics around it: the program still links and still draws, so
// nothing else in the process will ever say that its viewport routing
// silently collapsed back to one rectangle.
MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no "
"entry point to gate, so the routing cannot be emulated: every "
"index will rasterize against viewport 0. State program ID: %u.",
m_backendProgramId, stateProgramObject->GetExternalIndex());
}
} else if (PromoteViewportIndexGlobalToVarying(source)) {
programRoutesViewportIndex = true;
}
}
source = ForceSupporterOutput(source);
source = ClampNormFallbackOutputs(std::move(source), glShaderType,
m_snormFallbackClampOutputMask,
@@ -6691,6 +7174,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
BASE_VERTEX_UNIFORM_NAME);
m_baseInstanceWordIndexUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
// Asked of the DRIVER rather than remembered from the injection, deliberately: the
// gate is only real if the uniform survived compilation and linking, and this is the
// one question whose answer covers both. A gate the driver optimized away would
// otherwise leave the draw path replaying passes whose mask reaches nothing, which
// renders every index's primitives in every pass.
m_viewportPassMaskUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME);
if (m_viewportPassMaskUniformLocation >= 0) {
// Sticky, and never cleared on a relink: it only ever short-circuits a per-draw
// check, so being late to go false costs a pointer compare and being late to go
// true would cost correctness.
g_anyProgramRoutesViewportIndex = true;
}
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
// SSBO blocks after compile); record it so draws bind the indirect buffer there.
m_indirectParamsBinding = -1;
@@ -6893,6 +7389,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast<GLint>(drawId));
}
void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const {
if (m_viewportPassMaskUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast<GLint>(indexMask));
}
} // namespace PrgramImpl
namespace SamplerImpl {
+149 -2
View File
@@ -21,6 +21,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
// The ESSL half of the gl_ViewportIndex routing emulation, in the order a program's stages
// meet it. Both are pure String -> String rewrites over what SPIRV-Cross emitted once
// LowerViewportIndexPass has demoted the builtin to the plain global `mg_ViewportIndex`.
//
// The producing stage's global becomes an ordinary flat varying; true when there was one to
// promote, which is also the answer to "does this program route viewports at all".
Bool PromoteViewportIndexGlobalToVarying(String& source);
// The fragment stage grows a matching flat input, the mg_ViewportPassMask uniform the draw
// path writes, and a wrapper entry point that discards every fragment whose primitive routed
// to an index the current replay pass is not drawing. False when the stage has no entry point
// to wrap, which leaves the program renderable but unrouted.
Bool InjectViewportIndexPassGate(String& source);
// Whether a vertex shader may declare a storage block at all, given what the host driver
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
// global purely so the decision can be tested without one.
@@ -113,6 +126,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
// link.
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
//
// GLES has ONE viewport, ONE scissor rectangle and ONE depth range; GL 4.1 has sixteen of
// each, selected per primitive by gl_ViewportIndex. There is no ES entry point to program the
// other fifteen with (GL_OES_viewport_array exists but Adreno 830 does not have it, verified
// three ways), so the only way to rasterize a primitive against index i's rectangle is to
// make index i's rectangle THE viewport for the duration of a draw - which means issuing the
// draw once per distinct viewport state and letting the fragment stage throw away the
// primitives that belong to the other indices (the gate Managers.cpp injects).
//
// Indices whose whole state tuple (viewport rectangle, scissor rectangle, scissor-test enable,
// depth range) is identical share ONE pass, so the overwhelmingly common case - every index
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
//
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
// restores the pre-emulation path as a negative control.
Bool ViewportArrayEmulationEnabled();
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
// true; it exists so that BeginViewportRoutingPasses - which runs on every draw of every
// workload - can answer with one static load in the case that matters, which is every
// application that has never heard of gl_ViewportIndex.
extern Bool g_anyProgramRoutesViewportIndex;
// Number of times the current draw has to be issued. Always >= 1, and exactly 1 - with no
// state touched - whenever the current program does not route viewports, whenever every
// configured index shares one state, and whenever replaying would multiply a side effect the
// fragment gate cannot undo (transform feedback, rasterizer discard). Also seeds the pass
// mask uniform for that single-pass case, so a gated fragment shader never runs against the
// zero every GLSL uniform starts at - which would discard the whole draw.
Uint BeginViewportRoutingPasses();
// Push pass `pass`'s viewport / scissor / scissor-test / depth range onto the ES context and
// set the gate mask to the indices it serves. Only called when the count above exceeds 1.
void ApplyViewportRoutingPass(Uint pass);
// Restore the gate mask and mark the render-state shadow dirty, so the next ordinary draw
// re-pushes index 0's state. Takes the count so it can do nothing at all in the common case.
void EndViewportRoutingPasses(Uint passCount);
// Issue one draw, replayed once per viewport-routing pass. Every application-visible draw
// entry point wraps its native glDraw* call in this; the internal blit and clear helpers
// deliberately do not, because they bind their own programs, which never route.
template <typename IssueDraw>
inline void ForEachViewportRoutingPass(IssueDraw&& issue) {
const Uint passCount = BeginViewportRoutingPasses();
for (Uint pass = 0; pass < passCount; ++pass) {
if (passCount > 1) {
ApplyViewportRoutingPass(pass);
}
issue();
}
EndViewportRoutingPasses(passCount);
}
template <typename StateObject, typename BackendObject>
class StateBackendObjectRegistry {
public:
@@ -680,9 +745,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
// scratch buffer and has to outlive the returned pointer.
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
// transfer type's own "ones" is.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
Bool integerData = false);
Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
// returned pointer.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData);
struct StateTextureBasicInfo { // Used for tracking texture state changes
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
@@ -717,10 +794,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void RequireImageBindableStorage();
// Marks the texture as one whose ES storage has to be image-bindable, which for a
// non-core image format means re-minting it in the widening's carrier. Takes the state
// object because the levels already uploaded have to be marked dirty again: the
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
void RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// Whether this texture's ES storage was minted in an image carrier rather than in the
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
// the application floats.
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const;
// The id to hand glBindImageTexture for a SPLIT buffer image, or 0 when this texture
// takes no split. See m_bufferImageSplitViewId.
Uint GetBufferImageSplitViewId() const { return m_bufferImageSplitViewId; }
// Aggregate first-level clean gate for the per-draw trio
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
@@ -757,6 +848,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RecreateBackendTexture();
Uint m_backendTextureId = 0;
// A SECOND buffer-texture name over the SAME buffer object, viewed in the split's
// single-channel base format, used only as the glBindImageTexture target.
//
// The split needs the view to say r32f where the application said rg32f, but a buffer
// texture that is image-bound may ALSO be read through a samplerBuffer - and the
// sampler side is not subscript-rewritten, so re-describing the application's own
// texture broke it: texelFetch(s, i) returned component 2i of the base view instead of
// texel i's pair. That is exactly and only
// KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess, which image-stores
// into a GL_RG32F buffer texture and then reads the same texture through both an
// imageBuffer and a samplerBuffer in one shader, comparing the two.
//
// Two names over one buffer cost nothing and alias exactly: a buffer texture owns no
// storage, so both views are the application's bytes, and the split's whole premise is
// that the two describe the same memory. The application's own name therefore keeps
// the format it asked for - rg32f IS a legal SAMPLED buffer-texture format in ES 3.2,
// it is only the IMAGE binding ES cannot spell - and the private name below carries
// the split the shader was rewritten against. 0 when this texture takes no split.
Uint m_bufferImageSplitViewId = 0;
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
@@ -1103,23 +1213,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i.
//
// ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated
// (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list
// used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable
// shortcut and was two bugs: an image uniform this says "no" to is one
// CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor
// widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not
// supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and
// assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY
// image - which ES 3.2 has in core, so it is not even an emulated target - hit both.
inline Bool IsImageUniformType(GLenum type) {
switch (type) {
case 0x904C: /*GL_IMAGE_1D*/
case 0x904D: /*GL_IMAGE_2D*/
case 0x904E: /*GL_IMAGE_3D*/
case 0x904F: /*GL_IMAGE_2D_RECT*/
case 0x9050: /*GL_IMAGE_CUBE*/
case 0x9051: /*GL_IMAGE_BUFFER*/
case 0x9052: /*GL_IMAGE_1D_ARRAY*/
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/
case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/
case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/
case 0x9057: /*GL_INT_IMAGE_1D*/
case 0x9058: /*GL_INT_IMAGE_2D*/
case 0x9059: /*GL_INT_IMAGE_3D*/
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
case 0x905B: /*GL_INT_IMAGE_CUBE*/
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/
case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/
case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/
case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/
case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
return true;
default:
return false;
@@ -1200,6 +1338,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
// uniform write, and only such a program needs the reset after one.
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
// Which viewport indices the next draw's fragments may keep, one bit each. Written
// once per replay pass; see ForEachViewportRoutingPass.
void SetViewportPassMask(Uint32 indexMask) const;
// True when this build injected the fragment-stage viewport gate, i.e. when a
// pre-rasterization stage routes by gl_ViewportIndex AND the fragment stage can act
// on it. The uniform is the honest test for both halves: it exists only where the
// gate was injected, and the gate is injected only where a stage routes.
Bool RoutesViewportIndex() const { return m_viewportPassMaskUniformLocation >= 0; }
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a
@@ -1316,6 +1462,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_drawIdUniformLocation = -1;
Int m_baseVertexUniformLocation = -1;
Int m_baseInstanceWordIndexUniformLocation = -1;
Int m_viewportPassMaskUniformLocation = -1;
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
+18 -8
View File
@@ -414,14 +414,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
});
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
});
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -442,8 +446,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
});
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -515,8 +521,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// driver sees none - but gl_BaseVertex still has to report the value the
// application passed for this sub-draw.
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
});
cursor += static_cast<SizeT>(count[i]);
}
if (feedDrawID) SetCurrentDrawID(0);
@@ -870,7 +878,9 @@ void main() {
if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
});
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
return;
}
+96 -8
View File
@@ -277,8 +277,62 @@ namespace MobileGL::MG_Backend::DirectGLES {
// its own; this call is only here to spell the transfer pair that describes it.
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
nullptr, &widening.Format, &widening.Type);
// The two carriers that are not channel widenings, whose transfer pair has to say so.
// Every other entry keeps the frontend format's own component type - a GL_RG16F shadow
// is halves and so is its GL_RGBA16F carrier, so padding the channels is the whole
// conversion. These two shadows are a PACKED 32-bit word per texel
// (TextureFormatProcessor::NormalizePixelFormat), and no ES driver accepts either
// packed type for the carrier's level, so the transfer names the carrier's own layout
// and PrepareImageWidenedUpload splits the word into it.
switch (internalFormat) {
case TextureInternalFormat::R11FG11FB10F:
// GL_UNSIGNED_INT_10F_11F_11F_REV -> GL_RGBA / GL_FLOAT, legal for GL_RGBA16F.
widening.Format = GL_RGBA;
widening.Type = GL_FLOAT;
widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f;
break;
case TextureInternalFormat::RGB10A2UI:
case TextureInternalFormat::RGB10A2:
// GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the
// GL_RGBA16UI carrier already asked for above; only the split is new. The two
// formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are,
// and what the shader divides them by is not the transfer's business.
widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev;
break;
default:
break;
}
// The seven normalized formats whose carrier holds CODES rather than values. Both
// halves of the transfer need to know: a missing alpha is padded with the saturated
// code rather than the integer 1, and glGetTexImage has to divide the codes back out.
bool signedNormalized = false;
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax,
signedNormalized)) {
for (SizeT channel = 0; channel < 4; ++channel) {
widening.ChannelMax[channel] = channelMax[channel];
}
widening.SignedNormalized = signedNormalized;
}
return widening;
}
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const auto base = static_cast<GLenum>(
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
if (base == 0) {
return GL_UNKNOWN_MGL;
}
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
// or none of them may, or the shader subscripts a view the buffer is not described as.
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
return GL_UNKNOWN_MGL;
}
return base;
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
@@ -942,11 +996,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
String arraySuffix; // "" or "[7]"
SizeT declStart = 0;
SizeT declLength = 0;
SizeT nameStart = 0; // the name token alone, for a rename that edits nothing else
SizeT nameLength = 0;
SizeT referenceCount = 0; // uses this pass recognized and accounted for
Bool loaded = false;
Bool stored = false;
Bool unknownUse = false;
Bool split = false;
// SPIRV-Cross already tagged this one readonly or writeonly, so it needs no
// qualifier repair - only the rename that keeps two stages from merging it.
Bool preTaggedReadonly = false;
Bool preTaggedWriteonly = false;
};
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
@@ -1283,10 +1343,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
const std::smatch& match = *it;
const String qualifiers = match[2].str();
// Already legal: SPIRV-Cross decided one way, leave it alone.
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
continue;
}
const Bool hasReadonly = ContainsIdentifier(qualifiers, "readonly");
const Bool hasWriteonly = ContainsIdentifier(qualifiers, "writeonly");
// Carrying BOTH is a spelling no per-stage access analysis produces (SPIRV-Cross
// clears one decoration or the other as soon as it sees a load or a store), so it
// came from the application and is identical in every stage. Nothing to do.
if (hasReadonly && hasWriteonly) continue;
Bool hasFormat = false;
Bool exemptFormat = false;
@@ -1295,10 +1357,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
hasFormat = true;
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
}
// No format qualifier at all is a different (and, in ES, unconditionally
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
if (!hasFormat || exemptFormat) continue;
// A declaration carrying neither qualifier is illegal ES unless its format is
// r32f/r32i/r32ui, and no format qualifier at all is a shape SPIRV-Cross refuses
// to emit for an ES target. Either way there is no repair to make - and no rename
// to make either, because a declaration with no access qualifier is spelled the
// same in every stage.
if (!hasReadonly && !hasWriteonly && (!hasFormat || exemptFormat)) continue;
ImageUniformDecl decl;
decl.layout = match[1].str();
@@ -1308,6 +1372,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
decl.declStart = static_cast<SizeT>(match.position(0));
decl.declLength = match[0].str().size();
decl.nameStart = static_cast<SizeT>(match.position(4));
decl.nameLength = match[4].str().size();
decl.preTaggedReadonly = hasReadonly;
decl.preTaggedWriteonly = hasWriteonly;
decls.push_back(Move(decl));
}
if (decls.empty()) {
@@ -1439,6 +1507,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already readonly/writeonly in the source, or r32f/r32i/r32ui, which need no
// qualifier - keep their names, and they are exactly the ones that already match
// across stages.
if (decl.preTaggedReadonly || decl.preTaggedWriteonly) {
// No repair: SPIRV-Cross already emitted a legal qualifier. But it derived
// that qualifier from THIS STAGE's accesses, so a uniform stored in one stage
// and loaded in another arrives here `writeonly` in one and `readonly` in the
// other under ONE name - precisely the same-name/mismatched-qualifier pair
// Adreno merges while silently discarding the writing stage's stores
// (advanced-memory-dependentInvocation; a raw-ES probe reproduces it with no
// MobileGL in the process, and renaming either half fixes it). Keyed on the
// qualifier for the same reason the repair below is: two stages that agree
// spell the same alias and stay merged, so no shader gains an image uniform.
const char* preTagPrefix =
decl.preTaggedReadonly ? IMAGE_READONLY_ALIAS_PREFIX : IMAGE_WRITEONLY_ALIAS_PREFIX;
decl.aliasName = MakeImageAliasName(preTagPrefix, decl.name, glslCode, takenNames);
takenNames.push_back(decl.aliasName);
// The name token alone: the qualifiers are already right, and re-emitting the
// whole declaration would only risk changing them.
edits.push_back({decl.nameStart, decl.nameLength, decl.aliasName});
continue;
}
const char* aliasPrefix = decl.loaded && decl.stored ? IMAGE_SPLIT_READ_ALIAS_PREFIX
: decl.stored ? IMAGE_WRITEONLY_ALIAS_PREFIX
: IMAGE_READONLY_ALIAS_PREFIX;
+66 -2
View File
@@ -102,6 +102,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
// from "alpha" to a channel count, which is its own change.
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
// shadow already holds SourceChannels components of exactly the carrier's own type, so
// padding it out to four is the whole conversion. The packed entries do not - their shadow
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
// type takes twelve or sixteen bytes out of four and shears the level.
enum class ImageWidenSourceEncoding : Uint8 {
Components = 0,
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
PackedFloat11f11f10f,
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
// only in what the codes MEAN, which is the shader's business and not the transfer's.
PackedInt2101010Rev,
};
struct ImageBindableStorageWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
@@ -113,10 +129,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
// RG8UI), so the carrier decides.
Bool IntegerData = false;
// What the upload has to do to the frontend shadow before it describes the level to
// the driver (PrepareImageWidenedUpload).
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
// has no image format of any width for and which a float carrier would requantise.
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
//
// Two things depend on it, both because the ES storage no longer shares the frontend
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
// one), and glGetTexImage divides the codes back out into the floats the application
// is still owed.
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
Bool SignedNormalized = false;
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl
@@ -392,8 +444,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
// catches this.
//
// The declarations this pass leaves untouched keep their names, and those are exactly the
// ones that already agree across stages.
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
// nothing but the identifier changed. Stages that agree still reach the same alias and
// stay merged, so this costs no shader an extra image uniform.
//
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
// readonly and writeonly (a spelling no access analysis produces, so it came from the
// application and is identical everywhere), and one carrying NEITHER, which is legal only
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
//
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
// guarantees a write through one image variable is visible to a read through a DIFFERENT
@@ -562,12 +562,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile);
}
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
// so an application that simply uses doubles needs nothing advertised. What the extension
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
// decision about all of it rather than about the shader path alone.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
@@ -965,26 +966,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Never, on any device, and no longer for the reason it used to be. It used to track
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
// The device feature the whole fp64 story hangs off. With it, a module keeps its
// OpCapability Float64 and real doubles reach the driver; without it the transpile
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
// exactly as it always has.
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
// exist in the module at all; a 64-bit vertex FETCH was already impossible
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
// bitcast it back.
//
// The shader half of that is gone: every 64-bit float is narrowed before any module
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
// input would be silent garbage. Reconstructing the value would mean decoding the
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
// dvec4 cannot even express within one attribute location.
// Re-coupling it does not work, and the reason is worth recording because it is not
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
// `in vec4` is not only legal but the common case
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
// narrowing OFF for every one of them and the attributes come back unfetched.
//
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
// already were on Espryt and on every real mobile device (Adreno and Mali both report
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
// ShaderCompiler::SanitizeAndOptimizeBinary.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -112,10 +112,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
VertexStreamConversion conversion = VertexStreamConversion::None;
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
// load-bearing rather than belt-and-braces: the narrowing is only correct because
// DemoteFloat64Pass already turned the shader's `dvec` input into a `vec`, and that
// pass runs precisely when the backend declares no 64-bit vertex support. With the
// flag set, a dvec3/dvec4 is declined by ToVkVertexFormat AND left 64-bit in the
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
// guarantees that is the flag being clear. It is clear on every backend today, and a
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
// module, so a float32 stream would be fed to a Float64 input.
const Bool narrowFloat64Arrays =
MG_Backend::pActiveBackendObject == nullptr ||
@@ -972,10 +972,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// The fetch half of the fp64 demotion the shader side already does unconditionally
// (DemoteFloat64Pass): the source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is
// deinterleaved into a tightly packed float32 stream rather than dropped. `normalized` is not
// consulted - GL ignores it for floating-point array types.
// The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by
// SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64
// vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. The
// source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is deinterleaved into a
// tightly packed float32 stream rather than dropped. `normalized` is not consulted - GL
// ignores it for floating-point array types.
static Bool ConvertFloat64VertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
+132 -31
View File
@@ -880,6 +880,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
// each float back to the queried type, and it undoes the same padding itself.
// Float matrices only, in both senses: a DOUBLE matrix never comes through here, whether its
// program was demoted (components are floats, the query is not) or kept its doubles (the
// column stride is a dvec4's, and the caller's converting branch already walks it component
// by component with the right one).
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
if (!ttype.isMatrix || ttype.isDouble) return false;
const Int columns = ttype.matrixCols;
@@ -892,11 +896,12 @@ namespace MobileGL::MG_Impl::GLImpl {
}
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
// everything except a float matrix, whose padded columns make it wider. The rule itself
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
// one and two copies of a layout rule is one too many.
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize) {
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
// everything except a matrix, whose padded columns make it wider, and a `double` on a
// program whose modules were demoted, where it is half. The rule itself lives on
// ProgramObject, because the pipeline composite's uniform refresh needs the same one and
// two copies of a layout rule is one too many.
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize, const Bool nativeFloat64) {
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize, nativeFloat64);
}
void GetUniform_State(GLuint program, GLint location, void* params) {
@@ -929,7 +934,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = (char*)programObject->MapUBO();
const auto& ttype = programObject->GetUniformTypeFacts(location);
const SizeT span = UniformStorageSpanInBytes(ttype, size);
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
@@ -939,9 +945,9 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
// Never more than the uniform actually occupies. `size` is the GL type size,
// which for a `double` uniform is twice its storage - every 64-bit float is
// narrowed before the module reaches a backend, so the slot holds floats. The
// typed entry points (glGetUniformdv and friends) go through
// which on a DEMOTED program is twice a `double` uniform's storage - its 64-bit
// floats were narrowed before the module reached a backend, so the slot holds
// floats. The typed entry points (glGetUniformdv and friends) go through
// GetUniformScalar_State, which converts component by component; this raw
// copy has no type to convert with, so it is bounded rather than converted.
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
@@ -983,7 +989,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = static_cast<char*>(programObject->MapUBO());
const auto& ttype = programObject->GetUniformTypeFacts(location);
const SizeT span = UniformStorageSpanInBytes(ttype, size);
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
@@ -995,28 +1002,38 @@ namespace MobileGL::MG_Impl::GLImpl {
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
}
// A double-precision uniform is the one case where the stored component type differs
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
// narrowed to 32 bits before the module reaches a backend
// A double-precision uniform is the one case where the stored component type can differ
// from the DECLARED one for a non-opaque uniform: on a DEMOTED program the shader's
// 64-bit floats were narrowed to 32 before the module reached the backend
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
// component, laid out exactly like the float-typed twin of this uniform - std140
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
// two components reinterpreted as one. Read component by component and let GL's
// two components reinterpreted as one. A program that KEPT its doubles stores real ones
// at the dvec4 column stride instead, so the width and the stride both move; everything
// else about this walk is the same. Read component by component either way and let GL's
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
// widens back to the queried type, having lost precision at the glUniform*d that
// stored it and not here.
// widens back to the queried type, having lost precision - where it lost any - at the
// glUniform*d that stored it and not here.
if (ttype.isDouble) {
const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
const Int rows = ttype.isMatrix ? ttype.matrixRows
: (ttype.isVector ? ttype.vectorSize : 1);
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
// tightly packed run and never reaches the stride at all.
const SizeT columnStride = 4 * sizeof(GLfloat);
// A non-matrix is one tightly packed run and never reaches the stride at all.
const SizeT columnStride =
MG_State::GLState::ProgramObject::UniformMatrixColumnStride(ttype, nativeFloat64);
const SizeT componentSize = nativeFloat64 ? sizeof(GLdouble) : sizeof(GLfloat);
for (Int column = 0; column < columns; ++column) {
for (Int row = 0; row < rows; ++row) {
GLfloat component = 0.0f;
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
sizeof(component));
GLdouble component = 0.0;
if (nativeFloat64) {
Memcpy(&component, pUBO + offset + column * columnStride + row * componentSize,
sizeof(GLdouble));
} else {
GLfloat narrow = 0.0f;
Memcpy(&narrow, pUBO + offset + column * columnStride + row * componentSize,
sizeof(narrow));
component = static_cast<GLdouble>(narrow);
}
if constexpr (std::is_integral_v<T>) {
// Rounded to the nearest integer and clamped into the queried type's
// range, so a negative double read through glGetUniformuiv is 0
@@ -1287,17 +1304,45 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
// transpile chain narrows every 64-bit float in the shader to 32 bits
// Whether the program a uniform write is about to land in stores 64-bit floats at their
// declared width. Answered off the PROGRAM, never off the live backend: it describes the
// modules that were actually built for it, and a backend with native fp64 still demotes a
// program whose vertex stage declares a Float64 input (see ProgramSpirvTask::GenerateSpirv).
// Nullptr - no current program, or a name that is not a program - answers false and lets the
// callee record the same error it always did.
Bool CurrentProgramUsesNativeFloat64() {
if (MG_State::pGLContext == nullptr) return false;
const auto& programObject = MG_State::pGLContext->GetProgramForUniform();
return programObject != nullptr && programObject->UsesNativeFloat64();
}
Bool NamedProgramUsesNativeFloat64(GLuint program) {
const auto& programObject = TryToGetProgramObject(program);
return programObject != nullptr && programObject->GetLinkStatus() && programObject->UsesNativeFloat64();
}
// glUniform*d / glUniformMatrix*dv. On a DEMOTED program neither needs a layout of its own:
// the transpile chain narrowed every 64-bit float in the shader to 32
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
// path is what keeps the two in step; a separate double-shaped layout here would write
// path is what keeps the two in step; a separate double-shaped layout there would write
// 8-byte components into 4-byte slots and silently address the wrong ones.
//
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
// The narrowing is the same static_cast the demoted shader's own arithmetic performs, so the
// value the shader reads is the value glUniform*d was given, at float precision.
//
// On a program that KEPT its doubles the reverse is true and for the same reason: its global
// UBO really does hold 8-byte components, so narrowing would leave a float bit pattern in the
// low half of a double slot - which is not a precision loss but a garbage value. The 64-bit
// values go through unchanged then, and the upload path is width-agnostic (it is templated on
// the component type and bounded by the uniform's own slot span).
//
// Note TryToGetProgramObject / GetProgramForUniform run TWICE on this path, once for the
// width question and once inside the call below. That is a lookup and a join on an entry
// point no shader pack uses; the alternative is duplicating both functions' whole validation
// sequence here, which is the thing that must not drift.
template <GLsizei ItemCount>
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
if (value == nullptr || count <= 0) {
@@ -1306,6 +1351,10 @@ namespace MobileGL::MG_Impl::GLImpl {
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
return;
}
if (location != -1 && CurrentProgramUsesNativeFloat64()) {
Uniformv_State<ItemCount>(location, count, value);
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
Uniformv_State<ItemCount>(location, count, narrowed.data());
@@ -1317,6 +1366,10 @@ namespace MobileGL::MG_Impl::GLImpl {
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
return;
}
if (location != -1 && NamedProgramUsesNativeFloat64(program)) {
ProgramUniformv_State<ItemCount>(program, location, count, value);
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
@@ -1368,15 +1421,63 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
// is then the one implementation both spellings share.
// glUniformMatrix*dv / glProgramUniformMatrix*dv on a program that KEPT its doubles. Same
// walk as UniformMatrixfv_Object down to the last branch, and deliberately a copy of it
// rather than a template over the component type: the two differ in exactly one number that
// is not derivable from the component type alone - std140 pads a double matrix's column out
// to a dvec4 (32 bytes) unless the column is a dvec2, which is already 16 - and folding that
// into the float version would put a per-call branch on the hot glUniformMatrix4fv path
// Minecraft calls thousands of times a frame for a case no shader pack ever takes.
template <typename Program>
void UniformMatrixdvNative_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows,
const String& ownerDescription) {
const SizeT columnStride = rows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
GLdouble column[4] = {};
for (GLint matrix = 0; matrix < count; ++matrix) {
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
if (!programObject.IsValidUniformLocation(location + matrix)) {
RecordInvalidUniformLocationError("glUniformMatrixdv", location + matrix, ownerDescription);
return;
}
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "glUniformMatrixdv",
"Opaque uniforms cannot be set with matrix Uniform calls."));
return;
}
const GLdouble* source = value + static_cast<SizeT>(matrix) * componentCount;
for (Int c = 0; c < columns; ++c) {
for (Int r = 0; r < rows; ++r) {
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
}
const SizeT byteOffset = static_cast<SizeT>(c) * columnStride;
switch (rows) {
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
}
}
}
}
// glUniformMatrix*dv / glProgramUniformMatrix*dv. On a DEMOTED program this narrows to the
// float form and hands it straight over: after DemoteFloat64Pass a `dmat4` uniform is a
// `mat4` in the shader and a mat4-shaped slot in the global UBO, columns padded to a vec4
// and all. Everything else about the call - transpose handling, the array-element walk, the
// opaque-uniform refusal - is then the one implementation both spellings share. A program
// that kept its doubles gets the same walk at double width and the wider column stride.
template <typename Program>
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows) {
if (value == nullptr || count <= 0) return;
if (programObject.UsesNativeFloat64()) {
UniformMatrixdvNative_Object(programObject, location, count, transpose, value, columns, rows,
"the current program object");
return;
}
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
@@ -6,27 +6,32 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
// Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
//
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
// shader: `double` compiles and runs everywhere, at float precision.
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
// type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
// profile") and the program never reaches the driver. On every such backend MobileGL narrows
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
// declining the shader: `double` compiles and runs everywhere, at float precision. Where the
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
// and the doubles reach the driver whole.
//
// The narrowing is only half a contract. The other half is the API side: the global UBO is
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values
// through the API and have the SHADER report what it saw.
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
// std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values through
// the API and have the SHADER report what it saw.
//
// What is deliberately NOT asserted: that the values are exact to double precision. They are
// not, and cannot be. Every expectation here is the float value of the double that was set,
// which is the whole point.
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
// float32, so the same number is correct in both regimes and the assertions test the LAYOUT
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
// that is not - 0.1 - and it names both answers explicitly.
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
@@ -153,6 +158,151 @@ void main() {
std::string m_buildLog;
};
// A SHADER STORAGE BLOCK that holds doubles is the one place the narrowing is NOT free:
// demoting `double` to `float` also repacks the block, and the bytes the application
// wrote into the buffer do not move with it. Every member past the first double then
// reads and writes at the wrong offset, and the block is simply shorter than the one
// that was bound - the tail of it is never touched at all
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3, whose output matched its
// input up to the first double's slot and was zero from there on).
//
// The block layout is fixed by GL 4.6 core 7.6.2.2 and is asserted here as literal byte
// offsets rather than queried, so this says what the SPEC requires and not what MobileGL
// happens to report. Both packings are covered because they differ in exactly the places
// that matter: std140 rounds an array's stride and a matrix's column stride up to 16,
// std430 does not, and only std430 packs the scalars tightly.
//
// Every value is exactly representable in binary32, so a correct implementation copies
// the block BYTE FOR BYTE even though it narrows each double on the way through.
constexpr const char* kBlockCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) buffer In140 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_in140;
layout(std430, binding = 1) buffer In430 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_in430;
layout(std140, binding = 2) buffer Out140 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_out140;
layout(std430, binding = 3) buffer Out430 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_out430;
void main() {
g_out140.data0 = g_in140.data0;
for (int i = 0; i < 3; ++i) g_out140.data1[i] = g_in140.data1[i];
g_out140.data2 = g_in140.data2;
g_out140.data3 = g_in140.data3;
for (int i = 0; i < 2; ++i) g_out140.data4[i] = g_in140.data4[i];
g_out140.data5 = g_in140.data5;
g_out140.data6 = g_in140.data6;
g_out430.data0 = g_in430.data0;
for (int i = 0; i < 3; ++i) g_out430.data1[i] = g_in430.data1[i];
g_out430.data2 = g_in430.data2;
g_out430.data3 = g_in430.data3;
for (int i = 0; i < 2; ++i) g_out430.data4[i] = g_in430.data4[i];
g_out430.data5 = g_in430.data5;
g_out430.data6 = g_in430.data6;
}
)";
// GL 4.6 core 7.6.2.2 rule by rule, for the block above.
// std140: an array's element stride and a matrix's column stride round up to 16, a
// double aligns to 8 and a dvec3 to 32.
// std430: the same without the rounding - so the scalars pack tightly and only the
// dvec3's 32-byte alignment leaves a hole.
struct BlockLayout {
int data0;
int data1;
int data1Stride;
int data2;
int data2ColumnStride;
int data3;
int data4;
int data4Stride;
int data5;
int data6;
int size;
};
constexpr BlockLayout kStd140{0, 16, 16, 64, 16, 112, 128, 16, 160, 192, 216};
constexpr BlockLayout kStd430{0, 4, 4, 16, 8, 40, 48, 8, 64, 96, 120};
void PokeInt(std::vector<unsigned char>& bytes, int offset, int value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
void PokeFloat(std::vector<unsigned char>& bytes, int offset, float value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
void PokeDouble(std::vector<unsigned char>& bytes, int offset, double value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
// The block's contents, at the offsets the standard puts them. Padding stays zero, which
// is what makes a byte-for-byte comparison against the (zero-initialised) output buffer
// catch a member that landed somewhere it should not have.
std::vector<unsigned char> MakeBlockContents(const BlockLayout& layout) {
std::vector<unsigned char> bytes(static_cast<std::size_t>(layout.size), 0);
PokeInt(bytes, layout.data0, 1);
for (int i = 0; i < 3; ++i) {
PokeFloat(bytes, layout.data1 + i * layout.data1Stride, 2.0f + static_cast<float>(i));
}
// Column-major, two rows per column.
for (int column = 0; column < 3; ++column) {
for (int row = 0; row < 2; ++row) {
PokeFloat(bytes, layout.data2 + column * layout.data2ColumnStride + row * 4,
5.0f + static_cast<float>(column * 2 + row));
}
}
PokeDouble(bytes, layout.data3, 11.0);
for (int i = 0; i < 2; ++i) {
PokeDouble(bytes, layout.data4 + i * layout.data4Stride, 12.0 + static_cast<double>(i));
}
PokeInt(bytes, layout.data5, 14);
for (int i = 0; i < 3; ++i) {
PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast<double>(i));
}
return bytes;
}
// Names the first byte that differs, and which member owns it, so a failure is a
// diagnosis rather than "the buffer is wrong".
std::string DescribeOffset(const BlockLayout& layout, int offset) {
const std::pair<int, const char*> members[] = {
{layout.data0, "data0"}, {layout.data1, "data1"}, {layout.data2, "data2"},
{layout.data3, "data3"}, {layout.data4, "data4"}, {layout.data5, "data5"},
{layout.data6, "data6"}};
const char* owner = "(padding before data0)";
for (const auto& [start, name] : members) {
if (offset >= start) owner = name;
}
return std::string(owner);
}
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
@@ -428,12 +578,24 @@ void main() {
glUseProgram(0);
// The readback has to undo exactly what the write did - the same std140 column
// padding, the same 4-byte components - or a dmat4 comes back with its columns
// shifted and nothing else in the API would say so.
// padding, the same component width - or a dmat4 comes back with its columns
// shifted and nothing else in the API would say so. Every value below except the
// scalar is exact in float32, so those expectations pin the LAYOUT and hold in
// either regime; the scalar is the one that also pins the PRECISION.
GLdouble readScalar = 0.0;
glGetUniformdv(m_program, scalar, &readScalar);
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
<< "the value is what a float can hold, not the double that was passed in";
// 0.1 is not representable in float32, so what comes back names the regime: a
// backend without native fp64 narrowed it at the glUniform1d above (the module's own
// doubles were demoted, so its storage is 4 bytes per component), and one with it
// stored the double whole. Both are correct; asserting only the narrow answer would
// fail the moment fp64 stops being emulated, and asserting only the wide one would
// fail on every mobile device there is.
if (readScalar == 0.1) {
SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
} else {
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
<< "the value is what a float can hold, not the double that was passed in";
}
GLdouble readVector[3] = {};
glGetUniformdv(m_program, vector, readVector);
@@ -447,7 +609,8 @@ void main() {
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
}
// The float query sees the same storage through the type it is actually stored as.
// The float query sees the same storage through a narrower type, and answers the
// same float either way: GL 4.6 core 7.6 converts on the way out.
GLfloat readFloat = 0.0f;
glGetUniformfv(m_program, scalar, &readFloat);
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
@@ -819,5 +982,66 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
EXPECT_EQ(FirstGLError(), 0u);
}
TEST_F(DoublePrecisionScenario, AStorageBlockWithDoublesKeepsTheLayoutItWasBoundWith) {
if (!Ready()) return;
GLint blocks = 0;
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
if (blocks < 4) {
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 4";
}
const unsigned int program = CompileComputeProgram(kBlockCopySource);
ASSERT_NE(program, 0u) << m_buildLog;
const std::vector<unsigned char> in140 = MakeBlockContents(kStd140);
const std::vector<unsigned char> in430 = MakeBlockContents(kStd430);
const std::vector<unsigned char> zero140(in140.size(), 0);
const std::vector<unsigned char> zero430(in430.size(), 0);
GLuint buffers[4] = {};
glGenBuffers(4, buffers);
const std::vector<unsigned char>* contents[4] = {&in140, &in430, &zero140, &zero430};
for (int i = 0; i < 4; ++i) {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), buffers[i]);
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(contents[i]->size()),
contents[i]->data(), GL_DYNAMIC_COPY);
}
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
EXPECT_EQ(FirstGLError(), 0u);
for (int pass = 0; pass < 2; ++pass) {
const BlockLayout& layout = pass == 0 ? kStd140 : kStd430;
const std::vector<unsigned char>& expected = pass == 0 ? in140 : in430;
const char* packing = pass == 0 ? "std140" : "std430";
std::vector<unsigned char> observed(expected.size(), 0xEE);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
static_cast<GLsizeiptr>(observed.size()), observed.data());
int mismatches = 0;
int firstMismatch = -1;
for (std::size_t i = 0; i < expected.size(); ++i) {
if (expected[i] == observed[i]) continue;
++mismatches;
if (firstMismatch < 0) firstMismatch = static_cast<int>(i);
}
EXPECT_EQ(mismatches, 0)
<< packing << " block: " << mismatches << " of " << expected.size()
<< " bytes differ, first at byte " << firstMismatch << " (in "
<< DescribeOffset(layout, firstMismatch < 0 ? 0 : firstMismatch)
<< "); a block that was repacked around its doubles reads and writes every "
"member after the first one at the wrong offset";
}
glUseProgram(0);
glDeleteProgram(program);
glDeleteBuffers(4, buffers);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace
} // namespace MGITest
@@ -179,6 +179,13 @@ void main()
in flat uint v_index;
out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// The colour index spelled out at its default value. Says nothing that
// `layout(location = 0)` alone does not, and must therefore cost nothing.
constexpr const char* kExplicitColorIndexFS = R"(#version 420 core
layout(location = 0, index = 0) out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
class Glsl420DeclarationScenario : public ScenarioTest {
@@ -473,4 +480,24 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
}
// `layout(location = 0, index = 0)` is the GL default written out loud, and an application
// is entitled to write it - KHR-GL43.shader_atomic_counters.basic-program-query does. It has
// to reach the driver as an ORDINARY single-source output: GLSL ES has no `index` qualifier
// in core, so a transpiler that prints the decoration back gets "index layout qualifier
// requires EXT_blend_func_extended", the stage never compiles, the program runs with a stage
// missing and the draw paints nothing at all. Black, not red - which is why the conformance
// case looked like the atomic counters had stopped counting.
TEST_F(Glsl420DeclarationScenario, AnExplicitDefaultColorIndexStillDraws) {
if (!Ready()) return;
const GLuint program = Build(kQuadVS, kExplicitColorIndexFS);
if (program == 0) return;
const Rgba8 centre = DrawAndRead(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(centre.g, 255) << "a fragment output declared layout(location = 0, index = 0) painted "
"nothing; its stage was almost certainly refused by the driver";
EXPECT_EQ(centre.r, 0u);
}
} // namespace MGITest
@@ -39,6 +39,8 @@
// store, or masked it with the wrong constants, or widened the storage without widening the bind,
// fails these on the device while the software lanes stay green.
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
@@ -176,6 +178,62 @@ namespace MGITest {
return texels;
}
// A GL_TEXTURE_CUBE_MAP_ARRAY of `cubeCount` cubes, i.e. 6 * cubeCount layer-faces
// addressed as array layers. The target the allTargets walkers reach last and the one
// that has caught the most emulation bugs, because it is the only one whose ES
// equivalent is a 2D array with a different addressing rule from the GL name.
GLuint MakeCubeArrayTexture(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType,
const void* seed, int cubeCount) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent,
6 * cubeCount);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "allocating cube-array storage errored with " << GLErrorName(error);
return 0;
}
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
if (seed != nullptr) {
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent,
6 * cubeCount, uploadFormat, uploadType, seed);
}
while (glGetError() != GL_NO_ERROR) {
}
return texture;
}
void BindLayeredImage(GLuint unit, GLuint texture, GLenum internalFormat, GLenum access) {
glBindImageTexture(unit, texture, 0, GL_TRUE, 0, access, internalFormat);
ASSERT_EQ(FirstGLError(), 0u)
<< "glBindImageTexture refused layered format " << std::hex << internalFormat;
}
// Sets `name` from `values`, which must hold 4 * count floats.
void SetVec4Array(GLuint program, const char* name, const std::vector<float>& values,
int count) {
glUseProgram(program);
const GLint location = glGetUniformLocation(program, name);
ASSERT_GE(location, 0) << "the uniform array '" << name << "' was not reflected";
glUniform4fv(location, count, values.data());
EXPECT_EQ(FirstGLError(), 0u) << "setting '" << name << "' errored";
glUseProgram(0);
}
std::vector<float> ReadFloatsFrom(GLenum target, GLuint texture, GLenum format,
int componentsPerTexel, int texelCount) {
std::vector<float> texels(static_cast<std::size_t>(texelCount) * componentsPerTexel,
-12345.0f);
glBindTexture(target, texture);
glGetTexImage(target, 0, format, GL_FLOAT, texels.data());
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
}
return texels;
}
std::vector<GLuint> ReadUints(GLuint texture, GLenum format, int componentsPerTexel) {
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
0xFFFFFFFFu);
@@ -262,6 +320,182 @@ void main()
}
}
// GL_R11F_G11F_B10F, the format the allFormats and allTargets walkers stop at once the
// channel widening has carried everything before it - and the one carrier that is NOT a
// channel widening. It has no core format of its own per-channel width, so it is carried
// in GL_RGBA16F, whose 5-bit exponent and longer mantissa hold every 11f (e5m6) and 10f
// (e5m5) value exactly.
//
// What makes this case different from every other one here, and why it is worth its own
// test: the frontend's shadow for this format is ONE PACKED 32-BIT WORD per texel, not
// three components of the carrier's type. The upload therefore has to DECODE it, where
// every other widening only pads channels onto data already in the right component type.
// A widening that reused the channel repack reads three floats out of a four-byte texel
// and shears the whole level - which a STORE test cannot see, because the dispatch
// overwrites every texel the upload got wrong. So the seed here is per-texel distinct and
// is checked through an imageLoad BEFORE anything is stored.
//
// Every constant is chosen to be exact in both encodings, so the comparisons can be
// equality rather than tolerance: the 1/8 steps need three mantissa bits of the 11f
// channels' six, and the 1/16 steps at exponent 1 need one of the 10f channel's five.
TEST_F(NonCoreImageFormatScenario, PackedFloatImageDecodesItsUploadAndDropsSurplusStores) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
std::vector<float> seed(static_cast<std::size_t>(kTexels) * 3u, 0.0f);
for (int texel = 0; texel < kTexels; ++texel) {
seed[texel * 3 + 0] = 1.0f + static_cast<float>(texel) / 8.0f;
seed[texel * 3 + 1] = 2.0f + static_cast<float>(texel) / 8.0f;
seed[texel * 3 + 2] = 3.0f + static_cast<float>(texel) / 16.0f;
}
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
const GLuint narrow = MakeTexture(GL_R11F_G11F_B10F, GL_RGB, GL_FLOAT, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r11f_g11f_b10f, binding = 0) readonly uniform image2D narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r11f_g11f_b10f, binding = 0) writeonly uniform image2D narrow;
void main()
{
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(5.0, 6.0, 7.0, 8.0));
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
// THE UPLOAD, read back through the image. A sheared decode still produces plausible
// floats, so the check is per texel and the seed never repeats a value.
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_FLOAT_EQ(loaded[texel * 4 + 0], seed[texel * 3 + 0]) << "texel " << texel << " red";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], seed[texel * 3 + 1]) << "texel " << texel << " green";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], seed[texel * 3 + 2]) << "texel " << texel << " blue";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
}
// THE STORE. Three channels survive and the fourth is dropped, which is the mask this
// format needs and no other widened format does - every other carrier here pins two
// or three of the carrier's channels, this one pins only alpha.
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_WRITE_ONLY);
Dispatch(storeProgram);
const std::vector<float> stored = ReadFloats(narrow, GL_RGB, 3);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_FLOAT_EQ(stored[texel * 3 + 0], 5.0f) << "texel " << texel << " red";
EXPECT_FLOAT_EQ(stored[texel * 3 + 1], 6.0f) << "texel " << texel << " green";
EXPECT_FLOAT_EQ(stored[texel * 3 + 2], 7.0f) << "texel " << texel << " blue";
}
}
// GL_RGB10_A2UI, the format all four allFormats walkers stop at once r11f_g11f_b10f is
// carried - and the only widening whose carrier has as MANY channels as the original, so
// GL leaves nothing to pin and neither access is rewritten. What it does need is the other
// packed transfer: its shadow is one GL_UNSIGNED_INT_2_10_10_10_REV word per texel, which
// the GL_RGBA16UI carrier is uploaded as four shorts.
//
// The seed is checked through an imageLoad BEFORE anything is stored, for the reason the
// r11f case is: a sheared split still produces plausible integers, and a store would
// overwrite every texel the upload got wrong. Every channel of every texel is distinct,
// and the alpha values walk the whole 0..3 a two-bit channel has - a widening that pinned
// alpha to GL's "1" the way a three-channel one must would pass for texel 1 alone.
TEST_F(NonCoreImageFormatScenario, PackedIntegerImageSplitsItsUploadAndKeepsAllFourChannels) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
std::vector<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
std::vector<GLuint> expected(static_cast<std::size_t>(kTexels) * 4u, 0u);
for (int texel = 0; texel < kTexels; ++texel) {
const GLuint r = static_cast<GLuint>(texel) * 7u; // 0 .. 105
const GLuint g = 1023u - static_cast<GLuint>(texel) * 11u; // 1023 .. 858
const GLuint b = 512u + static_cast<GLuint>(texel); // 512 .. 527
const GLuint a = static_cast<GLuint>(texel) % 4u; // the whole 0..3
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
expected[texel * 4 + 0] = r;
expected[texel * 4 + 1] = g;
expected[texel * 4 + 2] = b;
expected[texel * 4 + 3] = a;
}
const std::vector<GLuint> wideSeed(static_cast<std::size_t>(kTexels) * 4u, 999u);
const GLuint narrow =
MakeTexture(GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgb10_a2ui, binding = 0) readonly uniform uimage2D narrow;
layout (rgba32ui, binding = 1) writeonly uniform uimage2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgb10_a2ui, binding = 0) writeonly uniform uimage2D narrow;
void main()
{
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), uvec4(11u, 22u, 33u, 2u));
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32UI, GL_WRITE_ONLY);
Dispatch(loadProgram);
const std::vector<GLuint> loaded = ReadUints(wide, GL_RGBA_INTEGER, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(loaded[texel * 4 + 0], expected[texel * 4 + 0]) << "texel " << texel << " red";
EXPECT_EQ(loaded[texel * 4 + 1], expected[texel * 4 + 1]) << "texel " << texel << " green";
EXPECT_EQ(loaded[texel * 4 + 2], expected[texel * 4 + 2]) << "texel " << texel << " blue";
EXPECT_EQ(loaded[texel * 4 + 3], expected[texel * 4 + 3]) << "texel " << texel << " alpha";
}
// THE STORE. All four channels survive - this is the one widened format where GL drops
// nothing, so a mask here would be a bug rather than the emulation.
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_WRITE_ONLY);
Dispatch(storeProgram);
const std::vector<GLuint> stored = ReadUints(narrow, GL_RGBA_INTEGER, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(stored[texel * 4 + 0], 11u) << "texel " << texel << " red";
EXPECT_EQ(stored[texel * 4 + 1], 22u) << "texel " << texel << " green";
EXPECT_EQ(stored[texel * 4 + 2], 33u) << "texel " << texel << " blue";
EXPECT_EQ(stored[texel * 4 + 3], 2u) << "texel " << texel << " alpha";
}
}
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
// no text was produced for the stage and the dispatch could not run.
@@ -330,6 +564,652 @@ void main()
}
}
// GL_RG16, the first of the seven NORMALIZED formats and the first carrier that changes the
// shader-visible TYPE: core ESSL has no 16-bit normalized image format of any width, and
// no float carrier is honest either (a half has eleven mantissa bits against sixteen), so
// the rgba16ui behind it holds the format's own CODES and every access converts.
//
// Both directions of GL 4.6 2.3.5 are checked, and the STORE direction is checked as exact
// INTEGER CODES rather than as floats within a tolerance - which is the point of a code
// carrier over a float one, and the only thing that would catch a rounding rule that was
// merely close. The values are chosen so that the products are exact in float32: 0.25 and
// 0.75 land off a tie, 0.5 lands exactly ON one (0.5 * 65535 = 32767.5), and the two
// out-of-range values must be clamped before they are rounded rather than after.
TEST_F(NonCoreImageFormatScenario, UnsignedNormalizedImageCarriesItsCodesBothWays) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
constexpr double kUnorm16Max = 65535.0;
// THE UPLOAD. Distinct per texel, and the codes are the shadow's own 16-bit words: a
// widening that padded or sheared them still produces plausible normalized floats.
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels) * 2u, 0);
for (int texel = 0; texel < kTexels; ++texel) {
seed[texel * 2 + 0] = static_cast<GLushort>(texel * 4001);
seed[texel * 2 + 1] = static_cast<GLushort>(65535 - texel * 3001);
}
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
const GLuint narrow = MakeTexture(GL_RG16, GL_RG, GL_UNSIGNED_SHORT, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg16, binding = 0) readonly uniform image2D narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg16, binding = 0) writeonly uniform image2D narrow;
uniform vec4 g_values[16];
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[texel * 2 + 0])
<< "texel " << texel << " red";
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), seed[texel * 2 + 1])
<< "texel " << texel << " green";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f)
<< "texel " << texel << ": imageLoad on a two-channel format must report 0 for blue";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
}
// THE STORE, per GL 4.6 2.3.5: c = round(clamp(f, 0, 1) * (2^b - 1)), with a tie
// rounded away from zero.
struct Boundary {
float value;
long code;
};
const Boundary boundaries[kTexels] = {
{0.0f, 0}, {1.0f, 65535}, {0.5f, 32768}, {0.25f, 16384},
{0.75f, 49151}, {-0.5f, 0}, {2.0f, 65535}, {-1.0f, 0},
{1.0f / 131072.0f, 0}, // 0.4999923 of a code: rounds DOWN
{3.0f / 131072.0f, 1}, // 1.4999771 of a code: rounds DOWN to 1
{1.0f / 65535.0f, 1}, // exactly one code
{32767.0f / 65535.0f, 32767}, {32768.0f / 65535.0f, 32768},
// 0.125 * 65535 = 8191.875 and 0.875 * 65535 = 57343.125 - neither is a tie, and
// both round DOWN, which is the pair that catches a conversion that scaled by 2^b
// instead of 2^b - 1.
{65534.0f / 65535.0f, 65534}, {0.125f, 8192}, {0.875f, 57343},
};
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
for (int texel = 0; texel < kTexels; ++texel) {
values[texel * 4 + 0] = boundaries[texel].value;
values[texel * 4 + 1] = boundaries[texel].value;
values[texel * 4 + 2] = 0.5f; // dropped: a two-channel format has no blue
values[texel * 4 + 3] = 0.5f; // dropped: nor an alpha
}
SetVec4Array(storeProgram, "g_values", values, kTexels);
BindImage(kNarrowUnit, narrow, GL_RG16, GL_WRITE_ONLY);
Dispatch(storeProgram);
// Read back through the IMAGE, so what is compared is the code the store actually
// wrote rather than anything the readback path might renormalize on its own.
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), boundaries[texel].code)
<< "texel " << texel << " stored " << boundaries[texel].value;
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), boundaries[texel].code)
<< "texel " << texel << " green";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f) << "texel " << texel << " blue";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f) << "texel " << texel << " alpha";
}
// ...and glGetTexImage owes the application the NORMALIZED value, whatever the ES
// storage holds. The whole texture is an integer one now, so this is the only place
// the readback conversion is exercised at all.
const std::vector<float> viaGetTexImage = ReadFloats(narrow, GL_RG, 2);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 0] * kUnorm16Max), boundaries[texel].code)
<< "texel " << texel << " red through glGetTexImage";
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 1] * kUnorm16Max), boundaries[texel].code)
<< "texel " << texel << " green through glGetTexImage";
}
}
// GL_RGBA16_SNORM, the signed twin. Two things differ and both are one-line mistakes: the
// code is a two's-complement 16-bit integer stored in an UNSIGNED carrier channel, so it
// has to be sign-extended on the way out (a zero extension reads every negative value as
// something near +1), and the decode is max(c / 32767, -1) rather than the bare division,
// because the code -32768 exists and GL says it means exactly -1.
TEST_F(NonCoreImageFormatScenario, SignedNormalizedImageSignExtendsItsCodesAndClampsAtMinusOne) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
constexpr double kSnorm16Max = 32767.0;
// The seed walks the whole signed range, INCLUDING -32768, whose decode is the one
// value the division alone gets wrong.
const GLshort seedCodes[kTexels] = {0, 32767, -32767, -32768, 1, -1, 16384, -16384,
12345, -12345, 32766, -32766, 255, -256, 4095, -4096};
std::vector<GLshort> seed(static_cast<std::size_t>(kTexels) * 4u, 0);
for (int texel = 0; texel < kTexels; ++texel) {
seed[texel * 4 + 0] = seedCodes[texel];
seed[texel * 4 + 1] = static_cast<GLshort>(-seedCodes[texel] == -32768 ? 32767
: -seedCodes[texel]);
seed[texel * 4 + 2] = seedCodes[(texel + 1) % kTexels];
seed[texel * 4 + 3] = seedCodes[(texel + 2) % kTexels];
}
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -12.0f);
const GLuint narrow = MakeTexture(GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgba16_snorm, binding = 0) readonly uniform image2D narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgba16_snorm, binding = 0) writeonly uniform image2D narrow;
uniform vec4 g_values[16];
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
for (int channel = 0; channel < 4; ++channel) {
const GLshort code = seed[texel * 4 + channel];
const float expected =
std::max(static_cast<float>(code) / static_cast<float>(kSnorm16Max), -1.0f);
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel], expected)
<< "texel " << texel << " channel " << channel << " code " << code;
}
}
// THE STORE: c = round(clamp(f, -1, 1) * (2^(b-1) - 1)), ties away from zero on BOTH
// sides - which is what makes -0.5 land on -16384 rather than on -16383.
struct Boundary {
float value;
long code;
};
const Boundary boundaries[kTexels] = {
{0.0f, 0}, {1.0f, 32767}, {-1.0f, -32767}, {0.5f, 16384},
{-0.5f, -16384}, {2.0f, 32767}, {-2.0f, -32767}, {0.25f, 8192},
{-0.25f, -8192}, {1.0f / 32767.0f, 1}, {-1.0f / 32767.0f, -1},
// Three quarters of a code, not half: GL leaves the direction of a TIE to the
// implementation ("if two values are equally near, the implementation may choose
// either"), and Magma hands these formats to Vulkan unemulated, so a value exactly
// on 0.5 of a code is the one thing the two backends are allowed to disagree
// about. Every entry here is off a tie except the ones at 0.5 and 0.25 of the
// RANGE, whose products (16383.5 and 8192) round the same way under either rule.
{0.75f / 32767.0f, 1}, {-0.75f / 32767.0f, -1},
{16383.0f / 32767.0f, 16383}, {-16383.0f / 32767.0f, -16383},
{0.125f, 4096},
};
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
for (int texel = 0; texel < kTexels; ++texel) {
for (int channel = 0; channel < 4; ++channel) {
values[texel * 4 + channel] = boundaries[texel].value;
}
}
SetVec4Array(storeProgram, "g_values", values, kTexels);
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_WRITE_ONLY);
Dispatch(storeProgram);
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
for (int channel = 0; channel < 4; ++channel) {
EXPECT_EQ(std::lround(loaded[texel * 4 + channel] * kSnorm16Max),
boundaries[texel].code)
<< "texel " << texel << " channel " << channel << " stored "
<< boundaries[texel].value;
}
}
}
// GL_RGB10_A2, the one normalized format whose channels are not all the same width: three
// of ten bits and one of two. A single denominator would be right for three quarters of
// every texel and wildly wrong for the fourth - alpha 1.0 would come back as 3/1023.
TEST_F(NonCoreImageFormatScenario, TenTenTenTwoImageUsesItsOwnPerChannelDenominators) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
std::vector<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
std::vector<GLuint> seedCodes(static_cast<std::size_t>(kTexels) * 4u, 0u);
for (int texel = 0; texel < kTexels; ++texel) {
const GLuint r = static_cast<GLuint>(texel) * 67u;
const GLuint g = 1023u - static_cast<GLuint>(texel) * 13u;
const GLuint b = 341u + static_cast<GLuint>(texel);
const GLuint a = static_cast<GLuint>(texel) % 4u;
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
seedCodes[texel * 4 + 0] = r;
seedCodes[texel * 4 + 1] = g;
seedCodes[texel * 4 + 2] = b;
seedCodes[texel * 4 + 3] = a;
}
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
const GLuint narrow =
MakeTexture(GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgb10_a2, binding = 0) readonly uniform image2D narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rgb10_a2, binding = 0) writeonly uniform image2D narrow;
void main()
{
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.5, 1.0, 1.0));
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
for (int channel = 0; channel < 4; ++channel) {
const auto denominator = channel == 3 ? 3.0f : 1023.0f;
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel],
static_cast<float>(seedCodes[texel * 4 + channel]) / denominator)
<< "texel " << texel << " channel " << channel;
}
}
// 0.5 through a TWO-bit channel is 1.5 of a code and rounds away from zero to 2, which
// is 2/3 back - a value only the two-bit denominator can produce.
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_WRITE_ONLY);
Dispatch(storeProgram);
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * 1023.0), 0) << "texel " << texel << " red";
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * 1023.0), 512) << "texel " << texel << " green";
EXPECT_EQ(std::lround(loaded[texel * 4 + 2] * 1023.0), 1023) << "texel " << texel << " blue";
EXPECT_EQ(std::lround(loaded[texel * 4 + 3] * 3.0), 3) << "texel " << texel << " alpha";
}
}
// The same carrier on a GL_TEXTURE_CUBE_MAP_ARRAY, the target the allTargets walkers reach
// last and the one whose ES equivalent is addressed differently from its GL name (six
// layer-faces per cube, as array layers). Nothing about the format conversion changes with
// the target - which is exactly the claim, since the storage widening, the layered bind and
// the per-layer readback all have their own code paths for this target alone.
TEST_F(NonCoreImageFormatScenario, NormalizedImageCarriesEveryLayerFaceOfACubeMapArray) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
constexpr int kCubes = 2;
constexpr int kLayerFaces = 6 * kCubes;
constexpr int kTexelsPerFace = kExtent * kExtent;
constexpr int kTexels = kTexelsPerFace * kLayerFaces;
constexpr double kUnorm16Max = 65535.0;
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels), 0);
for (int texel = 0; texel < kTexels; ++texel) {
seed[texel] = static_cast<GLushort>((texel * 5477u) & 0xFFFFu);
}
const GLuint narrow =
MakeCubeArrayTexture(GL_R16, GL_RED, GL_UNSIGNED_SHORT, seed.data(), kCubes);
if (narrow == 0) return;
// THE UPLOAD, read back through glGetTexImage across every layer-face. A carrier that
// widened the storage but seeded only the first face leaves the rest at zero, which is
// what the per-layer readback path is there to catch, and this target is the only one
// whose readback goes layer by layer.
const std::vector<float> uploaded =
ReadFloatsFrom(GL_TEXTURE_CUBE_MAP_ARRAY, narrow, GL_RED, 1, kTexels);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(std::lround(uploaded[texel] * kUnorm16Max), seed[texel])
<< "texel " << texel << " of " << kTexels;
}
// ...and through an imageCubeArray, which is the declaration the shader half has to
// carry for this target: a layered bind, an ivec3 coordinate whose z is the
// layer-face, and the same unpack as every other target.
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexelsPerFace) * 4u, -1.0f);
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r16, binding = 0) readonly uniform imageCubeArray narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
uniform int g_layerFace;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, ivec3(coord, g_layerFace)));
}
)");
if (loadProgram == 0) return;
// Two faces, one of them past the first cube, so a carrier that addressed only the
// first six layer-faces cannot pass.
for (const int layerFace : {1, 9}) {
glUseProgram(loadProgram);
const GLint location = glGetUniformLocation(loadProgram, "g_layerFace");
ASSERT_GE(location, 0) << "g_layerFace was not reflected";
glUniform1i(location, layerFace);
glUseProgram(0);
BindLayeredImage(kNarrowUnit, narrow, GL_R16, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexelsPerFace; ++texel) {
const int sourceTexel = layerFace * kTexelsPerFace + texel;
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[sourceTexel])
<< "layer-face " << layerFace << " texel " << texel;
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 0.0f)
<< "layer-face " << layerFace << " texel " << texel
<< ": imageLoad on a one-channel format must report 0 for green";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
<< "layer-face " << layerFace << " texel " << texel
<< ": imageLoad on a format without alpha must report 1";
}
}
}
// A BUFFER image, which takes neither of the emulations above. Its texels are the
// application's buffer object - at the size and layout the application gave it, and
// usually also a vertex, index or storage buffer - so there is nothing to reallocate a
// carrier in. What CAN be done is a SPLIT: rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the view is re-declared and every subscript is
// doubled (WidenImageFormatsPass, and the matching glTexBuffer/glBindImageTexture format
// in TextureImpl).
//
// THE NUMBERS HERE ARE THE ONES THAT PINNED THE OLD BUG. Widening a buffer image instead
// leaves the shader striding 16 bytes through 8-byte texels: measured on an Adreno 830
// with this exact 32-byte GL_RG32F buffer and this exact shader, the readback came back
// [1,100] [0,1] [2,100] [0,1] - texels 0 and 1 landed on top of all four, and texels 2 and
// 3 were written past the end of the application's buffer.
//
// This runs on every backend, and on a driver that CAN spell rg32f for an imageBuffer
// (Mesa's, which the software lanes use) nothing is split at all - which is the other half
// of the claim: the arming has to agree with the shader, so a split that fired where the
// driver needed none would double every subscript and fail here just as loudly.
TEST_F(NonCoreImageFormatScenario, BufferImageAddressesTheApplicationsOwnTexels) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
GLint maxTextureBufferSize = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
constexpr int kBufferTexels = 4;
const std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, -1.0f);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
GL_DYNAMIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
if (const GLenum error = FirstGLError()) {
glDeleteBuffers(1, &buffer);
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
}
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg32f, binding = 0) writeonly uniform imageBuffer narrow;
void main()
{
int texel = int(gl_GlobalInvocationID.x);
imageStore(narrow, texel, vec4(float(texel + 1), 100.0, 3.0, 4.0));
}
)");
if (storeProgram == 0) {
glDeleteBuffers(1, &buffer);
return;
}
BindImage(kNarrowUnit, texture, GL_RG32F, GL_WRITE_ONLY);
glUseProgram(storeProgram);
glDispatchCompute(kBufferTexels, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
std::vector<float> readback(seed.size(), -12345.0f);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
EXPECT_EQ(FirstGLError(), 0u) << "reading the buffer back errored";
for (int texel = 0; texel < kBufferTexels; ++texel) {
EXPECT_FLOAT_EQ(readback[texel * 2 + 0], static_cast<float>(texel + 1))
<< "texel " << texel << " red";
EXPECT_FLOAT_EQ(readback[texel * 2 + 1], 100.0f) << "texel " << texel << " green";
}
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteBuffers(1, &buffer);
while (glGetError() != GL_NO_ERROR) {
}
}
// The SAME buffer texture read through BOTH doors at once, which is the shape the split
// originally broke. A buffer texture that is image-bound is split - the view is re-declared
// one component at a time and every image subscript is doubled to match - but the sampler
// side is NOT subscript-rewritten, so re-describing the APPLICATION's own texture made
// texelFetch(s, i) return component 2i of the base view instead of texel i's whole pair.
// The split therefore goes on a private second name over the same buffer
// (BackendTextureObject::m_bufferImageSplitViewId) and the application's name keeps the
// format it asked for: rg32f is a legal SAMPLED buffer-texture format in ES 3.2, it is only
// the IMAGE binding ES cannot spell.
//
// This is KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess reduced to one
// dispatch. That case image-stores into a GL_RG32F buffer texture and then, in one shader,
// reads the same texture through an imageBuffer AND a samplerBuffer and compares the two -
// so it went red on every pixel while its sibling -vertexArray, which never samples the
// buffer texture, passed.
//
// Like the case above this runs on every backend, and on a driver that can spell rg32f for
// an imageBuffer nothing is split at all - both doors then trivially agree, which is the
// other half of the claim: a split that fired where the driver needed none would show up
// here as the two disagreeing.
TEST_F(NonCoreImageFormatScenario, ASplitBufferImageStillSamplesWholeTexels) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
GLint maxTextureBufferSize = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
constexpr int kBufferTexels = 4;
// Both components of every texel distinct and non-zero, so a sampler that reads the
// SPLIT view cannot accidentally agree: texel i would come back as (2i-th component,
// 0, 0, 1) rather than (x, y, 0, 1), and every one of those is a value no texel holds.
std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, 0.0f);
for (int texel = 0; texel < kBufferTexels; ++texel) {
seed[static_cast<std::size_t>(texel) * 2u + 0u] = static_cast<float>(texel * 10 + 1);
seed[static_cast<std::size_t>(texel) * 2u + 1u] = static_cast<float>(texel * 10 + 2);
}
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
GL_DYNAMIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
if (const GLenum error = FirstGLError()) {
glDeleteBuffers(1, &buffer);
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
}
// The answer buffer is rgba32f, which IS core ESSL, so it is never split and cannot
// hide a mistake in the thing under test.
constexpr int kAnswers = kBufferTexels * 2;
const std::vector<float> answerSeed(static_cast<std::size_t>(kAnswers) * 4u, -12345.0f);
GLuint answerBuffer = 0;
glGenBuffers(1, &answerBuffer);
glBindBuffer(GL_TEXTURE_BUFFER, answerBuffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(answerSeed.size() * sizeof(float)),
answerSeed.data(), GL_DYNAMIC_DRAW);
GLuint answerTexture = 0;
glGenTextures(1, &answerTexture);
m_textures.push_back(answerTexture);
glBindTexture(GL_TEXTURE_BUFFER, answerTexture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, answerBuffer);
if (const GLenum error = FirstGLError()) {
glDeleteBuffers(1, &buffer);
glDeleteBuffers(1, &answerBuffer);
GTEST_SKIP() << "glTexBuffer(GL_RGBA32F) errored with " << GLErrorName(error);
}
const GLuint program = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg32f, binding = 0) readonly uniform imageBuffer narrow;
layout (rgba32f, binding = 1) writeonly uniform imageBuffer answers;
uniform samplerBuffer sampled;
void main()
{
int texel = int(gl_GlobalInvocationID.x);
imageStore(answers, texel * 2 + 0, imageLoad(narrow, texel));
imageStore(answers, texel * 2 + 1, texelFetch(sampled, texel));
}
)");
if (program == 0) {
glDeleteBuffers(1, &buffer);
glDeleteBuffers(1, &answerBuffer);
return;
}
BindImage(kNarrowUnit, texture, GL_RG32F, GL_READ_ONLY);
BindImage(kWideUnit, answerTexture, GL_RGBA32F, GL_WRITE_ONLY);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glUseProgram(program);
glUniform1i(glGetUniformLocation(program, "sampled"), 0);
glDispatchCompute(kBufferTexels, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
std::vector<float> readback(answerSeed.size(), -54321.0f);
glBindBuffer(GL_TEXTURE_BUFFER, answerBuffer);
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
EXPECT_EQ(FirstGLError(), 0u) << "reading the answers back errored";
for (int texel = 0; texel < kBufferTexels; ++texel) {
const float red = static_cast<float>(texel * 10 + 1);
const float green = static_cast<float>(texel * 10 + 2);
const std::size_t viaImage = static_cast<std::size_t>(texel) * 8u;
const std::size_t viaSampler = viaImage + 4u;
EXPECT_FLOAT_EQ(readback[viaImage + 0u], red) << "texel " << texel << " imageLoad red";
EXPECT_FLOAT_EQ(readback[viaImage + 1u], green) << "texel " << texel << " imageLoad green";
EXPECT_FLOAT_EQ(readback[viaSampler + 0u], red) << "texel " << texel << " texelFetch red";
EXPECT_FLOAT_EQ(readback[viaSampler + 1u], green)
<< "texel " << texel
<< " texelFetch green: a samplerBuffer must see whole texels even where the "
"image side of the same texture was split";
EXPECT_FLOAT_EQ(readback[viaSampler + 2u], 0.0f) << "texel " << texel << " texelFetch blue";
EXPECT_FLOAT_EQ(readback[viaSampler + 3u], 1.0f) << "texel " << texel << " texelFetch alpha";
}
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteBuffers(1, &buffer);
glDeleteBuffers(1, &answerBuffer);
while (glGetError() != GL_NO_ERROR) {
}
}
// The other consumer of the same texture. A widened texture's ES storage really does have
// four channels, so a sampler reading it raw would see whatever the carrier holds; the
// logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges
@@ -64,6 +64,25 @@ void main() {
g_length[2] = g_input23[0].data.length();
g_length[3] = g_input23[1].data.length();
}
)";
// GL 4.6 core 4.10 lets a buffer variable be declared readonly AND writeonly at once:
// it can then be neither read nor written, and `.length()` is the only thing left that
// may be asked of it. The pair is inert - and printing it into ESSL is not, because
// SPIRV-Cross hoists the qualifiers every member shares onto the BLOCK and Mesa's ES
// compiler refuses that spelling ("Interface block sets both readonly and writeonly").
// Lifted from KHR-GL43.shader_storage_buffer_object.basic-readonly-writeonly.
constexpr const char* kReadonlyWriteonlyComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Input {
readonly writeonly int g_in[];
};
layout(std430, binding = 4) buffer Output {
int g_length[];
};
void main() {
g_length[0] = g_in.length();
}
)";
constexpr int kElementBytes = 16; // ivec4, std430
@@ -212,4 +231,33 @@ void main() {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
}
// A buffer variable qualified readonly AND writeonly can only be asked its length, and that
// question still has to be answered. A stage the driver refused answers 0 - and refuses
// silently, because the program links without it and the dispatch is then a no-op.
TEST_F(SsboArrayLengthScenario, AReadonlyWriteonlyArrayStillReportsItsLength) {
if (!Ready() || IsSkipped()) return;
const GLuint program = CompileComputeProgram(kReadonlyWriteonlyComputeSource);
ASSERT_NE(program, 0u) << m_buildLog;
const GLuint input = MakeStorageBuffer(6); // 6 ivec4 = 24 ints
const GLuint output = MakeStorageBuffer(1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
int length = -1;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, output);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(length), &length);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(length, 24) << "a readonly+writeonly runtime array reported length " << length
<< "; 0 means the stage never reached the program";
glUseProgram(m_program);
glDeleteProgram(program);
}
} // namespace MGITest
@@ -30,15 +30,23 @@
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
//
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
// still red). The skip is explicit rather than silent so a future emulation lands here as a
// failing test and not as a test that was quietly never running. DirectVulkan additionally
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
// it matters.
// BOTH BACKENDS RUN EVERY CASE, by two completely different routes, which is the point of
// keeping them in one file. DirectVulkan declares sixteen viewports on the pipeline and lets the
// hardware route. DirectGLES has one viewport, one scissor rectangle and one depth range and no
// gl_ViewportIndex at all, so it EMULATES: the builtin becomes a flat varying, the fragment stage
// gets a gate, and the draw is replayed once per distinct viewport state (Managers.h,
// ForEachViewportRoutingPass). Every assertion below is about pixels, so it cannot tell the two
// apart - which is exactly what has to be true.
//
// DirectVulkan skips when the device lacks the multiViewport feature - Vulkan then forbids a
// pipeline from declaring more than one viewport at all, which is a device limit and not a
// MobileGL bug; lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do
// run where it matters.
//
// The last case is the negative control for the emulation and runs on DirectGLES only: it builds
// the SAME program with the emulation switched off and requires the routing to collapse onto
// viewport 0. Without it every assertion above could be satisfied by a backend that happened to
// be right for some other reason, and the emulation's own switch would be untested.
#include <cmath>
#include <string>
@@ -47,6 +55,10 @@
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
// to know which backend it is running on.
#include <Config.h>
#ifdef GLAPI
#undef GLAPI
#endif
@@ -142,13 +154,6 @@ void main() { fragColor = gl_FragCoord.z; }
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
"rendering half of KHR-GL43.viewport_array.";
}
GLint maxViewports = 0;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
@@ -520,11 +525,78 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
DestroyIntTarget(target);
}
// --- 4. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
// --- 4. the negative control for the DirectGLES emulation -----------------------------
//
// Deliberately NOT a ViewportArrayScenario case, because it must run on DirectGLES - the
// backend that got it wrong - and that fixture skips there. It needs none of the routing:
// one viewport, one scissor rectangle, no geometry stage.
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
// emulation that works from a backend that was going to be right anyway. This case builds
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
// (and the failure signature KHR-GL43.viewport_array reported on it), pinned here so that
// (a) the three cases above are known to be testing the emulation and not the weather,
// and (b) the switch itself has a test.
//
// DirectGLES only: the flag steers nothing on DirectVulkan, which routes natively.
TEST_F(ViewportArrayScenario, WithoutTheEmulationEveryIndexCollapsesOntoViewportZero) {
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "the emulation switch is a DirectGLES concern; DirectVulkan routes "
"gl_ViewportIndex natively and ignores it";
}
// The feature table is a process-global and this fixture shares its context with every
// other scenario in the process, so the restore is not optional.
struct ScopedEmulationOff {
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
MobileGL::MG_Config::Features.ViewportArrayEmulation =
MobileGL::MG_Config::QuirkOverride::ForceOff;
}
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
MobileGL::MG_Config::QuirkOverride saved;
};
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
SetupGridViewports(kCellSize, kCellSize);
GLuint unroutedProgram = 0;
{
const ScopedEmulationOff scopedEmulationOff;
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
// that carries this flag, so reusing m_program would just replay the routed build.
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
glUseProgram(unroutedProgram);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
}
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
// Cell (0, 0) IS viewport 0's rectangle, so it is the one cell an unrouted draw paints
// with something. Everything it holds comes from the last geometry invocation.
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, 0, 0), kViewportCount - 1)
<< "with the emulation off, viewport 0's rectangle must hold the LAST invocation's "
"index - if it holds 0 the routing is still happening and this control proves "
"nothing";
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
if (x == 0 && y == 0) continue;
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
<< "cell (" << x << ", " << y << ") is outside viewport 0's rectangle and an "
<< "unrouted draw cannot reach it";
}
}
glUseProgram(0);
glDeleteProgram(unroutedProgram);
DestroyIntTarget(target);
}
// --- 5. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
//
// Deliberately NOT a ViewportArrayScenario case, because that fixture's geometry stage
// routes and this claim needs none of it: one viewport, one scissor rectangle, no
// geometry stage - and it has to hold identically whether or not anything routes.
//
// glScissor(0, 0, 0, 0) is legal GL meaning "the scissor test rejects every fragment",
// but it is byte-identical to the all-zero rectangle a context starts with, whose meaning
@@ -787,9 +787,11 @@ namespace MobileGL::MG_State::GLState {
// The L1 key. Every input below is one that can change the SPIR-V this program
// generates; see the key inventory on SpirvTranslationKeyInputs.
//
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
// Deliberately NOT keyed on: anything that only steers a BACKEND transpile - see the
// classification on CompileEnv::frontendFingerprint, and L2's own key in
// MG_Util/ShaderTranspiler/TranslationCache.h.
// MG_Util/ShaderTranspiler/TranslationCache.h. The single capability bit that IS here
// (nativeFloat64) earns its place by changing SanitizeAndOptimizeBinary's own output,
// which is what the payload stores.
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
using namespace MG_Util::ShaderTranspiler;
@@ -805,6 +807,11 @@ namespace MobileGL::MG_State::GLState {
// value cannot alias a module parsed without it.
keyInputs.shaderCompileFlags = 0;
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
// The one BACKEND capability bit in this key, and it has to be here: it reaches inside
// SanitizeAndOptimizeBinary, whose output is what the payload holds. Read from the same
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
// disagree.
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
keyInputs.stages.reserve(in.shaders.size());
for (const LinkShaderInput& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
@@ -155,6 +155,10 @@ namespace MobileGL::MG_State::GLState {
Uint8* const scratch = m_spirv.globalUboScratch.data();
const SizeT uboSize = m_spirv.globalUboScratch.size();
// Read straight off m_spirv, not through UsesNativeFloat64(): this runs INSIDE the
// phase-B publish, where the join gate is not re-entrant. Same reason the scratch above
// is taken directly.
const Bool nativeFloat64 = m_spirv.nativeFloat64;
for (const auto& init : initializers) {
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
@@ -165,12 +169,13 @@ namespace MobileGL::MG_State::GLState {
const Int elements = init.arraySize;
if (componentsPerElement <= 0 || elements <= 0) continue;
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
// float in a shader is narrowed to 32 bits before the module reaches a backend
// EbtDouble belongs with the floats, not with the skipped types. On a DEMOTED
// program its 64-bit floats were narrowed to 32 before the module reached a backend
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
// value into floatValues, which is a vector<double> either way. Leaving it out meant
// the initializer was silently dropped and the uniform came up zero.
// the 32-bit shadow encoding a `uniform float` does; on a program that kept them it
// has an 8-byte one, which the store width below picks up. glslang folded the value
// into floatValues, a vector<double>, in both cases. Leaving it out meant the
// initializer was silently dropped and the uniform came up zero.
const Bool isFloat = init.basicType == glslang::EbtFloat ||
init.basicType == glslang::EbtFloat16 ||
init.basicType == glslang::EbtDouble;
@@ -195,22 +200,36 @@ namespace MobileGL::MG_State::GLState {
// std140 pads every column of a float matrix out to a vec4, so the columns of
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span
// states the stride the rest of the pipeline agreed on rather than guessing it.
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location));
// The static form, with the width taken from m_spirv directly: the member
// overload asks UsesNativeFloat64(), which joins phase B - and phase B is what
// is publishing right now.
const SizeT slotSpan =
UniformStorageSpanInBytes(GetUniformTypeFacts(static_cast<Uint>(location)),
GetUniformSizesInBytes(static_cast<Uint>(location)), nativeFloat64);
const SizeT columnStride =
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
const Int columnCount = columns > 0 ? columns : 1;
// A `double` initializer on a program that KEPT its doubles lands in an 8-byte
// component, not a 4-byte one; every other basic type - and every double on a
// demoted program - stays one 32-bit word. glslang folded the value into
// floatValues (a vector<double>) either way, so only the store width moves.
const Bool isWideDouble = init.basicType == glslang::EbtDouble && nativeFloat64;
const SizeT componentSize = isWideDouble ? sizeof(Double) : sizeof(Uint32);
for (Int column = 0; column < columnCount; ++column) {
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32);
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * componentSize;
if (byteOffset + writeSize > uboSize) break;
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
static_cast<SizeT>(column) * componentsPerColumn;
for (Int component = 0; component < componentsPerColumn; ++component) {
const SizeT source = firstComponent + static_cast<SizeT>(component);
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32);
if (isFloat) {
Uint8* const destination = scratch + byteOffset + component * componentSize;
if (isWideDouble) {
const Double value = init.floatValues[source];
std::memcpy(destination, &value, sizeof(value));
} else if (isFloat) {
const Float value = static_cast<Float>(init.floatValues[source]);
std::memcpy(destination, &value, sizeof(value));
} else {
@@ -565,26 +565,47 @@ namespace MobileGL::MG_State::GLState {
: kInvalidUniformOffset;
}
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
// shader is narrowed to 32 bits before the module reaches a backend
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
// reading or writing a whole uniform's storage - a bounds check, a copy between two
// programs' shadows - wants this rather than GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) {
if (type.isMatrix) {
return static_cast<SizeT>(type.matrixCols) * 4 * sizeof(Float);
// std140 column stride of a matrix uniform in the global UBO: every column is padded out
// to the base alignment of a vec4 for 32-bit components, and of a dvec4 for 64-bit ones -
// except that a 2-ROW double column is a dvec2, whose base alignment is already 16.
// (GL 4.6 core 7.6.2.2 rules 2-4; SPIRV-Cross derives the same numbers, which is what
// makes this agree with the reflected module.)
static SizeT UniformMatrixColumnStride(const TypeFacts& type, const Bool nativeFloat64) {
if (type.isDouble && nativeFloat64) {
return type.matrixRows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
}
if (type.isDouble) {
return 4 * sizeof(Float);
}
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
// for two reasons. std140 pads each column of a matrix out to a vec4 (or a dvec4), so a
// mat3 spans 48 bytes even though only 36 of them carry components. And a 64-bit float
// may have been narrowed to 32 before the module reached the backend
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting
// whichever module was produced - so on a DEMOTED program a `double` uniform occupies
// exactly what its float-typed twin would, half its GL type size, and a `dmat4` is padded
// like any other 32-bit matrix. On a program that kept its doubles it occupies the full
// GL type size and its matrix columns are twice as far apart. `nativeFloat64` is the
// program's own SpirvArtifacts flag, never a live backend read: it describes the modules
// that were actually built. Anything reading or writing a whole uniform's storage - a
// bounds check, a copy between two programs' shadows - wants this rather than
// GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize,
const Bool nativeFloat64 = false) {
if (type.isMatrix) {
return static_cast<SizeT>(type.matrixCols) * UniformMatrixColumnStride(type, nativeFloat64);
}
if (type.isDouble && !nativeFloat64) {
return tightSize / 2;
}
return tightSize;
}
// Whether this program's modules KEPT their 64-bit floats. Joins phase B, like every
// other question about the global UBO's layout - and it is one: it decides how wide a
// `double` uniform's slot is.
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
SizeT GetUniformStorageSpanInBytes(Uint location) const {
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location));
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
UsesNativeFloat64());
}
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
@@ -1323,6 +1344,15 @@ namespace MobileGL::MG_State::GLState {
// not drawable, which the backends already express through their link-status
// gates.
Bool spirvStatus = false;
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
// different offset in each. Recorded here rather than re-derived from the backend
// because it is the layout THESE modules were built with: it is what the routing
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
// width the shader actually declares.
Bool nativeFloat64 = false;
};
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
@@ -122,7 +122,14 @@ namespace MobileGL::MG_State::GLState {
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
artifacts.enableSpirvValidation = enableSpirvValidation;
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
// Whether this backend consumes 64-bit floats itself. Read off the SNAPSHOT, like every
// other environment question this node asks: a worker may not touch
// MG_Backend::pActiveBackendObject, and the answer has to be the one the L1 key was built
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
// one answer could be handed back under the other.
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
nativeFloat64);
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
// them here rather than at the end of the body, which is ~87% of this node's runtime
@@ -181,7 +188,7 @@ namespace MobileGL::MG_State::GLState {
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
const Bool deferOutputValidationForDirectVulkan,
const Bool enableSpirvValidation) {
const Bool enableSpirvValidation, const Bool nativeFloat64) {
/* As we passed first stage compilation/linking,
* we'll assume all the operations here should
* pass. We may be able to employ some optimizations
@@ -209,12 +216,39 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
artifacts.generatedSpirv.size());
// The fp64 verdict, taken ONCE for the whole program and before any module is touched.
//
// Per program rather than per module, and that is forced by the global UBO: all stages
// read one buffer whose layout is derived by reflecting the modules, so a vertex stage
// that stored a `uniform double` as 4 bytes next to a fragment stage that stored it as 8
// would put every uniform after it somewhere different in each, and the routing table
// (one offset per location) could only describe one of them.
//
// The exception itself is the vertex INPUT: no backend here can fetch a 64-bit attribute,
// and VertexInputStateFactory picks the format from the VAO attribute without ever seeing
// what the shader declared, so a Float64 input would meet a narrowed float32 stream. One
// such stage demotes the whole program, which is exactly what every backend without
// native fp64 does to it anyway.
Bool keepFloat64 = nativeFloat64;
if (keepFloat64) {
for (const auto& spv : artifacts.generatedSpirv) {
if (ShaderCompiler::ModuleDeclaresFloat64VertexInput(spv)) {
keepFloat64 = false;
MGLOG_D("ProgramObject %u: a vertex stage declares a 64-bit float input; demoting the "
"whole program despite native fp64",
externalIndex);
break;
}
}
}
artifacts.nativeFloat64 = keepFloat64;
// Linked SPIR-V generated, sanitize and optimize it
Bool allOptimized = true;
{
for (auto& spv : artifacts.generatedSpirv) {
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation, keepFloat64);
if (!success) {
// The one genuine phase-B failure mode: one of the seven optimizer passes
// reported failure, so `spv` is whatever the run left behind. A fordebug
@@ -66,7 +66,8 @@ namespace MobileGL::MG_State::GLState {
void RunBody() override;
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
Bool nativeFloat64);
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
@@ -434,8 +434,28 @@ namespace MobileGL {
}
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
const Bool mipmapped =
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
// A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly
// one level and says its sampler state is not used at all - texelFetch is the only way
// a shader can read it - so 8.14's filter-completeness rules, which is what the
// `mipmapped` branch below asks about, never apply to it.
//
// Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture
// look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture
// that can only ever have one level never satisfies the mip-chain check. Both backends
// treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in
// ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the
// shader declared was left pointing at nothing and every texelFetch read zero. That is
// the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture,
// which fails at the first data7 element with the multisample texture correctly
// cleared and simply never bound.
//
// IsCopyImageEndpointComplete already spells the same guard as
// CopyImageTargetHasMipmapChain; this was the one place that asked without it.
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
const Bool filtered = target != TextureTarget::Texture2DMultisample &&
target != TextureTarget::Texture2DMultisampleArray;
const Bool mipmapped = filtered && effectiveSampler != nullptr &&
effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped);
}
@@ -32,6 +32,23 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_executable(
ViewportIndexRoutingTest
ViewportIndexRoutingTest.cpp
)
target_include_directories(ViewportIndexRoutingTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ViewportIndexRoutingTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ViewportIndexRoutingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -160,8 +160,12 @@ TEST(SplitReadWriteImageUniformsTest, ExemptFormatsAreLeftCompletelyAlone) {
}
}
// A declaration SPIRV-Cross already qualified is none of this pass's business.
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreUntouched) {
// A declaration SPIRV-Cross already qualified needs no REPAIR - but it still needs the rename.
// The input to this pass is SPIRV-Cross output, not application source, and SPIRV-Cross picks
// `readonly` or `writeonly` from the accesses of the stage it is emitting, so "already qualified"
// says nothing about whether the other stages spell it the same way. The qualifiers must survive
// untouched; only the identifier changes.
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreRenamedButNotRequalified) {
const String source = R"(#version 320 es
layout(binding = 0, rgba8) uniform readonly highp image2D reader;
layout(binding = 1, rgba8) uniform writeonly highp image2D writer;
@@ -170,9 +174,33 @@ void main()
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
}
)";
// Untouched means UNRENAMED too: a declaration that already carries its qualifier in the
// source carries the SAME one in every stage, so there is no cross-stage mismatch to break up
// and renaming it would only churn the text.
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "layout(binding = 0, rgba8) uniform readonly highp image2D " +
RoAlias("reader") + ";"))
<< out;
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " +
WoAlias("writer") + ";"))
<< out;
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("writer") + ",")) << out;
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("reader") + ",")) << out;
// Neither declaration is doubled and neither gains a qualifier it did not have: this is a
// rename, not a repair.
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX)) << out;
EXPECT_EQ(CountOf(out, "coherent"), 0u) << out;
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
}
// A declaration carrying BOTH qualifiers is a spelling no per-stage access analysis produces, so
// it came from the application and reads the same in every stage. Nothing to rename.
TEST(SplitReadWriteImageUniformsTest, ADeclarationQualifiedBothWaysIsLeftCompletelyAlone) {
const String source = R"(#version 320 es
layout(binding = 0, rgba8) uniform readonly writeonly highp image2D inert;
void main()
{
highp ivec2 size = imageSize(inert);
if (size.x < 0) discard;
}
)";
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
}
@@ -494,6 +522,67 @@ void main()
EXPECT_TRUE(Contains(fsOut, "binding = 0"));
}
// The same defect, in the shape it actually reaches the driver in. SPIRV-Cross emits the access
// qualifier ITSELF whenever the stage only loads or only stores, so the declaration arrives here
// already legal - and this pass used to skip it on exactly that ground, leaving the vertex stage's
// `coherent writeonly g_image` and the fragment stage's `coherent readonly g_image` sharing one
// name. That is the pair a raw-ES probe on the Adreno 830 reproduces with no MobileGL in the
// process: the fragment stage reads back the untouched zeros
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation's [1,0,0,0.2]), and
// renaming either half fixes it. This is the emitted text of that test, verbatim.
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedDifferentlyGetDifferentNames) {
const String vertexSource = R"(#version 320 es
layout(binding = 1, rgba32f) uniform coherent writeonly highp image2D g_image;
void main()
{
imageStore(g_image, ivec2(0), vec4(2.0));
gl_Position = vec4(0.0);
}
)";
const String fragmentSource = R"(#version 320 es
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
mg_FragColor = imageLoad(g_image, ivec2(0));
}
)";
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
const String vsName = WoAlias("g_image");
const String fsName = RoAlias("g_image");
EXPECT_NE(vsName, fsName);
EXPECT_TRUE(Contains(vsOut, "uniform coherent writeonly highp image2D " + vsName + ";")) << vsOut;
EXPECT_TRUE(Contains(fsOut, "uniform coherent readonly highp image2D " + fsName + ";")) << fsOut;
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
// Nothing left for a linker to merge and mis-qualify...
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
// ...and the image unit is still the one the application asked for.
EXPECT_TRUE(Contains(vsOut, "binding = 1")) << vsOut;
EXPECT_TRUE(Contains(fsOut, "binding = 1")) << fsOut;
}
// ...and the budget half of it: two stages SPIRV-Cross qualified the SAME way must still land on
// one shared name, or every stage that names the image spends an image location of its own.
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedAlikeShareOneName) {
const String stage = R"(#version 320 es
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
mg_FragColor = imageLoad(g_image, ivec2(0));
}
)";
const String first = SplitReadWriteImageUniforms(stage);
const String second = SplitReadWriteImageUniforms(stage);
EXPECT_EQ(first, second);
EXPECT_TRUE(Contains(first, "uniform coherent readonly highp image2D " + RoAlias("g_image") + ";"))
<< first;
}
// The other side of that coin, and the one a per-STAGE tag got wrong. Two stages that use the
// image the same way emit byte-identical declarations, so they must arrive at ONE shared name:
// Adreno allocates an image LOCATION per distinct uniform, and giving each stage its own name
@@ -0,0 +1,179 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/ViewportIndexRoutingTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// The ESSL half of the gl_ViewportIndex routing emulation (MG_Backend/DirectGLES/Managers.cpp).
// GLES has one viewport, one scissor rectangle and one depth range where GL 4.1 has sixteen of
// each selected per primitive, and the target device has no GL_OES_viewport_array to borrow, so
// DirectGLES turns the builtin into an ordinary flat varying and gives the fragment stage a gate
// the draw path replays against.
//
// Both passes are pure String -> String over what SPIRV-Cross emits once LowerViewportIndexPass
// has demoted the builtin, so no GL context and no driver: the shapes they have to survive - and
// the ones they must refuse - can be pinned here rather than only on a device. What they cannot
// pin is that the routing produces the right pixels; that is
// MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp, which runs the same claim through both
// backends.
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/Managers.h>
using MobileGL::Bool;
using MobileGL::String;
using MobileGL::MG_Backend::DirectGLES::InjectViewportIndexPassGate;
using MobileGL::MG_Backend::DirectGLES::PromoteViewportIndexGlobalToVarying;
namespace {
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// What SPIRV-Cross hands the backend for a geometry stage after LowerViewportIndexPass has
// demoted gl_ViewportIndex: a plain file-scope global the shader still writes and which, until
// this pass runs, nothing anywhere reads.
constexpr const char* kLoweredGeometryShader = R"(#version 320 es
layout(invocations = 16, points) in;
layout(max_vertices = 4, triangle_strip) out;
layout(location = 0) flat out int gsIndex;
int mg_ViewportIndex;
void main()
{
gsIndex = gl_InvocationID;
mg_ViewportIndex = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
EmitVertex();
EndPrimitive();
}
)";
constexpr const char* kFragmentShader = R"(#version 320 es
precision mediump float;
precision highp int;
layout(location = 0) flat in int gsIndex;
layout(location = 0) out highp vec4 fragColor;
void main()
{
fragColor = vec4(float(gsIndex));
}
)";
} // namespace
// The promotion itself. The declaration becomes an interface variable and the STORE is left
// exactly where it was - the pass must not touch the body, because the body is the application's.
TEST(ViewportIndexRoutingTest, TheDemotedGlobalBecomesAFlatVarying) {
String source = kLoweredGeometryShader;
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source)) << source;
EXPECT_TRUE(Contains(source, "flat out highp int mg_ViewportIndex;")) << source;
EXPECT_FALSE(Contains(source, "\nint mg_ViewportIndex;")) << source;
EXPECT_TRUE(Contains(source, " mg_ViewportIndex = gl_InvocationID;")) << source;
}
// FLAT is the semantics and not a hint: GL takes a primitive's viewport index from its provoking
// vertex, and flat interpolation is what delivers that. An interpolated integer would not even
// compile in ESSL, so losing the qualifier fails loudly - but silently losing it to a `smooth`
// rewrite somewhere downstream would route by whichever vertex the rasterizer felt like.
TEST(ViewportIndexRoutingTest, ThePromotedVaryingIsFlatAndCarriesNoExplicitLocation) {
String source = kLoweredGeometryShader;
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source));
const size_t declPos = source.find("flat out highp int mg_ViewportIndex;");
ASSERT_NE(declPos, String::npos) << source;
// No layout(location = N): the two stages are transpiled independently and cannot agree on a
// number, so the varying is matched by NAME. A location that appeared here would have to
// appear identically in the fragment stage, which nothing can guarantee.
const size_t lineStart = source.rfind('\n', declPos);
const String declLine = source.substr(lineStart + 1, declPos - lineStart - 1);
EXPECT_EQ(declLine, "") << "the declaration must start its own line, with no layout qualifier";
}
// A precision-qualified declaration is the same declaration. SPIRV-Cross prints one or the other
// depending on what the module carried, and a pass that only matched the bare form would leave
// half the drivers unrouted while reporting success.
TEST(ViewportIndexRoutingTest, APrecisionQualifiedDeclarationIsPromotedToo) {
String source = "#version 320 es\nhighp int mg_ViewportIndex;\nvoid main() { mg_ViewportIndex = 3; }\n";
ASSERT_TRUE(PromoteViewportIndexGlobalToVarying(source)) << source;
EXPECT_TRUE(Contains(source, "flat out highp int mg_ViewportIndex;")) << source;
}
// A stage that never routed must come out byte-identical, because every stage of every program on
// this backend goes through the pass.
TEST(ViewportIndexRoutingTest, AStageWithoutTheGlobalIsUntouched) {
const String before = kFragmentShader;
String source = before;
EXPECT_FALSE(PromoteViewportIndexGlobalToVarying(source));
EXPECT_EQ(source, before);
}
// The one shape that would silently break a shader: a name that ends in mg_ViewportIndex but is
// not the declaration. Only a declaration starting its own line may be rewritten.
TEST(ViewportIndexRoutingTest, ADeclarationThatIsNotAtLineStartIsRefused) {
const String before = "#version 320 es\nuniform highp int mg_ViewportIndex;\nvoid main() {}\n";
String source = before;
EXPECT_FALSE(PromoteViewportIndexGlobalToVarying(source));
EXPECT_EQ(source, before);
}
// The fragment gate. Three things have to be true at once: the varying and the uniform are
// declared, the application's entry point survives under a new name, and the new entry point
// discards on a mask miss and calls the old one otherwise.
TEST(ViewportIndexRoutingTest, TheFragmentGateWrapsTheEntryPoint) {
String source = kFragmentShader;
ASSERT_TRUE(InjectViewportIndexPassGate(source)) << source;
EXPECT_TRUE(Contains(source, "flat in highp int mg_ViewportIndex;")) << source;
EXPECT_TRUE(Contains(source, "uniform highp int mg_ViewportPassMask;")) << source;
EXPECT_TRUE(Contains(source, "void mg_ViewportGatedMain()")) << source;
EXPECT_TRUE(Contains(source, "discard;")) << source;
EXPECT_TRUE(Contains(source, "mg_ViewportGatedMain();")) << source;
// The application's body is not edited, only renamed.
EXPECT_TRUE(Contains(source, " fragColor = vec4(float(gsIndex));")) << source;
// Exactly one entry point remains, and it is the wrapper.
EXPECT_EQ(source.find("void main()"), source.rfind("void main()")) << source;
}
// The shift operand has to be clamped. GL leaves a gl_ViewportIndex outside [0, MAX_VIEWPORTS)
// undefined and the emulation is free to pick anything, but an ESSL shift by >= 32 is undefined
// in a way that can take the whole draw with it - so the gate must not be able to reach one.
TEST(ViewportIndexRoutingTest, TheGateClampsTheShiftIntoRange) {
String source = kFragmentShader;
ASSERT_TRUE(InjectViewportIndexPassGate(source));
EXPECT_TRUE(Contains(source, "mg_ViewportPassMask >> (mg_ViewportIndex & 15)")) << source;
}
// A fragment stage that READS gl_ViewportIndex has no ESSL spelling for it either, and the
// routing varying is exactly the value it wanted. This is the only place the read can be repaired
// - LowerViewportIndexPass deliberately demotes outputs only, because a demoted input would
// answer from an undefined global.
TEST(ViewportIndexRoutingTest, AFragmentStageReadOfTheBuiltinIsRedirectedOntoTheVarying) {
String source = R"(#version 320 es
precision highp int;
layout(location = 0) out highp vec4 fragColor;
void main()
{
fragColor = vec4(float(gl_ViewportIndex));
}
)";
ASSERT_TRUE(InjectViewportIndexPassGate(source)) << source;
EXPECT_FALSE(Contains(source, "gl_ViewportIndex")) << source;
EXPECT_TRUE(Contains(source, "fragColor = vec4(float(mg_ViewportIndex));")) << source;
}
// A stage the pass declines must reach the driver exactly as it arrived, not half-rewritten.
// The caller logs the decline and the program still renders - unrouted, which is the old
// behaviour - so a partially edited source here would turn a degradation into a broken shader.
TEST(ViewportIndexRoutingTest, AStageWithNoEntryPointIsDeclinedWithoutBeingEdited) {
const String before = "#version 320 es\nprecision highp int;\nhighp int f() { return gl_ViewportIndex; }\n";
String source = before;
EXPECT_FALSE(InjectViewportIndexPassGate(source));
EXPECT_EQ(source, before);
}
+16 -3
View File
@@ -3999,6 +3999,8 @@ namespace {
constexpr Uint kGlR8ui = 0x8232;
constexpr Uint kGlR32f = 0x822E;
constexpr Uint kGlRgb10A2ui = 0x906F;
constexpr Uint kGlRgb10A2 = 0x8059;
constexpr Uint kGlRgb8 = 0x8051; // not one of the forty image formats at all
} // namespace
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
@@ -4049,15 +4051,26 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
// pass on the ESSL chain and re-declares them in a core carrier SPIRV-Cross does print, so for
// those the module is the right place and the text completion would put back the narrow token no
// ES driver accepts. r8ui - which the stencil half of the packed_depth_stencil case binds - is
// one of the rescued ones; rgb10_a2ui, whose 10/10/10/2 channel widths no core format has, is not.
// one of the rescued ones, and so, now that the carriers cover all twenty-six non-core formats,
// is every other IMAGE format. What is left for the guard is a format that is not an image format
// at all: it has no carrier and no ESSL image spelling either, so baking it would put a token in
// the module that means nothing.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesOnlyTheFormatsNoCoreCarrierRescues) {
using namespace MG_Util::ShaderTranspiler;
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
<< "if SPIRV-Cross ever learns to print r8ui for ES, this route can go";
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlR8ui), 0u);
// Unprintable and rescued anyway: rgb10_a2ui's channels are unsigned INTEGER, so an rgba16ui
// holds all four outright, and rgb10_a2's are the same channels read as NORMALIZED, which the
// same carrier holds as their codes.
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2ui));
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2));
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u);
// ...and the one the guard still turns away.
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb8));
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb8), 0u);
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
@@ -4072,7 +4085,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
{ // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles.
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb10A2ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb8}}, baked));
EXPECT_EQ(baked, spirv) << "a format nothing can carry must leave the module untouched";
EXPECT_FALSE(DecompileToEssl(baked).empty());
}
+57
View File
@@ -263,6 +263,63 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
extensions.end());
}
// A multisample texture is fetched, never filtered, so the mip-chain completeness rules never
// apply to it (GL 4.6 core 8.17). It has exactly one level and MIN_FILTER's initial value is
// NEAREST_MIPMAP_LINEAR, so asking those rules anyway calls EVERY multisample texture incomplete
// - and both backends express "incomplete" as "leave the native target unbound", which makes the
// shader's sampler2DMS read zero from a texture that was written correctly.
//
// That is KHR-GL43.compute_shader.resource-texture: it clears its 2DMS texture to 123.0 through
// an FBO (which succeeds - the ES clear is issued on a COMPLETE 4-sample framebuffer with no
// error) and then fails at the first sampler2DMS element because the texture was never bound.
TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
using namespace MobileGL;
namespace DirectGLES = MG_Backend::DirectGLES;
ScopedDirectGLESTextureBindings state;
GLuint frontendTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
ASSERT_NE(frontendTexture, 0u);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, frontendTexture);
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2DMultisample)
.GetBoundObject();
ASSERT_NE(textureObject, nullptr);
textureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
textureObject->SetSamples(4);
textureObject->SetFixedSampleLocations(false);
// One level, 4x4 - the shape glTexImage2DMultisample produces, and a size whose mip chain
// would need three levels if the filter rules were (wrongly) applied.
MG_State::GLState::AsMipmapTexture(textureObject.get())
->AllocateStorage(TextureUploadTarget::Texture2DMultisample, 0, {{4, 4, 1}, 4});
// The precondition that used to poison it, asserted rather than assumed: the texture's own
// sampler still reports a mipmapping filter, because GL's initial MIN_FILTER is
// NEAREST_MIPMAP_LINEAR and a multisample texture has no way (and no reason) to change it.
// If a future default made this None the test would pass without covering anything.
const auto& sampler = textureObject->GetSamplerObject();
ASSERT_NE(sampler, nullptr);
ASSERT_NE(sampler->GetMipmapMode(), SamplerMipmapMode::None)
<< "fixture is stale: the default sampler no longer asks for mipmapping, so this test "
"would not exercise the multisample guard";
EXPECT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), sampler.get()))
<< "a multisample texture is never filter-incomplete";
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
// The symptom itself: the per-unit walk has to actually bind it.
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 1u)
<< "the multisample texture was not bound; every texelFetch against it reads zero";
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D_MULTISAMPLE);
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
}
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
using namespace MobileGL;
namespace DirectGLES = MG_Backend::DirectGLES;
+16
View File
@@ -15,5 +15,21 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
${LINK_LIBRARIES}
)
add_executable(
DriverBugProbesTest
DriverBugProbesTest.cpp
)
target_include_directories(DriverBugProbesTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(DriverBugProbesTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,660 @@
// MobileGL - MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <MG_Util/SelfTest/DriverBugProbes.h>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::SelfTest::CollectGlesKnownDriverBugs;
using MobileGL::MG_Util::SelfTest::DriverBugVerdict;
using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites;
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
namespace {
// A driver table with nothing resolved. Every probe has to treat this as "cannot tell",
// never as "affected".
MG_External::GLESFunctionsTable EmptyFunctionTable() {
return MG_External::GLESFunctionsTable{};
}
// ===================== THE FAKE DRIVER =====================
//
// Same idea as the fake GLES table BackendLoaderTest drives the gl_InstanceID probe with:
// captureless lambdas over one file-scope state, with per-test knobs that turn each defect
// on and off. It is deliberately a MODEL of the defect rather than a canned answer - the
// fake reads the shader text the probe actually submitted and reproduces what the affected
// driver does with it, so a probe that stopped building the triggering shape would stop
// detecting, which is exactly what these tests are for.
//
// These tests call the Probe* functions directly rather than through
// CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a
// memo latched by one test would decide the answer for every later one.
// The exact text an affected Adreno driver puts in the info log for this refusal.
const char* const kImageLocationLinkLog =
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
struct FakeDriver {
// ---- limits the probes gate on -------------------------------------
GLint maxColorTextureSamples = 4;
GLint maxImageUnits = 8;
GLint maxVertexImageUniforms = 8;
GLint maxFragmentImageUniforms = 8;
GLint maxGeometryImageUniforms = 3;
// The landed geometry probe reads this; zero keeps it inert so it cannot interfere.
GLint maxGeometrySsboBlocks = 0;
bool geometryImageLimitQueryRaisesError = false;
bool colorTextureSamplesQueryRaisesError = false;
// ---- defect knobs ---------------------------------------------------
// Probe 1: a swizzled-alpha, non-zero-sample .w fetch reads garbage from the second
// sampling program onward.
bool msaaSwizzledAlphaCorrupted = false;
// Probe 1's inconclusive path: EVERY sampled read is wrong, including the controls.
bool msaaEveryReadWrong = false;
// Probe 2: the link fails once the program declares more distinct image uniform NAMES
// than this.
int distinctImageNameBudget = 1000;
// Probe 3: a same-name coherent writeonly/readonly pair loses the writing stage's store.
bool sameNameImagePairDropsWrites = false;
// Probe 3's inconclusive path: the renamed control loses it too.
bool everyVertexImageWriteDropped = false;
// Probe 4: how many texels the in-invocation dependent read misses under the STRONGEST
// shape, how many it misses under the shape MobileGL emits today, and whether the
// two-draw control misses them too.
int coherencyStrongestShapeFailedTexels = 0;
int coherencyEmittedShapeFailedTexels = 0;
int coherencyControlFailedTexels = 0;
// ---- object bookkeeping ---------------------------------------------
GLenum pendingError = GL_NO_ERROR;
GLuint nextShaderId = 1;
GLuint nextProgramId = 1;
GLuint nextTextureId = 1;
GLuint nextFramebufferId = 1;
GLuint nextVertexArrayId = 1;
int aliveShaders = 0;
int alivePrograms = 0;
int aliveTextures = 0;
int aliveFramebuffers = 0;
int aliveVertexArrays = 0;
std::map<GLuint, std::string> shaderSources;
std::map<GLuint, std::vector<GLuint>> programShaders;
std::map<GLuint, bool> programLinked;
std::map<GLuint, std::string> programInfoLogs;
// texture id -> GL_TEXTURE_SWIZZLE_A
std::map<GLuint, GLenum> multisampleAlphaSwizzle;
GLuint boundMultisampleTexture = 0;
GLuint currentProgram = 0;
// How many programs that sample a multisample texture have been linked so far. The
// corruption starts at the second.
int sampledMultisampleProgramCount = 0;
// Set by glDrawArrays, consumed by glReadPixels.
GLfloat lastSampledValue = 1.0f;
int lastFailedTexelCount = 0;
};
FakeDriver g_fake;
void ResetFakeDriver() { g_fake = FakeDriver{}; }
const std::string& SourceOf(GLuint shader) {
static const std::string empty;
const auto it = g_fake.shaderSources.find(shader);
return it == g_fake.shaderSources.end() ? empty : it->second;
}
bool Contains(const std::string& haystack, const char* needle) {
return haystack.find(needle) != std::string::npos;
}
// Every `image2D <name>` the program declares, across all its stages.
std::vector<std::string> DeclaredImageNames(GLuint program) {
std::vector<std::string> names;
const auto attached = g_fake.programShaders.find(program);
if (attached == g_fake.programShaders.end()) return names;
for (const GLuint shader : attached->second) {
const std::string& source = SourceOf(shader);
std::size_t at = 0;
while ((at = source.find("image2D ", at)) != std::string::npos) {
at += std::strlen("image2D ");
const std::size_t end = source.find_first_of(";,)", at);
if (end == std::string::npos) break;
std::string name = source.substr(at, end - at);
while (!name.empty() && (name.back() == ' ' || name.back() == '\t')) name.pop_back();
if (std::find(names.begin(), names.end(), name) == names.end()) {
names.push_back(name);
}
at = end;
}
}
return names;
}
std::string StageSourceContaining(GLuint program, const char* needle) {
const auto attached = g_fake.programShaders.find(program);
if (attached == g_fake.programShaders.end()) return {};
for (const GLuint shader : attached->second) {
const std::string& source = SourceOf(shader);
if (Contains(source, needle)) return source;
}
return {};
}
// The uniform name in `... image2D <name>;` of the first declaration in `source`.
std::string FirstImageNameIn(const std::string& source) {
const std::size_t at = source.find("image2D ");
if (at == std::string::npos) return {};
const std::size_t start = at + std::strlen("image2D ");
const std::size_t end = source.find(';', start);
if (end == std::string::npos) return {};
return source.substr(start, end - start);
}
// Whatever the sampling vertex shader asked for: `texelFetch(mg_probeSampler, ivec2(0), N).C`.
void ParseSampledFetch(const std::string& source, int& sampleIndex, char& component) {
sampleIndex = -1;
component = '?';
const std::size_t at = source.find("texelFetch(mg_probeSampler, ivec2(0), ");
if (at == std::string::npos) return;
const std::size_t start = at + std::strlen("texelFetch(mg_probeSampler, ivec2(0), ");
sampleIndex = std::atoi(source.c_str() + start);
const std::size_t dot = source.find(").", start);
if (dot != std::string::npos && dot + 2 < source.size()) component = source[dot + 2];
}
MG_External::GLESFunctionsTable MakeFakeGLESFunctions() {
MG_External::GLESFunctionsTable funcs{};
funcs.glGetError = []() -> GLenum {
const GLenum error = g_fake.pendingError;
g_fake.pendingError = GL_NO_ERROR;
return error;
};
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
switch (pname) {
case GL_MAX_COLOR_TEXTURE_SAMPLES:
if (g_fake.colorTextureSamplesQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxColorTextureSamples;
}
break;
case GL_MAX_IMAGE_UNITS:
*data = g_fake.maxImageUnits;
break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms;
break;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
*data = g_fake.maxFragmentImageUniforms;
break;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
if (g_fake.geometryImageLimitQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxGeometryImageUniforms;
}
break;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*data = g_fake.maxGeometrySsboBlocks;
break;
default:
break;
}
};
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) { *data = 0; };
funcs.glGetFloatv = [](GLenum, GLfloat* data) {
data[0] = 0.0f;
data[1] = 0.0f;
data[2] = 0.0f;
data[3] = 0.0f;
};
funcs.glIsEnabled = [](GLenum) -> GLboolean { return GL_FALSE; };
funcs.glEnable = [](GLenum) {};
funcs.glDisable = [](GLenum) {};
funcs.glFinish = []() {};
funcs.glMemoryBarrier = [](GLbitfield) {};
funcs.glPixelStorei = [](GLenum, GLint) {};
funcs.glViewport = [](GLint, GLint, GLsizei, GLsizei) {};
funcs.glClear = [](GLbitfield) {};
funcs.glClearColor = [](GLfloat, GLfloat, GLfloat, GLfloat) {};
funcs.glActiveTexture = [](GLenum) {};
// ---- shaders and programs -------------------------------------------
funcs.glCreateShader = [](GLenum) -> GLuint {
++g_fake.aliveShaders;
return g_fake.nextShaderId++;
};
funcs.glShaderSource = [](GLuint shader, GLsizei count, const GLchar* const* strings,
const GLint*) {
std::string source;
for (GLsizei i = 0; i < count; ++i) {
if (strings[i] != nullptr) source += strings[i];
}
g_fake.shaderSources[shader] = std::move(source);
};
funcs.glCompileShader = [](GLuint) {};
funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) {
if (pname == GL_COMPILE_STATUS) *params = GL_TRUE;
};
funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
if (bufSize > 0) infoLog[0] = '\0';
};
funcs.glDeleteShader = [](GLuint shader) {
if (shader != 0) --g_fake.aliveShaders;
};
funcs.glCreateProgram = []() -> GLuint {
++g_fake.alivePrograms;
return g_fake.nextProgramId++;
};
funcs.glAttachShader = [](GLuint program, GLuint shader) {
g_fake.programShaders[program].push_back(shader);
};
funcs.glLinkProgram = [](GLuint program) {
const std::vector<std::string> names = DeclaredImageNames(program);
const bool overBudget = static_cast<int>(names.size()) > g_fake.distinctImageNameBudget;
g_fake.programLinked[program] = !overBudget;
g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : "";
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
++g_fake.sampledMultisampleProgramCount;
}
};
funcs.glGetProgramiv = [](GLuint program, GLenum pname, GLint* params) {
if (pname != GL_LINK_STATUS) return;
const auto it = g_fake.programLinked.find(program);
*params = (it == g_fake.programLinked.end() || it->second) ? GL_TRUE : GL_FALSE;
};
funcs.glGetProgramInfoLog = [](GLuint program, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
if (bufSize <= 0) return;
const auto it = g_fake.programInfoLogs.find(program);
const std::string& log = it == g_fake.programInfoLogs.end() ? std::string() : it->second;
const GLsizei copied = static_cast<GLsizei>(
std::min<std::size_t>(log.size(), static_cast<std::size_t>(bufSize - 1)));
std::memcpy(infoLog, log.data(), static_cast<std::size_t>(copied));
infoLog[copied] = '\0';
};
funcs.glDeleteProgram = [](GLuint program) {
if (program != 0) --g_fake.alivePrograms;
};
funcs.glUseProgram = [](GLuint program) { g_fake.currentProgram = program; };
funcs.glGetUniformLocation = [](GLuint, const GLchar*) -> GLint { return 0; };
funcs.glUniform1i = [](GLint, GLint) {};
// ---- textures, framebuffers, vertex arrays ---------------------------
funcs.glGenTextures = [](GLsizei n, GLuint* textures) {
for (GLsizei i = 0; i < n; ++i) {
textures[i] = g_fake.nextTextureId++;
++g_fake.aliveTextures;
}
};
funcs.glBindTexture = [](GLenum target, GLuint texture) {
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
};
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
for (GLsizei i = 0; i < n; ++i) {
if (textures[i] != 0) --g_fake.aliveTextures;
}
};
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
if (target == GL_TEXTURE_2D_MULTISAMPLE && pname == GL_TEXTURE_SWIZZLE_A) {
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
static_cast<GLenum>(param);
}
};
funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
const void*) {};
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
const void*) {};
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
funcs.glTexStorage2DMultisample = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei, GLboolean) {};
funcs.glGenFramebuffers = [](GLsizei n, GLuint* framebuffers) {
for (GLsizei i = 0; i < n; ++i) {
framebuffers[i] = g_fake.nextFramebufferId++;
++g_fake.aliveFramebuffers;
}
};
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
for (GLsizei i = 0; i < n; ++i) {
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
}
};
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
for (GLsizei i = 0; i < n; ++i) {
arrays[i] = g_fake.nextVertexArrayId++;
++g_fake.aliveVertexArrays;
}
};
funcs.glBindVertexArray = [](GLuint) {};
funcs.glDeleteVertexArrays = [](GLsizei n, const GLuint* arrays) {
for (GLsizei i = 0; i < n; ++i) {
if (arrays[i] != 0) --g_fake.aliveVertexArrays;
}
};
funcs.glBindImageTexture = [](GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum) {};
// ---- the draw, where the defects live --------------------------------
funcs.glDrawArrays = [](GLenum, GLint, GLsizei) {
const GLuint program = g_fake.currentProgram;
const std::string sampling = StageSourceContaining(program, "texelFetch(mg_probeSampler");
if (!sampling.empty()) {
int sampleIndex = -1;
char component = '?';
ParseSampledFetch(sampling, sampleIndex, component);
const GLenum swizzle = g_fake.multisampleAlphaSwizzle.count(
g_fake.boundMultisampleTexture) != 0
? g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture]
: GL_ALPHA;
// An R32F texel filled with (1, 0, 0, -) reads 1.0 through both the ALPHA and the
// RED swizzle sources, which is why one expected constant covers every shape.
g_fake.lastSampledValue = 1.0f;
if (g_fake.msaaEveryReadWrong) {
g_fake.lastSampledValue = 0.0f;
} else if (g_fake.msaaSwizzledAlphaCorrupted && swizzle == GL_RED && component == 'w' &&
sampleIndex != 0 && g_fake.sampledMultisampleProgramCount >= 2) {
// Uninitialised memory: a value that is neither the answer nor the clear.
g_fake.lastSampledValue = -1.34954e-17f;
}
return;
}
// Matched on the access qualifier alone, not on "coherent writeonly": the strongest
// coherency shape spells it "coherent volatile writeonly".
const std::string writeStage = StageSourceContaining(program, "writeonly");
const std::string readStage = StageSourceContaining(program, "readonly");
if (!writeStage.empty() && !readStage.empty() && Contains(readStage, "memoryBarrierImage")) {
// The coherency probe: one invocation stores and then reads back. `volatile` is
// what tells the strongest shape apart from the one MobileGL emits today, and
// giving them separate knobs is what lets a test pin the case where only the
// emitted shape is wrong - a fixable defect that must not be reported here.
g_fake.lastFailedTexelCount = Contains(readStage, "coherent volatile")
? g_fake.coherencyStrongestShapeFailedTexels
: g_fake.coherencyEmittedShapeFailedTexels;
return;
}
if (!writeStage.empty() && readStage.empty()) {
// The coherency control's store half; the load half decides the result.
g_fake.lastFailedTexelCount = 0;
return;
}
if (writeStage.empty() && !readStage.empty()) {
g_fake.lastFailedTexelCount = g_fake.coherencyControlFailedTexels;
return;
}
if (!writeStage.empty() && !readStage.empty()) {
// The qualifier-merge pair: the stores are lost when the two halves share a name.
const bool sharedName =
FirstImageNameIn(writeStage) == FirstImageNameIn(readStage) &&
!FirstImageNameIn(writeStage).empty();
const bool lost = g_fake.everyVertexImageWriteDropped ||
(g_fake.sameNameImagePairDropsWrites && sharedName);
g_fake.lastFailedTexelCount = lost ? 1 << 20 : 0;
return;
}
g_fake.lastFailedTexelCount = 0;
};
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels) {
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
if (format == GL_RED && type == GL_FLOAT) {
GLfloat* out = static_cast<GLfloat*>(pixels);
for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue;
return;
}
GLubyte* out = static_cast<GLubyte*>(pixels);
const std::size_t failed =
std::min<std::size_t>(texels, static_cast<std::size_t>(g_fake.lastFailedTexelCount));
for (std::size_t i = 0; i < texels; ++i) {
const bool ok = i >= failed;
out[i * 4 + 0] = ok ? 0 : 255;
out[i * 4 + 1] = ok ? 255 : 0;
out[i * 4 + 2] = 0;
out[i * 4 + 3] = 255;
}
};
return funcs;
}
void ExpectProbeReleasedEverything() {
EXPECT_EQ(g_fake.aliveShaders, 0);
EXPECT_EQ(g_fake.alivePrograms, 0);
EXPECT_EQ(g_fake.aliveTextures, 0);
EXPECT_EQ(g_fake.aliveFramebuffers, 0);
EXPECT_EQ(g_fake.aliveVertexArrays, 0);
}
} // namespace
// The rule the whole section depends on: a probe that cannot run reports NO bug. If an
// unrunnable probe answered "affected", every device without the entry points - every desktop
// build, every unit-test process - would grow a driver-bug row it has no evidence for, and the
// section would stop meaning "this device has these bugs".
TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
EXPECT_FALSE(ProbeGeometryStageSsboWriteAfterEmitDropped(gl))
<< "a probe with no entry points to call must not claim the driver is affected";
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
}
// The section lists only bugs the device HAS, so a driver nothing could be probed on renders
// nothing at all rather than a list of reassurances.
TEST(DriverBugProbes, CollectsNoFindingsWhenNothingCanBeProbed) {
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
EXPECT_TRUE(CollectGlesKnownDriverBugs(gl).empty());
}
// Every finding the table can produce is a bug that is PRESENT, which is why the vocabulary is
// FIXED/UNFIXABLE and not PASS/FAIL. This latches that no probe can smuggle in a "not affected"
// row by returning a finding with an empty name or detail - the screen renders both.
TEST(DriverBugProbes, EveryFindingCarriesANameAndAnExplanation) {
const MG_External::GLESFunctionsTable gl = EmptyFunctionTable();
for (const auto& finding : CollectGlesKnownDriverBugs(gl)) {
EXPECT_FALSE(finding.name.empty());
EXPECT_FALSE(finding.detail.empty()) << finding.name << " must say what MobileGL does about it";
EXPECT_TRUE(finding.verdict == DriverBugVerdict::Fixed ||
finding.verdict == DriverBugVerdict::Unfixable);
}
}
// ===================== R32F MULTISAMPLE SWIZZLE =====================
TEST(DriverBugProbes, R32FMultisampleSwizzleIsCleanOnAConformingDriver) {
ResetFakeDriver();
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
ExpectProbeReleasedEverything();
}
TEST(DriverBugProbes, R32FMultisampleSwizzleIsDetectedFromTheSecondProgramOnward) {
ResetFakeDriver();
g_fake.msaaSwizzledAlphaCorrupted = true;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_TRUE(ProbeR32FMultisampleSwizzleCorruption(gl));
ExpectProbeReleasedEverything();
}
// The control rule, made executable: a driver on which even the default-swizzle, sample-zero and
// .x reads are wrong is broken in some larger way, and the probe may not name the alpha swizzle
// as the cause.
TEST(DriverBugProbes, R32FMultisampleSwizzleReportsNothingWhenTheControlsAreWrongToo) {
ResetFakeDriver();
g_fake.msaaSwizzledAlphaCorrupted = true;
g_fake.msaaEveryReadWrong = true;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl))
<< "with every read wrong the probe has no evidence that the alpha swizzle is the variable";
}
TEST(DriverBugProbes, R32FMultisampleSwizzleNeedsMoreThanOneSample) {
ResetFakeDriver();
g_fake.msaaSwizzledAlphaCorrupted = true;
g_fake.maxColorTextureSamples = 1;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
}
// ===================== IMAGE LOCATION PER NAME =====================
TEST(DriverBugProbes, ImageLocationBudgetIsCleanWhenNamesDoNotCost) {
ResetFakeDriver();
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
const auto measurement = ProbeImageLocationPerNameBudget(gl);
EXPECT_FALSE(measurement.detected);
ExpectProbeReleasedEverything();
}
TEST(DriverBugProbes, ImageLocationBudgetIsDetectedWhenOnlyTheSharedNamesLink) {
ResetFakeDriver();
// Four image uniforms per stage: twelve distinct names in the subject, four in the control.
g_fake.distinctImageNameBudget = 5;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
const auto measurement = ProbeImageLocationPerNameBudget(gl);
EXPECT_TRUE(measurement.detected);
EXPECT_EQ(measurement.perStageImageUniforms, g_fake.maxGeometryImageUniforms + 1);
EXPECT_EQ(measurement.subjectDistinctNames, measurement.perStageImageUniforms * 3);
EXPECT_EQ(measurement.controlDistinctNames, measurement.perStageImageUniforms);
EXPECT_NE(measurement.driverMessage.find("exceeds max allowed"), String::npos)
<< "the report quotes the driver rather than paraphrasing it";
ExpectProbeReleasedEverything();
}
// The control rule again: when the shared-name program is refused too, the shape is simply too
// big for this driver and the refusal is honest.
TEST(DriverBugProbes, ImageLocationBudgetReportsNothingWhenTheControlAlsoFails) {
ResetFakeDriver();
g_fake.distinctImageNameBudget = 2;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
}
TEST(DriverBugProbes, ImageLocationBudgetNeedsAGeometryStageThatCanHoldImages) {
ResetFakeDriver();
g_fake.distinctImageNameBudget = 5;
g_fake.maxGeometryImageUniforms = 0;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
}
TEST(DriverBugProbes, ImageLocationBudgetStaysSilentOnAContextWithoutTheGeometryLimit) {
ResetFakeDriver();
g_fake.distinctImageNameBudget = 5;
g_fake.geometryImageLimitQueryRaisesError = true;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected)
<< "a pre-ES-3.2 context has no geometry stage to build the shape out of";
}
// ===================== CROSS-STAGE QUALIFIER MERGE =====================
TEST(DriverBugProbes, QualifierMergeIsCleanWhenTheDriverKeepsTheStore) {
ResetFakeDriver();
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
ExpectProbeReleasedEverything();
}
TEST(DriverBugProbes, QualifierMergeIsDetectedWhenOnlyTheSharedNameLosesTheStore) {
ResetFakeDriver();
g_fake.sameNameImagePairDropsWrites = true;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_TRUE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
ExpectProbeReleasedEverything();
}
// A driver that loses the RENAMED store too cannot write images from the vertex stage at all -
// a different and much larger claim, which this probe may not make.
TEST(DriverBugProbes, QualifierMergeReportsNothingWhenTheRenamedControlAlsoFails) {
ResetFakeDriver();
g_fake.sameNameImagePairDropsWrites = true;
g_fake.everyVertexImageWriteDropped = true;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
}
TEST(DriverBugProbes, QualifierMergeNeedsVertexStageImageUniforms) {
ResetFakeDriver();
g_fake.sameNameImagePairDropsWrites = true;
g_fake.maxVertexImageUniforms = 0;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
}
// ===================== IMAGE COHERENCY RESIDUAL =====================
TEST(DriverBugProbes, ImageCoherencyIsCleanWhenTheDependentReadObservesTheStore) {
ResetFakeDriver();
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
EXPECT_FALSE(measurement.detected);
ExpectProbeReleasedEverything();
}
TEST(DriverBugProbes, ImageCoherencyResidualIsDetectedAndQuantified) {
ResetFakeDriver();
g_fake.coherencyStrongestShapeFailedTexels = 376;
g_fake.coherencyEmittedShapeFailedTexels = 418;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
EXPECT_TRUE(measurement.detected);
EXPECT_EQ(measurement.mismatchedTexels, 376);
EXPECT_EQ(measurement.emittedShapeMismatchedTexels, 418)
<< "the row reports what applications get, not only what is theoretically reachable";
EXPECT_GT(measurement.totalTexels, 418) << "the report needs a denominator to quote a rate";
ExpectProbeReleasedEverything();
}
// The reason the subject is the STRONGEST shape and not the one MobileGL emits. Mesa llvmpipe
// misses every texel with `coherent` + memoryBarrierImage() and none once the pair is also
// `volatile` - a defect MobileGL could fix by emitting a different shape, which is not what
// UNFIXABLE means and does not belong in this section.
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenAStrongerShapeWouldFixIt) {
ResetFakeDriver();
g_fake.coherencyStrongestShapeFailedTexels = 0;
g_fake.coherencyEmittedShapeFailedTexels = 4096;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected)
<< "a driver the volatile shape satisfies has a fixable defect, not an unfixable one";
}
// The control rule once more: a driver whose glFinish-separated two-draw dependency is ALSO
// dirty has a bigger defect than an in-invocation ordering residual, and this probe must not
// dress that up as one.
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlIsDirtyToo) {
ResetFakeDriver();
g_fake.coherencyStrongestShapeFailedTexels = 376;
g_fake.coherencyControlFailedTexels = 4096;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
}
TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
ResetFakeDriver();
g_fake.coherencyStrongestShapeFailedTexels = 376;
g_fake.maxFragmentImageUniforms = 1;
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
}
@@ -8,6 +8,7 @@ add_executable(
FixIterationRPSubgroupScratchTest.cpp
EmulateSubgroupsTest.cpp
DemoteFloat64Test.cpp
FlattenFloat64StorageBlockTest.cpp
FlattenXfbInterfaceBlocksTest.cpp
UniquifyIoBlockNamesTest.cpp
LowerViewportIndexTest.cpp
@@ -379,6 +379,90 @@ TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
}
// ---------------------------------------------------------------------------
// The capability gate. A backend that consumes 64-bit floats itself gets none of this.
// ---------------------------------------------------------------------------
namespace {
// Everything kWideVertexSource has except the 64-bit vertex INPUT, which is what the
// whole-program demotion falls back for. A fragment stage, so there is no input to have.
constexpr const char* kWideFragmentSource = R"(#version 460 core
layout(std140, binding = 0) uniform Blk {
float a;
double d;
dvec2 v2;
dvec4 v4;
dmat4 m4;
double arr[3];
};
layout(location = 0) uniform double uScale;
layout(location = 0) in vec3 inNormal;
layout(location = 0) out float fOut;
void main() {
double s = d * uScale + a;
s += v2.x + v4.y + m4[0].z + arr[0] + arr[1] + arr[2] + 0.5lf;
fOut = float(s) + inNormal.x;
}
)";
} // namespace
// THE NEGATIVE CONTROL for the whole change: the identical module through the identical entry
// point answers both ways, and the only thing that moved is the capability argument.
TEST_F(DemoteFloat64Test, TheSharedChainKeepsFloat64WhenTheBackendConsumesIt) {
const Vector<Uint32> input = CompileToSpirv(GL_FRAGMENT_SHADER, kWideFragmentSource);
ASSERT_FALSE(input.empty());
ASSERT_TRUE(DeclaresFloat64Capability(input));
ASSERT_GT(CountFloatTypesOfWidth(input, 64), 0u);
Vector<Uint32> native;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, native, true, true, true));
EXPECT_TRUE(DeclaresFloat64Capability(native)) << Disassemble(native);
EXPECT_GT(CountFloatTypesOfWidth(native, 64), 0u) << Disassemble(native);
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(native));
Vector<Uint32> demoted;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, demoted, true, true, false));
EXPECT_FALSE(DeclaresFloat64Capability(demoted)) << Disassemble(demoted);
EXPECT_EQ(CountFloatTypesOfWidth(demoted, 64), 0u) << Disassemble(demoted);
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
EXPECT_NE(native, demoted);
}
// The exception the vertex path needs, at the level ProgramSpirvTask asks it: no backend here can
// FETCH 64 bits, so a stage that declares a Float64 input is demoted whole even where the rest of
// its doubles could have survived.
TEST_F(DemoteFloat64Test, AFloat64VertexInputIsRecognisedAndOnlyOnAVertexStage) {
const Vector<Uint32> vertexWithDoubleInput = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
ASSERT_FALSE(vertexWithDoubleInput.empty());
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(vertexWithDoubleInput));
// Doubles everywhere but the inputs: the same verdict must be false, or nothing would ever
// take the native path.
const Vector<Uint32> fragmentWithDoubles = CompileToSpirv(GL_FRAGMENT_SHADER, kWideFragmentSource);
ASSERT_FALSE(fragmentWithDoubles.empty());
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(fragmentWithDoubles));
// A vertex stage whose doubles are all internal is fine too - it is the INPUT that cannot be
// fed, not the stage.
const String vertexWithoutDoubleInput = R"(#version 460 core
layout(location = 0) uniform double uScale;
layout(location = 0) in vec3 inPos;
layout(location = 0) out float vOut;
void main() {
double s = double(inPos.x) * uScale + 0.5lf;
vOut = float(s);
gl_Position = vec4(float(s));
}
)";
const Vector<Uint32> internalOnly = CompileToSpirv(GL_VERTEX_SHADER, vertexWithoutDoubleInput);
ASSERT_FALSE(internalOnly.empty());
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(internalOnly));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(internalOnly));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput({}));
}
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
// one of these before demotion, so the program simply could not be transpiled at all.
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
@@ -21,6 +21,7 @@
#include <cstring>
#include <map>
#include <string>
#include <utility>
#include <vector>
using namespace MobileGL;
@@ -156,6 +157,53 @@ void main() {
g_out.value[0] = 1u;
}
)";
// glslang emits constants in FIRST-USE order, so a shader that does not use the flattened
// array's length until after it has declared the counter block leaves that constant BELOW the
// block. The pass needs the length to build `uint[length]` immediately before the block (SPIR-V
// forbids forward type references), and it used to decline the whole block in that case - which
// left the offsets in place and made SPIRV-Cross refuse the stage outright:
//
// Push constant block cannot be expressed as neither std430 nor std140.
//
// That is KHR-GL43.compute_shader.pipeline-compute-chain: its first kernel declares two counters
// at offset 8 (so the flattened array is 4 elements) and first uses the value 4 after the block,
// so the kernel never reached the driver and every buffer, image and counter it writes kept its
// initial value. Here `i < 4u` is what puts `uint 4` below the block; the ordering assertion
// below is the fixture's own latch, so a future glslang that emits constants differently reports
// a stale fixture rather than silently testing nothing.
constexpr const char* kLateLengthConstantCounters = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 1, offset = 8) uniform atomic_uint g_counter[2];
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
uint i = atomicCounterIncrement(g_counter[1]);
if (i < 4u) { g_out.value[0] = i; }
}
)";
// Index of the first OpConstant of type uint with value |value|, and of struct |structId|, in
// the module's instruction order. -1 when absent.
std::pair<Int64, Int64> UintConstantAndStructOrder(const Vector<Uint32>& spirv, Uint32 structId,
Uint32 value) {
Int64 index = 0, constantIndex = -1, structIndex = -1;
Uint32 uintTypeId = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpTypeInt && wordCount >= 4u && words[2] == 32u && words[3] == 0u) {
uintTypeId = words[1];
}
if (opcode == spv::Op::OpConstant && wordCount >= 4u && words[1] == uintTypeId &&
words[3] == value && constantIndex < 0) {
constantIndex = index;
}
if (opcode == spv::Op::OpTypeStruct && wordCount >= 2u && words[1] == structId) {
structIndex = index;
}
++index;
});
return {constantIndex, structIndex};
}
} // namespace
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
@@ -212,3 +260,44 @@ TEST(FlattenAtomicCounterBlockPass, IsIdempotent) {
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
EXPECT_EQ(twice, once);
}
// The block must still flatten when the module already declares the flattened array's length
// constant BELOW the block. The pass relocates that constant instead of declining; declining
// left the offsets in place and cost the whole stage its transpile.
TEST(FlattenAtomicCounterBlockPass, FlattensWhenTheLengthConstantIsDeclaredAfterTheBlock) {
const Vector<Uint32> input = CompileCompute(kLateLengthConstantCounters);
ASSERT_FALSE(input.empty());
const Uint32 structId = FindAtomicCounterBlockStructId(input);
ASSERT_NE(structId, 0u);
ASSERT_EQ(MemberOffsetOf(input, structId, 0u), 8);
// The fixture's precondition, asserted rather than assumed: two counters at offset 8 need a
// 4-element array, and this shader's `uint 4` really does sit below the block.
const auto [constantIndex, structIndex] = UintConstantAndStructOrder(input, structId, 4u);
ASSERT_GE(constantIndex, 0) << "fixture is stale: the module no longer declares a uint 4";
ASSERT_GE(structIndex, 0);
ASSERT_GT(constantIndex, structIndex)
<< "fixture is stale: `uint 4` is no longer declared after the counter block, so this "
"test would pass without exercising the relocation at all";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
ASSERT_FALSE(output.empty());
ASSERT_NE(output, input) << "the block was declined; the offsets are still in the module and "
"SPIRV-Cross will refuse the stage";
const Uint32 outStructId = FindAtomicCounterBlockStructId(output);
ASSERT_EQ(outStructId, structId);
EXPECT_EQ(MemberCountOf(output, outStructId), 1u);
EXPECT_EQ(MemberOffsetOf(output, outStructId, 0u), 0);
EXPECT_EQ(ArrayLengthOf(output, MemberTypeOf(output, outStructId, 0u)), 4);
// The relocation moved a definition; the module has to still be well-ordered.
EXPECT_TRUE(Validates(output));
// The symptom the CTS case actually failed on: with the block declined this throws.
MG_Util::ShaderTranspiler::SpvcSession session(
output, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << "ESSL transpile failed: " << (essl ? String{} : essl.error().log);
}
@@ -0,0 +1,347 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenFloat64StorageBlockTest.cpp
// Copyright (c) 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
//
// FlattenFloat64StorageBlockPass, over the module the production chain actually hands it:
// ShaderCompiler::SanitizeAndOptimizeBinary, where the pass sits immediately before the fp64
// demotion. The behavioural half - that a block copied through the flattened words comes back
// byte for byte - is DoublePrecisionScenario's; what only a module walk can say is WHICH blocks
// were flattened, how wide, and that the ones this pass must not touch came through unchanged.
#include <gtest/gtest.h>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// A test-side reference walker, deliberately independent of the production code: a bug in
// the pass must not be able to hide behind the same helper.
constexpr Uint32 kSpirvHeaderWordCount = 5;
constexpr Uint32 kOpName = 5;
constexpr Uint32 kOpDecorate = 71;
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kOpTypeInt = 21;
constexpr Uint32 kOpTypeFloat = 22;
constexpr Uint32 kOpTypeArray = 28;
constexpr Uint32 kOpTypeStruct = 30;
constexpr Uint32 kOpConstant = 43;
constexpr Uint32 kDecorationArrayStride = 6;
constexpr Uint32 kDecorationOffset = 35;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
visit(opcode, &spirv[i], wordCount);
i += wordCount;
}
}
Uint32 StructIdNamed(const Vector<Uint32>& spirv, const String& name) {
Uint32 structId = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpName || wordCount < 3 || structId != 0) return;
const char* text = reinterpret_cast<const char*>(&words[2]);
const SizeT available = static_cast<SizeT>(wordCount - 2) * sizeof(Uint32);
// The whole name, not a prefix of it: "Wide" must not match "WideOther".
if (available <= name.size() || text[name.size()] != 0) return;
if (std::strncmp(text, name.c_str(), name.size()) == 0) structId = words[1];
});
return structId;
}
// The operands of OpTypeStruct <structId>, i.e. one type id per member.
Vector<Uint32> MemberTypesOf(const Vector<Uint32>& spirv, Uint32 structId) {
Vector<Uint32> members;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpTypeStruct || wordCount < 2 || words[1] != structId) return;
for (Uint32 i = 2; i < wordCount; ++i) members.push_back(words[i]);
});
return members;
}
Vector<Uint32> MemberOffsetsOf(const Vector<Uint32>& spirv, Uint32 structId) {
std::map<Uint32, Uint32> byMember;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpMemberDecorate || wordCount < 5 || words[1] != structId) return;
if (words[3] != kDecorationOffset) return;
byMember[words[2]] = words[4];
});
Vector<Uint32> offsets;
for (const auto& [member, offset] : byMember) offsets.push_back(offset);
return offsets;
}
Uint32 DecorationValueOf(const Vector<Uint32>& spirv, Uint32 id, Uint32 decoration) {
Uint32 value = 0xFFFFFFFFu;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpDecorate || wordCount < 4 || words[1] != id || words[2] != decoration) return;
value = words[3];
});
return value;
}
// (element type id, declared length) of OpTypeArray <arrayId>, or (0, 0).
std::pair<Uint32, Uint32> ArrayShapeOf(const Vector<Uint32>& spirv, Uint32 arrayId) {
Uint32 elementTypeId = 0;
Uint32 lengthConstantId = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpTypeArray || wordCount < 4 || words[1] != arrayId) return;
elementTypeId = words[2];
lengthConstantId = words[3];
});
if (elementTypeId == 0) return {0, 0};
Uint32 length = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpConstant || wordCount < 4 || words[2] != lengthConstantId) return;
length = words[3];
});
return {elementTypeId, length};
}
Bool IsUint32Type(const Vector<Uint32>& spirv, Uint32 typeId) {
Bool isUint = false;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpTypeInt || wordCount < 4 || words[1] != typeId) return;
isUint = words[2] == 32u && words[3] == 0u;
});
return isUint;
}
Uint32 CountFloatTypesOfWidth(const Vector<Uint32>& spirv, Uint32 width) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpTypeFloat && wordCount >= 3 && words[2] == width) ++count;
});
return count;
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
// The whole shared chain, exactly as the frontend runs it at link.
Vector<Uint32> Sanitize(const Vector<Uint32>& input) {
Vector<Uint32> output;
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
return output;
}
// The block std140 lays out as data0@0, data1[3]@16 stride 16, data2@64 column stride 16,
// data3@112, data4[2]@128 stride 16, data5@160, data6@192 - 216 bytes, i.e. 54 words.
constexpr const char* kStd140BlockSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) buffer Wide {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_wide;
void main() {
g_wide.data0 = 1;
for (int i = 0; i < 3; ++i) g_wide.data1[i] = float(i);
g_wide.data2 = mat3x2(1.0);
g_wide.data3 = 2.0lf;
for (int i = 0; i < 2; ++i) g_wide.data4[i] = double(i);
g_wide.data5 = 3;
g_wide.data6 = dvec3(4.0lf);
}
)";
} // namespace
class FlattenFloat64StorageBlockTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
// The wrapper validates its output on every run, so this covers every rewrite the test
// performed without any of them having to say so.
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the flattened module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
TEST_F(FlattenFloat64StorageBlockTest, AStorageBlockWithDoublesBecomesOneWordArray) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kStd140BlockSource);
ASSERT_FALSE(input.empty());
// Before: seven members, at the std140 offsets the standard requires WITH the doubles.
const Uint32 inputStructId = StructIdNamed(input, "Wide");
ASSERT_NE(inputStructId, 0u) << Disassemble(input);
EXPECT_EQ(MemberOffsetsOf(input, inputStructId),
(Vector<Uint32>{0, 16, 64, 112, 128, 160, 192}))
<< Disassemble(input);
const Vector<Uint32> output = Sanitize(input);
ASSERT_FALSE(output.empty());
const Uint32 structId = StructIdNamed(output, "Wide");
ASSERT_NE(structId, 0u) << Disassemble(output);
const Vector<Uint32> members = MemberTypesOf(output, structId);
ASSERT_EQ(members.size(), 1u) << "the block should have collapsed to one member\n"
<< Disassemble(output);
EXPECT_EQ(MemberOffsetsOf(output, structId), (Vector<Uint32>{0}));
const auto [elementTypeId, length] = ArrayShapeOf(output, members[0]);
ASSERT_NE(elementTypeId, 0u) << "member 0 is not an array\n" << Disassemble(output);
EXPECT_TRUE(IsUint32Type(output, elementTypeId)) << Disassemble(output);
// 216 bytes is where the standard puts the end of this block; 216 / 4 = 54 words.
EXPECT_EQ(length, 54u) << Disassemble(output);
EXPECT_EQ(DecorationValueOf(output, members[0], kDecorationArrayStride), 4u);
// And the demotion that runs straight afterwards still has nothing 64-bit left to find.
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
}
// The gate, from the other side: a storage block with no 64-bit member keeps every member and
// every offset it was compiled with. This is what makes the pass free for every shader that does
// not use doubles - which is all of them but a handful.
TEST_F(FlattenFloat64StorageBlockTest, AStorageBlockWithoutDoublesIsLeftAlone) {
const String source = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) buffer Plain {
int data0;
float data1[3];
mat3x2 data2;
int data3;
} g_plain;
void main() {
g_plain.data0 = 1;
for (int i = 0; i < 3; ++i) g_plain.data1[i] = float(i);
g_plain.data2 = mat3x2(1.0);
g_plain.data3 = 2;
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
ASSERT_FALSE(input.empty());
const Vector<Uint32> output = Sanitize(input);
ASSERT_FALSE(output.empty());
const Uint32 structId = StructIdNamed(output, "Plain");
ASSERT_NE(structId, 0u) << Disassemble(output);
EXPECT_EQ(MemberTypesOf(output, structId).size(), 4u) << Disassemble(output);
EXPECT_EQ(MemberOffsetsOf(output, structId), (Vector<Uint32>{0, 16, 64, 112}))
<< Disassemble(output);
}
// A plain UNIFORM block is deliberately NOT flattened, however many doubles it holds: the
// frontend's glUniform*d routing is built by reflecting the DEMOTED module
// (ProgramSpirvTask::BuildGlobalUboRouting), so a representation change there would have to move
// with it. It keeps its members and takes the demotion's repacking, exactly as before.
TEST_F(FlattenFloat64StorageBlockTest, AUniformBlockWithDoublesIsLeftToTheDemotion) {
const String source = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) uniform Params {
int data0;
double data1;
int data2;
} g_params;
layout(std430, binding = 0) buffer Sink {
float g_out[];
};
void main() {
g_out[0] = float(g_params.data0) + float(g_params.data1) + float(g_params.data2);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
ASSERT_FALSE(input.empty());
const Vector<Uint32> output = Sanitize(input);
ASSERT_FALSE(output.empty());
const Uint32 structId = StructIdNamed(output, "Params");
ASSERT_NE(structId, 0u) << Disassemble(output);
EXPECT_EQ(MemberTypesOf(output, structId).size(), 3u)
<< "a uniform block must not be flattened\n"
<< Disassemble(output);
// The demotion's re-derived std140 layout for `int, float, int`, which is what the frontend
// reflects and what glUniform*d then writes into.
EXPECT_EQ(MemberOffsetsOf(output, structId), (Vector<Uint32>{0, 4, 8})) << Disassemble(output);
}
// ---------------------------------------------------------------------------
// The capability-gated half: a backend that consumes 64-bit floats natively gets neither pass.
// ---------------------------------------------------------------------------
// The flatten exists to preserve a byte layout ACROSS a narrowing. Where nothing narrows there is
// nothing to preserve and the driver lays the block out itself - so the block keeps its seven
// members at the offsets glslang computed, and the doubles in it are still doubles.
TEST_F(FlattenFloat64StorageBlockTest, TheNativePathLeavesTheBlockAndItsDoublesAlone) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kStd140BlockSource);
ASSERT_FALSE(input.empty());
const Uint32 inputStructId = StructIdNamed(input, "Wide");
ASSERT_NE(inputStructId, 0u);
const Vector<Uint32> inputOffsets = MemberOffsetsOf(input, inputStructId);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true, true));
ASSERT_FALSE(output.empty());
const Uint32 structId = StructIdNamed(output, "Wide");
ASSERT_NE(structId, 0u) << Disassemble(output);
EXPECT_EQ(MemberTypesOf(output, structId).size(), 7u)
<< "the block must not be flattened when nothing is narrowing it\n"
<< Disassemble(output);
EXPECT_EQ(MemberOffsetsOf(output, structId), inputOffsets) << Disassemble(output);
EXPECT_GT(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
}
// And the control: the SAME module through the SAME entry point with the bit clear is flattened
// exactly as it always was. This is the pair that pins "capability-false is byte-for-byte the old
// behaviour" at the level the device A/B checks.
TEST_F(FlattenFloat64StorageBlockTest, TheDemotedPathIsUnchangedByTheCapabilityArgument) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kStd140BlockSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> explicitlyDemoted;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, explicitlyDemoted, true, true, false));
// The four-argument spelling every existing caller uses, which must keep meaning "demote".
const Vector<Uint32> defaulted = Sanitize(input);
EXPECT_EQ(explicitlyDemoted, defaulted);
EXPECT_EQ(CountFloatTypesOfWidth(defaulted, 64), 0u) << Disassemble(defaulted);
}
@@ -135,7 +135,7 @@ void main() {
EsslTranslationKeyInputs inputs;
inputs.spirv = &spirv;
inputs.shaderType = GL_FRAGMENT_SHADER;
inputs.supportsViewportArray = false;
inputs.viewportIndexLoweringArmed = false;
inputs.supportsNoperspectiveInterpolation = false;
inputs.maxColorTextureSamples = 4;
inputs.maxIntegerSamples = 1;
@@ -491,6 +491,14 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
v.enableSpirvValidation = true;
variants.emplace_back("enableSpirvValidation", BuildSpirvTranslationKey(v));
}
{ // CompileEnv::ConsumesFloat64Natively(): the fp64 tail of SanitizeAndOptimizeBinary is
// skipped under it, so the SAME GLSL yields modules with real doubles under one answer
// and demoted, storage-block-flattened ones under the other. The one backend capability
// bit in this key, and the only one allowed in without changing what glslang produces.
SpirvTranslationKeyInputs v = base;
v.nativeFloat64 = true;
variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v));
}
// ---- inputs the WIDENED payload pulled into the key ----
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
// payload now carries, so they have to split the key. This is the group that would go
@@ -599,6 +607,45 @@ TEST_F(TranslationCacheTest, TwoBackendsCompilingTheSameGlslShareOneL1Entry) {
EXPECT_TRUE(BuildSpirvTranslationKey(onA) == BuildSpirvTranslationKey(onB));
}
// The ONE capability bit that breaks that sharing, and the two halves of why it is placed where
// it is. It must NOT move the front-end fingerprint - glslang parses, reflects and generates a
// `double` identically under it, and L1c (the parse-verdict memo) keys on that same fingerprint
// and would take a false miss per backend for nothing. It MUST move the L1 key, because L1's
// payload is the module AFTER SanitizeAndOptimizeBinary and the fp64 tail of that chain is
// exactly what this bit gates.
TEST_F(TranslationCacheTest, NativeFloat64IsOutOfTheFrontendFingerprintAndInsideTheL1Key) {
CompileEnv none; // no backend at all
CompileEnv emulated; // a backend without the feature
CompileEnv nativeEnv; // a backend with it
emulated.backend = BackendType::DirectVulkan;
nativeEnv.backend = BackendType::DirectVulkan;
nativeEnv.params.SupportsShaderFloat64 = true;
// No backend answers FALSE: the demoted module is the one that works everywhere, so a
// standalone compile gets it.
EXPECT_FALSE(none.ConsumesFloat64Natively());
EXPECT_FALSE(emulated.ConsumesFloat64Natively());
EXPECT_TRUE(nativeEnv.ConsumesFloat64Natively());
EXPECT_EQ(ComputeFrontendCompileEnvFingerprint(emulated), ComputeFrontendCompileEnvFingerprint(nativeEnv))
<< "the fp64 capability leaked into the front-end fingerprint";
EXPECT_NE(ComputeCompileEnvFingerprint(emulated), ComputeCompileEnvFingerprint(nativeEnv))
<< "the whole-environment fingerprint has to notice it - it is a DynamicBackendParameters "
"field, hashed by object representation";
const Vector<SpirvTranslationKeyInputs::Stage> stages{{GL_VERTEX_SHADER, kVertexSource},
{GL_FRAGMENT_SHADER, kFragmentSource}};
SpirvTranslationKeyInputs demoted = BaselineSpirvInputs(stages);
demoted.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(emulated);
demoted.nativeFloat64 = emulated.ConsumesFloat64Natively();
SpirvTranslationKeyInputs kept = BaselineSpirvInputs(stages);
kept.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(nativeEnv);
kept.nativeFloat64 = nativeEnv.ConsumesFloat64Natively();
EXPECT_FALSE(BuildSpirvTranslationKey(demoted) == BuildSpirvTranslationKey(kept))
<< "one L1 entry would then describe two different module sets";
}
// The other direction, one case per input that was KEPT. Each is a limit the front end
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
@@ -855,8 +902,8 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
}
{ // arms LowerViewportIndexForEssl
EsslTranslationKeyInputs v = base;
v.supportsViewportArray = true;
variants.emplace_back("supportsViewportArray", BuildEsslTranslationKey(v));
v.viewportIndexLoweringArmed = true;
variants.emplace_back("viewportIndexLoweringArmed", BuildEsslTranslationKey(v));
}
{ // arms EmulateNoPerspectiveForEssl
EsslTranslationKeyInputs v = base;
@@ -1008,7 +1055,7 @@ TEST_F(TranslationCacheTest, L2RunsTheEmitterOncePerDistinctKey) {
// ... and a gate that only steers the SPIR-V pass chain still moves the key, so the
// emitter runs again even though this stand-in ignores the bit.
inputs = BaselineEsslInputs(spirv);
inputs.supportsViewportArray = true;
inputs.viewportIndexLoweringArmed = true;
(void)translate(inputs);
EXPECT_EQ(emitCount, 3);
}
@@ -14,10 +14,10 @@
// and every draw with the program silently renders nothing while GL_LINK_STATUS still says TRUE.
//
// What has to hold is the emulation's exactness, in three parts at once: the DECLARED format must
// become the core carrier of the same per-channel width, every imageStore through it must have its
// surplus components replaced by GL's own (0.., 1) so the carrier's extra channels never hold
// anything GL has not defined, and every imageLoad must come back masked the same way. A module
// that declares only core formats - or one of the nine formats with no exact carrier - must come
// become a core carrier that loses nothing, every imageStore through it must have its surplus
// components replaced by GL's own (0.., 1) so the carrier's extra channels never hold anything GL
// has not defined, and every imageLoad must come back masked the same way. A module that declares
// only core formats - or one of the eight formats with no lossless carrier at all - must come
// out untouched, because widening those would be an approximation rather than an emulation. Real
// GLSL through the same glslang path the backends use, for the same reason
// ClampMultisampleFetchTest.cpp does it: what matters is what glslang actually emits.
@@ -89,6 +89,7 @@ namespace {
struct StorageImageType {
Uint32 resultId = 0u;
Uint32 format = 0u;
Uint32 sampledTypeId = 0u;
};
Vector<StorageImageType> CollectStorageImageTypes(const Vector<Uint32>& spirv) {
@@ -96,11 +97,27 @@ namespace {
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpTypeImage || wordCount < 9u) return;
if (words[7] != 2u) return;
types.push_back(StorageImageType{words[1], words[8]});
types.push_back(StorageImageType{words[1], words[8], words[2]});
});
return types;
}
// "float" / "uint" / "int" / "" for a scalar numeric type id, which is the one thing that says
// whether a declaration is still an image2D or has become a uimage2D.
String ScalarTypeSpellingOf(const Vector<Uint32>& spirv, Uint32 typeId) {
String spelling;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (words[1] != typeId) return;
// OpTypeFloat words: 1 result id, 2 width. OpTypeInt adds 3 signedness.
if (opcode == spv::Op::OpTypeFloat && wordCount >= 3u) {
spelling = "float";
} else if (opcode == spv::Op::OpTypeInt && wordCount >= 4u) {
spelling = words[3] != 0u ? "int" : "uint";
}
});
return spelling;
}
// OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2,
// 5.. the component selectors.
struct VectorShuffle {
@@ -229,15 +246,103 @@ void main() {
}
)";
// rg16 is one of the NINE with no core carrier of the same per-channel width. Widening it
// would change the quantisation an application sees, so it must be left alone and keep the
// honest "no GLSL ES spelling" diagnostic instead.
// r11f_g11f_b10f: THREE float channels in a packed 32-bit word, and the only format the four
// CTS allFormats/allTargets walkers still aborted on after the channel widening landed - it
// has no core carrier of the same per-channel width, so it took rgba16f, whose 5-bit exponent
// and longer mantissa represent every 11f and 10f value exactly.
const char* const kR11fG11fB10fLoadStore = R"(#version 430 core
layout(r11f_g11f_b10f, binding = 0) uniform image2D img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
fragColor = texel;
}
)";
// rg32f again, but as a BUFFER image. Same format, and NOT the same emulation: a buffer
// image's texels are the application's buffer object, so there is nothing to reallocate a
// carrier in - but the same bytes can be VIEWED as twice as many r32f texels, which is exact.
const char* const kRg32fBufferLoadStore = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, int(gl_FragCoord.x));
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
fragColor = texel;
}
)";
// ...and one that asks the image how big it is, which the split has to halve: the ES view has
// twice the texels the application's format describes.
const char* const kRg32fBufferSize = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
fragColor = vec4(float(imageSize(img)));
}
)";
// rg16f as a buffer image: two channels of 16-bit float, whose single-channel base r16f core
// ESSL does not have. Nothing to split it into, so it keeps the honest failure.
const char* const kRg16fBufferLoadStore = R"(#version 430 core
layout(rg16f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, int(gl_FragCoord.x));
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
fragColor = texel;
}
)";
// rgb10_a2ui: FOUR unsigned-integer channels of 10, 10, 10 and 2 bits, carried in an rgba16ui
// that gives each of them sixteen. The only widening whose carrier has as many channels as the
// original, so it is the only one where GL leaves NOTHING to pin and both accesses must come
// out exactly as glslang emitted them.
const char* const kRgb10A2uiLoadStore = R"(#version 430 core
layout(rgb10_a2ui, binding = 0) uniform uimage2D img;
out vec4 fragColor;
void main() {
uvec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), uvec4(7u, 8u, 9u, 3u));
fragColor = vec4(texel);
}
)";
// rg16: TWO unsigned-normalized 16-bit channels, which core ESSL has no image format of any
// width for. Carried as its own CODES in an rgba16ui, so the declaration comes out a
// uimage2D and every access is wrapped in GL 4.6 2.3.5 as well as masked.
const char* const kRg16LoadStore = R"(#version 430 core
layout(rg16, binding = 0) uniform image2D img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
fragColor = texel;
}
)";
// rgba16_snorm: the signed twin, whose code is a two's-complement 16-bit integer sitting in an
// UNSIGNED carrier channel - so the load has to sign-extend it back and the store has to mask
// it down, neither of which the unsigned conversion does.
const char* const kRgba16SnormLoadStore = R"(#version 430 core
layout(rgba16_snorm, binding = 0) uniform image2D img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, -1.0, 0.5, -0.5));
fragColor = texel;
}
)";
// rgb10_a2: FOUR normalized channels that are not all the same width, so its denominator is
// (1023, 1023, 1023, 3) and one number would be wrong for a quarter of every texel.
const char* const kRgb10A2LoadStore = R"(#version 430 core
layout(rgb10_a2, binding = 0) uniform image2D img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
fragColor = texel;
}
)";
@@ -247,7 +352,7 @@ void main() {
// the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts
// the three stop agreeing, and a narrow texture read through a wide image goes out of bounds
// silently on every driver tested.
TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
TEST(WidenImageFormats, TwentySixNonCoreFormatsHaveALosslessCoreCarrier) {
struct Case {
Uint requested;
Uint carrier;
@@ -272,6 +377,23 @@ TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
{0x8234, 0x8D76, 1, "GL_R16UI -> GL_RGBA16UI"},
{0x8238, 0x8D7C, 2, "GL_RG8UI -> GL_RGBA8UI"},
{0x8232, 0x8D7C, 1, "GL_R8UI -> GL_RGBA8UI"},
// The one entry that is a re-encoding rather than a channel widening: 11f is e5m6 and 10f
// is e5m5 against a half's s1e5m10, so the carrier is still lossless - and three channels,
// so the mask has to pin only alpha.
{0x8C3A, 0x881A, 3, "GL_R11F_G11F_B10F -> GL_RGBA16F"},
// FOUR channels: 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen, so nothing
// is masked at all and only the packed TRANSFER is re-encoded.
{0x906F, 0x8D76, 4, "GL_RGB10_A2UI -> GL_RGBA16UI"},
// The seven NORMALIZED formats, carried as their own channel CODES in the same rgba16ui.
// These are the entries whose carrier changes the shader-visible TYPE as well, which is
// why every access through them is wrapped in GL 4.6 2.3.5 rather than only masked.
{0x805B, 0x8D76, 4, "GL_RGBA16 -> GL_RGBA16UI"},
{0x822C, 0x8D76, 2, "GL_RG16 -> GL_RGBA16UI"},
{0x822A, 0x8D76, 1, "GL_R16 -> GL_RGBA16UI"},
{0x8059, 0x8D76, 4, "GL_RGB10_A2 -> GL_RGBA16UI"},
{0x8F9B, 0x8D76, 4, "GL_RGBA16_SNORM -> GL_RGBA16UI"},
{0x8F99, 0x8D76, 2, "GL_RG16_SNORM -> GL_RGBA16UI"},
{0x8F98, 0x8D76, 1, "GL_R16_SNORM -> GL_RGBA16UI"},
};
for (const Case& testCase : cases) {
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier)
@@ -287,7 +409,7 @@ TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) {
}
}
TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) {
TEST(WidenImageFormats, CoreFormatsAreRefused) {
// The thirteen GLSL ES already has: nothing to carry.
for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/,
0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/,
@@ -297,22 +419,63 @@ TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) {
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u)
<< "core format 0x" << std::hex << coreFormat;
}
// The nine with no core format of the same per-channel width. Carrying these would be an
// approximation - a different quantisation, or a different numeric domain for anything that
// samples the same texture - so they are deliberately left to the honest diagnostic.
for (const Uint hardFormat : {0x8C3Au /*R11F_G11F_B10F*/, 0x8059u /*RGB10_A2*/,
0x906Fu /*RGB10_A2UI*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/,
0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/,
0x8F98u /*R16_SNORM*/}) {
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u)
<< "format without an exact carrier 0x" << std::hex << hardFormat;
}
// Not an image format at all.
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u);
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u);
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u);
}
// The denominators of GL 4.6 2.3.5, which is the whole difference between a carrier that holds a
// format's VALUES and one that holds its CODES. Both halves of DirectGLES's transfer read them
// (the upload's synthetic alpha and glGetTexImage's divide), and so does the shader rewrite, so a
// wrong entry here is wrong in three places at once and consistently - which is exactly the kind
// of error a round-trip test cannot see.
TEST(WidenImageFormats, OnlyTheNormalizedFormatsCarryCodesAndTheirDenominatorsAreTheFormatsOwn) {
struct Case {
Uint format;
Uint32 channelMax[4];
bool isSigned;
const char* name;
};
const Case cases[] = {
{0x805B, {65535u, 65535u, 65535u, 65535u}, false, "GL_RGBA16"},
{0x822C, {65535u, 65535u, 65535u, 65535u}, false, "GL_RG16"},
{0x822A, {65535u, 65535u, 65535u, 65535u}, false, "GL_R16"},
// The one format whose channels are not all the same width, and the reason the answer is
// four numbers rather than one: a two-bit alpha saturates at 3, not at 1023.
{0x8059, {1023u, 1023u, 1023u, 3u}, false, "GL_RGB10_A2"},
{0x8F9B, {32767u, 32767u, 32767u, 32767u}, true, "GL_RGBA16_SNORM"},
{0x8F99, {32767u, 32767u, 32767u, 32767u}, true, "GL_RG16_SNORM"},
{0x8F98, {32767u, 32767u, 32767u, 32767u}, true, "GL_R16_SNORM"},
};
for (const Case& testCase : cases) {
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
bool isSigned = !testCase.isSigned;
EXPECT_TRUE(ShaderCompiler::NormalizedImageCarrierCodes(testCase.format, channelMax, isSigned))
<< testCase.name;
for (Uint channel = 0; channel < 4; ++channel) {
EXPECT_EQ(channelMax[channel], testCase.channelMax[channel])
<< testCase.name << " channel " << channel;
}
EXPECT_EQ(isSigned, testCase.isSigned) << testCase.name;
}
// Everything else keeps its own component type in the carrier, so nothing is converted: an
// rg8's carrier channel really is an 8-bit unsigned normalized one, and an rgb10_a2ui's
// channel really does hold the integer the shader stored.
for (const Uint direct : {0x8230u /*RG32F*/, 0x8229u /*R8*/, 0x8F94u /*R8_SNORM*/,
0x8232u /*R8UI*/, 0x8C3Au /*R11F_G11F_B10F*/, 0x906Fu /*RGB10_A2UI*/,
0x8814u /*RGBA32F*/, 0x8051u /*RGB8, not an image format*/}) {
Uint32 channelMax[4] = {7u, 7u, 7u, 7u};
bool isSigned = true;
EXPECT_FALSE(ShaderCompiler::NormalizedImageCarrierCodes(direct, channelMax, isSigned))
<< "format 0x" << std::hex << direct;
for (Uint channel = 0; channel < 4; ++channel) {
EXPECT_EQ(channelMax[channel], 7u) << "a refused format must leave the output alone";
}
}
}
TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) {
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
ASSERT_FALSE(spirv.empty());
@@ -357,6 +520,253 @@ TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked
<< "the mask must be a separate value, or it would feed itself";
}
// The three-channel case, which no format exercised before r11f_g11f_b10f was carried: only ALPHA
// is surplus, so the mask must take r, g and b from the texel and nothing but the fourth component
// from the (0, 0, 0, 1) constant. A mask that zeroed blue here - the shape a two-channel format
// wants - would silently drop the third channel of every store.
TEST(WidenImageFormats, ThreeChannelPackedFloatImageBecomesRgba16fWithOnlyAlphaPinned) {
const Vector<Uint32> spirv = CompileFragment(kR11fG11fB10fLoadStore);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R11fG11fB10f));
Vector<Uint32> widened;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(widened.empty());
EXPECT_TRUE(Validates(widened));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16f));
const auto shuffles = CollectVectorShuffles(widened);
const auto texelIds = CollectImageWriteTexelIds(widened);
ASSERT_EQ(texelIds.size(), 1u);
const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front());
ASSERT_NE(storeMask, nullptr) << "the imageStore texel is not a masked value";
EXPECT_TRUE(HasComponents(*storeMask, {0u, 1u, 2u, 7u}))
<< "expected (r, g, b, 1) - components 0, 1 and 2 of the texel, then 3 of (0,0,0,1)";
const auto readIds = CollectImageReadResultIds(widened);
ASSERT_EQ(readIds.size(), 1u);
const VectorShuffle* loadMask = FindShuffleOver(shuffles, readIds.front());
ASSERT_NE(loadMask, nullptr) << "the imageLoad result is consumed unmasked";
EXPECT_TRUE(HasComponents(*loadMask, {0u, 1u, 2u, 7u}));
}
// The four-channel case, which is the whole of rgb10_a2ui's shader-side emulation: the carrier has
// as many channels as the original, every value of every channel fits, and GL therefore defines
// NOTHING about a surplus channel because there is none. So both accesses have to come out
// untouched - a pass that masked here would replace the alpha the application stored (0..3 of a
// two-bit channel, which the CTS walker writes as 3) with the constant 1 and drop blue outright.
TEST(WidenImageFormats, FourChannelIntegerImageBecomesRgba16uiWithNeitherAccessMasked) {
const Vector<Uint32> spirv = CompileFragment(kRgb10A2uiLoadStore);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgb10a2ui));
Vector<Uint32> widened;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(widened.empty());
EXPECT_TRUE(Validates(widened));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
// The declaration moved and nothing else did.
EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size())
<< "a carrier with as many channels as the original must add no mask";
EXPECT_EQ(CollectImageReadResultIds(widened).size(), CollectImageReadResultIds(spirv).size())
<< "the imageLoad was duplicated for a rewrite that has nothing to rewrite";
}
// ...and the same module through the emitter, which is where the failure actually showed: ESSL has
// no `r11f_g11f_b10f` token, SPIRV-Cross throws for it, and the throw took every image uniform
// declared in the same stage with it.
TEST(WidenImageFormats, PackedFloatImageOnlyReachesEsslThroughTheCarrier) {
const Vector<Uint32> spirv = CompileFragment(kR11fG11fB10fLoadStore);
ASSERT_FALSE(spirv.empty());
const EsslAttempt before = EmitEssl(spirv);
EXPECT_FALSE(before.succeeded)
<< "SPIRV-Cross printed r11f_g11f_b10f for an ES target; the widening's premise has "
"changed:\n"
<< before.text;
Vector<Uint32> widened;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
const EsslAttempt after = EmitEssl(widened);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("rgba16f"), String::npos) << after.text;
EXPECT_EQ(after.text.find("r11f_g11f_b10f"), String::npos) << after.text;
}
// A BUFFER image is never WIDENED, whatever its format, and the format alone cannot say so -
// rg32f is carried in an rgba32f when it is an image2D. What makes the difference is that widening
// REALLOCATES the texture behind the image in the carrier, and a buffer image has no texture
// storage to reallocate: its texels are the application's buffer object, usually also a vertex,
// index or storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels
// - the measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
// [1,100] [0,1] [2,100] [0,1] instead of [1,100] [2,100] [3,100] [4,100], with the last two texels
// written past the end of the application's buffer.
//
// It is SPLIT instead, which is the opposite move: the bytes stay exactly where they are and the
// SUBSCRIPT changes. rg32f over N texels and r32f over 2N texels describe the same memory, so
// component j of texel i is texel 2i + j, and the base format is one of the thirteen ES has.
TEST(WidenImageFormats, BufferImagesAreSplitByTheSubscriptRatherThanWidened) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
ASSERT_FALSE(spirv.empty());
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
<< "the fixture stopped declaring the format this test is about";
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(split));
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R32f))
<< "the base format is the SINGLE-channel one, not the four-channel carrier a 2D image "
"would take - a buffer image that gained texel width would run off the end of the "
"application's buffer";
// ONE imageLoad became TWO, and ONE imageStore became two as well: each component of the
// original texel is its own texel of the base view.
EXPECT_EQ(CollectImageReadResultIds(split).size(), 2u * CollectImageReadResultIds(spirv).size());
EXPECT_EQ(CollectImageWriteTexelIds(split).size(), 2u * CollectImageWriteTexelIds(spirv).size());
// ...and the store's two texels are the two components, not the same one twice.
const auto shuffles = CollectVectorShuffles(split);
const auto texelIds = CollectImageWriteTexelIds(split);
ASSERT_EQ(texelIds.size(), 2u);
const VectorShuffle* firstTexel = FindShuffleWithResult(shuffles, texelIds[0]);
const VectorShuffle* secondTexel = FindShuffleWithResult(shuffles, texelIds[1]);
ASSERT_NE(firstTexel, nullptr);
ASSERT_NE(secondTexel, nullptr);
EXPECT_TRUE(HasComponents(*firstTexel, {0u, 4u, 4u, 7u}))
<< "expected (r, 0, 0, 1) - component 0 of the texel into a one-channel base format";
EXPECT_TRUE(HasComponents(*secondTexel, {1u, 4u, 4u, 7u}))
<< "expected (g, 0, 0, 1) - component 1 into the NEXT base texel";
// The subscript arithmetic itself: one multiply and one add per access.
Uint32 multiplies = 0;
Uint32 adds = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpIMul) ++multiplies;
if (opcode == spv::Op::OpIAdd) ++adds;
});
EXPECT_GE(multiplies, 2u) << "2i, once for the load and once for the store";
EXPECT_GE(adds, 2u) << "2i + 1, once for the load and once for the store";
// And what reaches the driver names a format ES has.
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("r32f"), String::npos) << after.text;
EXPECT_EQ(after.text.find("rg32f"), String::npos)
<< "the token no ES driver accepts is still in the emitted source:\n"
<< after.text;
}
// imageSize() has to be halved with everything else: the ES view really does have twice the texels
// the application's format describes, so a shader that walks the buffer by its own size would run
// off the end of it - or, on a well-behaved driver, spend half its invocations past the data.
TEST(WidenImageFormats, ASplitBufferImageReportsTheSizeItsOwnFormatDescribes) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferSize);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
Uint32 sizeQueries = 0;
Uint32 divisions = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpImageQuerySize) ++sizeQueries;
if (opcode == spv::Op::OpSDiv || opcode == spv::Op::OpUDiv) ++divisions;
});
EXPECT_EQ(sizeQueries, 1u) << "the query itself is not duplicated, only divided";
EXPECT_EQ(divisions, 1u);
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("imageSize"), String::npos) << after.text;
EXPECT_NE(after.text.find("/ 2"), String::npos)
<< "the reported size must be the application's, not the base view's:\n"
<< after.text;
}
// A buffer image whose base format is NOT core ESSL has nothing to split into, and must keep the
// honest "no GLSL ES spelling" failure rather than take a wider one: rg16f's components are 16-bit
// floats and core ESSL has no r16f, so a split would have to change the component type.
TEST(WidenImageFormats, ABufferImageWithNoCoreBaseFormatIsLeftAlone) {
const Vector<Uint32> spirv = CompileFragment(kRg16fBufferLoadStore);
ASSERT_FALSE(spirv.empty());
const auto types = CollectStorageImageTypes(spirv);
ASSERT_EQ(types.size(), 1u);
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true);
if (!split.empty()) {
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
}
}
// The table the three layers share, from the other side: only the 32-bit component family has a
// core single-channel base, and a two-dimensional image never takes this route.
TEST(WidenImageFormats, OnlyTheThirtyTwoBitTwoChannelFormatsSplitAsBufferImages) {
struct Case {
Uint format;
Uint base;
const char* name;
};
const Case cases[] = {
{0x8230, 0x822E, "GL_RG32F -> GL_R32F"},
{0x823B, 0x8235, "GL_RG32I -> GL_R32I"},
{0x823C, 0x8236, "GL_RG32UI -> GL_R32UI"},
};
for (const Case& testCase : cases) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(testCase.format), testCase.base)
<< testCase.name;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.base)) << testCase.name;
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.base), 1u) << testCase.name;
}
// No core single-channel base of the right component type, so no split.
for (const Uint refused : {0x822Fu /*RG16F*/, 0x8239u /*RG16I*/, 0x823Au /*RG16UI*/, 0x822Bu /*RG8*/,
0x8F95u /*RG8_SNORM*/, 0x822Cu /*RG16*/, 0x8237u /*RG8I*/, 0x8238u /*RG8UI*/,
// Already core, or four-channel, or not an image format at all.
0x8814u /*RGBA32F*/, 0x822Eu /*R32F*/, 0x8051u /*RGB8*/, 0u}) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(refused), 0u)
<< "format 0x" << std::hex << refused;
}
}
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
const Vector<Uint32> spirv = CompileFragment(kR8uiLoadStore);
ASSERT_FALSE(spirv.empty());
@@ -504,25 +914,132 @@ TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) {
}
}
TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) {
// The normalized carrier, which is the one that does not merely re-DECLARE the image: a 16-bit
// normalized channel has no core ESSL format of any width behind it, and no FLOAT carrier is
// honest either (a half has eleven mantissa bits against its sixteen), so what the rgba16ui holds
// is the format's own CODE. That changes the shader-visible TYPE, which is the thing to check -
// an image2D whose format moved to rgba16ui but whose sampled type stayed float is not merely
// wrong, it is invalid SPIR-V, and a module that kept the float type while the STORAGE became an
// integer texture would read whole texels as garbage.
TEST(WidenImageFormats, NormalizedImageBecomesAUimageWhoseAccessesConvertItsCodes) {
const Vector<Uint32> spirv = CompileFragment(kRg16LoadStore);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
// rg16 has no core format with 16-bit unsigned-normalized channels behind it. Anything wider
// would requantize differently from what the application asked for, so the pass declines and
// CollectImageFormatBakeInputs reports the format as unspellable instead.
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
EXPECT_EQ(ScalarTypeSpellingOf(spirv, beforeTypes.front().sampledTypeId), "float");
Vector<Uint32> widened;
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true);
if (!widened.empty()) {
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(widened.empty());
EXPECT_TRUE(Validates(widened));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint")
<< "the carrier's component type is unsigned integer, and spirv-val requires the image's "
"Sampled Type to say so";
// The masks are still there and still say what a two-channel format's surplus channels are -
// the conversion wraps them, it does not replace them.
const auto shuffles = CollectVectorShuffles(widened);
const auto texelIds = CollectImageWriteTexelIds(widened);
ASSERT_EQ(texelIds.size(), 1u);
// The texel is now the PACKED value, so the mask is one step further back: find the shuffle
// by its component selectors instead.
Bool sawTwoChannelMask = false;
for (const VectorShuffle& shuffle : shuffles) {
sawTwoChannelMask = sawTwoChannelMask || HasComponents(shuffle, {0u, 1u, 6u, 7u});
}
EXPECT_TRUE(sawTwoChannelMask) << "expected the (r, g, 0, 1) mask a two-channel format needs";
// ...and the ESSL says the whole story: a uimage2D holding rgba16ui, divided and multiplied
// by the format's own 65535.
const EsslAttempt after = EmitEssl(widened);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
EXPECT_EQ(after.text.find("rg16"), String::npos)
<< "the token no ES driver accepts is still in the emitted source:\n"
<< after.text;
EXPECT_NE(after.text.find("65535.0"), String::npos)
<< "the unsigned-normalized denominator is 2^16 - 1:\n"
<< after.text;
EXPECT_EQ(after.text.find("32767.0"), String::npos)
<< "an unsigned format must not take the SIGNED denominator:\n"
<< after.text;
}
// The signed half, which needs two things the unsigned one does not: the code is sign-extended
// out of the unsigned carrier channel on the way in, and the decode is max(c / 32767, -1) rather
// than the bare division - GL clamps -2^15/32767 up to exactly -1.
TEST(WidenImageFormats, SignedNormalizedImageSignExtendsItsCodeAndClampsAtMinusOne) {
const Vector<Uint32> spirv = CompileFragment(kRgba16SnormLoadStore);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> widened;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
ASSERT_FALSE(widened.empty());
EXPECT_TRUE(Validates(widened));
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
// The sign extension is a shift PAIR, and the arithmetic one is what makes it a sign
// extension rather than a zero extension.
Bool sawShiftLeft = false;
Bool sawArithmeticShiftRight = false;
ForEachInstruction(widened, [&](spv::Op opcode, const Uint32*, Uint32) {
sawShiftLeft = sawShiftLeft || opcode == spv::Op::OpShiftLeftLogical;
sawArithmeticShiftRight = sawArithmeticShiftRight || opcode == spv::Op::OpShiftRightArithmetic;
});
EXPECT_TRUE(sawShiftLeft);
EXPECT_TRUE(sawArithmeticShiftRight)
<< "a logical shift right would read every negative code as a large positive one";
const EsslAttempt after = EmitEssl(widened);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
EXPECT_EQ(after.text.find("rgba16_snorm"), String::npos) << after.text;
EXPECT_NE(after.text.find("32767.0"), String::npos)
<< "the signed-normalized denominator is 2^15 - 1:\n"
<< after.text;
EXPECT_NE(after.text.find("-1.0"), String::npos)
<< "GL clamps the signed decode at -1:\n"
<< after.text;
}
// rgb10_a2, whose four channels are 10, 10, 10 and 2 bits: the only entry where one denominator
// would be wrong for a channel that IS present, rather than for one the mask discards anyway.
TEST(WidenImageFormats, TenTenTenTwoImageTakesAPerChannelDenominator) {
const Vector<Uint32> spirv = CompileFragment(kRgb10A2LoadStore);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> widened;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
ASSERT_FALSE(widened.empty());
EXPECT_TRUE(Validates(widened));
const auto afterTypes = CollectStorageImageTypes(widened);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
const EsslAttempt after = EmitEssl(widened);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("1023.0"), String::npos) << after.text;
EXPECT_NE(after.text.find("3.0"), String::npos)
<< "the two-bit alpha saturates at 3, not at 1023:\n"
<< after.text;
EXPECT_EQ(after.text.find("rgb10_a2)"), String::npos) << after.text;
}
+50
View File
@@ -5286,3 +5286,53 @@ TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingC
EXPECT_TRUE(widened.empty());
}
}
// The OTHER transfer shape the image widening needs, and the one a channel repack cannot serve:
// GL_RGB10_A2UI's shadow is ONE 32-bit word per texel, not four components of the GL_RGBA16UI
// carrier's own type. Repacking it as components would take sixteen bytes out of a four-byte texel
// and shear the level - which only a LOAD notices, because a store overwrites whatever the upload
// got wrong.
//
// GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST component in the LOW bits, which is the whole
// content of the word "REV" and the single thing this can get backwards, so every field here is a
// different value and the boundary codes (0, the 10-bit maximum, the 2-bit maximum) are pinned
// exactly rather than compared with a tolerance.
TEST_F(TextureTest, ImageWidenedUploadSplitsAPacked2101010RevShadowIntoFourChannelCodes) {
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PreparePackedIntWidenedUpload;
const IntVec3 texelSize(3, 1, 1);
// r=1, g=2, b=3, a=1 | r=1023, g=0, b=1023, a=3 | r=0, g=1023, b=0, a=0
const Uint32 source[] = {
1u | (2u << 10) | (3u << 20) | (1u << 30),
1023u | (0u << 10) | (1023u << 20) | (3u << 30),
0u | (1023u << 10) | (0u << 20) | (0u << 30),
};
Vector<Uint8> widened;
const auto* result = static_cast<const Uint16*>(
PreparePackedIntWidenedUpload(texelSize, source, sizeof(source), widened));
ASSERT_NE(result, static_cast<const void*>(source));
ASSERT_EQ(widened.size(), 12 * sizeof(Uint16));
const Uint16 expected[] = {1, 2, 3, 1, 1023, 0, 1023, 3, 0, 1023, 0, 0};
for (SizeT i = 0; i < 12; ++i) {
EXPECT_EQ(result[i], expected[i]) << "component " << i;
}
// Sized from the LEVEL, never from the source: the driver reads a full width*height*4 shorts
// for the transfer it was handed, so a short source still has to leave a full destination.
{
Vector<Uint8> shortWidened;
const auto* shortResult = static_cast<const Uint16*>(
PreparePackedIntWidenedUpload(texelSize, source, sizeof(Uint32), shortWidened));
ASSERT_EQ(shortWidened.size(), 12 * sizeof(Uint16));
for (SizeT i = 4; i < 12; ++i) {
EXPECT_EQ(shortResult[i], 0u) << "component " << i << " past the source must be zero";
}
}
// Nothing to split.
{
Vector<Uint8> empty;
EXPECT_EQ(PreparePackedIntWidenedUpload(texelSize, nullptr, 0, empty), nullptr);
EXPECT_TRUE(empty.empty());
}
}
File diff suppressed because it is too large Load Diff
+199
View File
@@ -0,0 +1,199 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.h
// Copyright (c) 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 <MG_Util/BackendLoaders/OpenGL/Loader.h>
namespace MobileGL::MG_Util::SelfTest {
// ===================== KNOWN DRIVER BUGS =====================
//
// THIS IS THE DESIGNATED HOME FOR DRIVER-CAPABILITY LIES.
//
// The rest of the POST suite answers a different question: does the extension exist, and
// does a simple probe show it working. The entries here are not extension questions at
// all - they are CORE functionality that a driver advertises, accepts without error, and
// then does not perform. Nothing in an extension string or a limit query says so, which
// is exactly why each one needs its own executable probe.
//
// The inventory comes from CAMPAIGN FINDINGS, not from anything the driver reports.
//
// EVERY PROBE MUST CARRY A CONTROL. The geometry entry below is why the rule is written
// down: the same defect was first characterised as "this driver drops all geometry-stage
// storage-buffer writes", which would have justified withdrawing
// GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS entirely. A control showed geometry-stage writes
// land perfectly well when they precede EmitVertex(), so the limit is not a lie and
// withdrawing it would have broken shaders that work today. A probe without a control
// measures a symptom and invites exactly that over-correction.
//
// ADDING A SIBLING IS ONE FUNCTION: write an `Optional<DriverBugFinding> ProbeXxx(gl)`
// that returns nullopt when the driver is not affected, and add it to the table in
// CollectGlesKnownDriverBugs().
// What MobileGL can do about a bug this device HAS. There is deliberately no "not
// affected" member: a driver that passes the probe produces no finding at all, so the
// report only ever lists bugs actually present on this device.
enum class DriverBugVerdict : Uint8 {
// A MobileGL quirk repairs or substitutes for the defect and the application sees
// correct behaviour.
Fixed,
// There is no substitute. `detail` says what MobileGL does defensively instead, and
// what an application can still rely on.
Unfixable,
};
struct DriverBugFinding {
// Short name of the bug, not of the feature.
String name;
DriverBugVerdict verdict = DriverBugVerdict::Unfixable;
// One line: what the driver does wrong, and what MobileGL does about it.
String detail;
};
// Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one
// BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the
// before-emit write lands and the after-emit write does not.
//
// The before-emit write is the control, and it is the whole point of the probe. Adreno 830
// discards geometry-stage storage writes issued after the last emit while performing the
// identical write issued before it (measured both ways, and for both point and triangle
// geometry shaders, so the primitive shape is not the variable). Reading only the
// after-emit half would say "geometry storage writes do not work on this driver", which is
// false and would justify withdrawing a limit applications legitimately use.
//
// Deterministic by construction - the write either reaches memory or the driver
// structurally discards it - so the answer is latched, not sampled. Returns false when the
// driver advertises no geometry storage blocks, when an entry point is missing, or when
// anything about the probe fails to set up: an inconclusive probe must never be reported
// as a bug. Restores every piece of GL state it touches.
Bool ProbeGeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
// ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process.
Bool GeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
// Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice,
// with a separately linked program each time. Returns true only when the swizzled read goes
// wrong while every control read stays right.
//
// Adreno 830 returns uninitialised memory - a different value every run - for
// texelFetch(sampler2DMS, ..., sampleIndex != 0).w on an R32F multisample texture whose
// GL_TEXTURE_SWIZZLE_A is not the default, from the SECOND such program in the context
// onward. The first program reads correctly, which is why the probe links two.
//
// THREE CONTROLS, each identical to the subject but for one variable, and all three must
// read correctly for a wrong subject to count: (1) the same fetch with
// GL_TEXTURE_SWIZZLE_A left at its default, (2) the same fetch at sample index 0, and
// (3) the same swizzled texture read through .x instead of .w. Without them a driver that
// simply cannot render R32F, or cannot sample multisample textures at all, would be
// reported as having this very specific corruption.
//
// Returns false when the driver cannot host the shape (no multisample R32F colour target,
// fewer than two samples, a missing entry point, an incomplete framebuffer): an
// inconclusive probe must never be reported as a bug. Restores every piece of GL state it
// touches.
Bool ProbeR32FMultisampleSwizzleCorruption(const MG_External::GLESFunctionsTable& gl);
// ProbeR32FMultisampleSwizzleCorruption(), evaluated at most once per process.
Bool R32FMultisampleSwizzleCorrupted(const MG_External::GLESFunctionsTable& gl);
// What the image-location budget probe measured. `detected` is the only field the verdict
// depends on; the rest exist so the report can say what the shape was instead of asserting
// a number that was true on one device in one campaign.
struct ImageLocationBudgetMeasurement {
Bool detected = false;
// Image uniforms declared per stage in both the subject and the control - one more than
// GL_MAX_GEOMETRY_IMAGE_UNIFORMS, which is the smallest of the three stages' budgets.
Int perStageImageUniforms = 0;
// Distinct uniform NAMES in the subject (per-stage-unique) and in the control (shared).
Int subjectDistinctNames = 0;
Int controlDistinctNames = 0;
// The first line of the driver's info log for the failing link, so the report quotes the
// driver rather than paraphrasing it.
String driverMessage;
};
// Links the same three-stage (vertex, geometry, fragment) program twice: once with every
// stage naming its image uniforms uniquely, once with all three stages sharing one set of
// names. Both declare the same number of image uniforms per stage, on the same bindings,
// with the same qualifier and the same stores - the names are the only difference.
//
// Adreno 830 charges its image-location budget per distinct NAME, so the shared-name program
// links while the per-stage-named one is rejected with "Image location or component exceeds
// max allowed", even though nothing about the image USAGE changed. That is what makes the
// shared-name link the control: it proves the driver can host this exact amount of image
// work and that only the naming moved the answer.
//
// `detected` is false unless the subject fails AND the control links. Both failing means the
// shape is simply too large for the driver (an honest refusal); both linking means the
// driver does not have this bug.
ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
// ProbeImageLocationPerNameBudget(), evaluated at most once per process.
const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
// Draws one quad whose vertex stage stores to a `coherent writeonly` image and whose
// fragment stage reads the same image declared `coherent readonly` under the SAME name, then
// checks every fragment saw the store. Returns true only when the same-name program loses
// the store while the different-name control keeps it.
//
// Adreno 830 merges the two declarations into one uniform and silently discards the writing
// stage's stores. The control is the identical pair of shaders with the two halves renamed -
// exactly what MobileGL's image-uniform repair emits - which keeps every store. Without it
// the probe would be indistinguishable from "this driver cannot store to images from the
// vertex stage", which is a different and much larger claim.
//
// Returns false when the driver advertises no vertex-stage image uniforms, when an entry
// point is missing, or when the setup fails.
Bool ProbeCrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
// ProbeCrossStageImageQualifierMergeDropsWrites(), evaluated at most once per process.
Bool CrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
// What the image coherency probe measured. The residual is reported rather than hard-coded:
// it is a rate, it differs between devices, and a report that quotes a number measured
// somewhere else is worse than no number at all.
struct ImageCoherencyResidualMeasurement {
Bool detected = false;
// Texels the STRONGEST in-shader shape missed - that is what makes the defect unfixable.
Int mismatchedTexels = 0;
// Texels the shape MobileGL emits today missed, on the same driver in the same run. It
// is what applications actually get, and it is not always the same number.
Int emittedShapeMismatchedTexels = 0;
Int totalTexels = 0;
};
// Counts the texels whose dependent imageLoad() did not observe the imageStore() that
// precedes it in the same fragment invocation.
//
// THE SUBJECT IS THE STRONGEST SHAPE THE LANGUAGE OFFERS - a `coherent volatile`
// readonly/writeonly pair on one binding with BOTH memoryBarrierImage() and memoryBarrier()
// between the store and the read - and that choice is the whole reason the row can say
// "unfixable". Probing only the shape MobileGL emits today (`coherent` plus
// memoryBarrierImage()) reports a bug on drivers where simply adding `volatile` makes the
// read correct, which is a defect MobileGL could fix rather than one it cannot: measured on
// Mesa llvmpipe, the emitted shape misses every texel while the `volatile` shape misses
// none. Only a driver that fails even the strongest shape has no in-shader substitute left.
//
// The control is the same dependency split across TWO draws with a glMemoryBarrier and a
// glFinish between them. It separates "this driver cannot make image writes visible at all"
// (control also dirty - a far worse defect, and the probe declines to call it this one) from
// the finding, which is about ordering inside one invocation.
//
// `detected` is false unless the strongest shape is dirty AND the control is clean. The
// shape MobileGL emits is measured either way, so the report can say what applications get.
ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual(
const MG_External::GLESFunctionsTable& gl);
// ProbeImageWriteReadCoherencyResidual(), evaluated at most once per process.
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
const MG_External::GLESFunctionsTable& gl);
// Every known driver bug this GLES driver actually has. Bugs it does not have are absent,
// so an unaffected device renders an empty section rather than a wall of "not affected".
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
} // namespace MobileGL::MG_Util::SelfTest
+341 -188
View File
@@ -37,35 +37,61 @@
namespace MobileGL::MG_Util::SelfTest {
namespace {
// Display ranks for PostCheck::displayRank: within one backend section, FAIL
// rows render first, then WARN, PASS, INFO, then the device-driver identity
// rows render first, then WARN, then PASS, then the device-driver identity
// strings, and always last (regardless of status) the strings MobileGL itself
// reports to applications. Rows are stable-sorted, so relative order within a
// rank is preserved. Purely cosmetic: the verdict computation is unaffected.
//
// There is no rank between PASS and the identity blocks because there are no INFO
// capability rows any more - see the taxonomy on ReportBuilder below.
enum DisplayRank : Int {
RankFail = 0,
RankWarn = 1,
RankPass = 2,
RankInfo = 3,
RankDriverReported = 4,
RankMobileGLReported = 5,
RankDriverReported = 3,
RankMobileGLReported = 4,
};
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
// a config flag rather than on anything either backend probes: the demotion is what
// makes doubles work, but GL_ARB_gpu_shader_fp64 promises the PRECISION the demotion
// cannot deliver, so the string is opt-in and the row has to say which way it went.
// a config flag rather than on anything either backend probes: doubles WORK on every
// backend, but GL_ARB_gpu_shader_fp64 additionally promises 64-bit PRECISION, which only
// a backend that consumes fp64 natively actually has. The string is opt-in either way -
// advertising it is a decision about the whole extension's surface, not just about
// precision - so the row has to say which way it went.
String AppendFp64AdvertisementNote(String detail) {
if (MG_Config::Features.AdvertiseFp64) {
return Move(detail) +
". GL_ARB_gpu_shader_fp64 IS advertised (MOBILEGL_ADVERTISE_FP64): an application "
"that checks the string will believe it has 64-bit precision, and it does not";
"that checks the string will believe it has 64-bit precision, which is true only "
"where the row above says native";
}
return Move(detail) +
". GL_ARB_gpu_shader_fp64 is not advertised, because the precision it promises is the "
"one thing the demotion cannot provide; set MOBILEGL_ADVERTISE_FP64=1 to advertise it "
"anyway";
". GL_ARB_gpu_shader_fp64 is not advertised by default; set MOBILEGL_ADVERTISE_FP64=1 "
"to advertise it anyway";
}
// ===================== THE ROW VERDICT TAXONOMY =====================
//
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL. INFO IS FOR IDENTITY ONLY - renderer
// names, version strings, driver strings - and there is deliberately no way to emit an
// INFO capability row from here: the only INFO emitters are the two identity helpers at
// the bottom of this struct. A row that says "not supported; no impact today" tells a
// reader nothing about whether their application will work, which is the one question
// the screen exists to answer.
//
// PASS - the backend supports the capability directly.
// WARN - the backend does NOT support it directly, but a MobileGL quirk substitutes
// and the application still sees correct behaviour. The detail names the
// substitute and whatever it costs.
// FAIL - unsupported, with no substitute: an application that uses it gets wrong
// output, a failed draw, or nothing at all. The detail says what breaks.
//
// FAIL comes in two flavours, and the difference is about the BACKEND, not the row.
// Fail() is for a capability the backend cannot start without, and it drives the
// backend summary to UNSUPPORTED. FailOptional() is for a capability that is just as
// unusable but that the backend runs fine without, so the summary stays DEGRADED - a
// device with no dual-source blend still plays Minecraft, and reporting the whole
// backend as unusable because of it would be a lie in the other direction.
struct ReportBuilder {
BackendPostReport report;
Bool fatalFailed = false;
@@ -75,20 +101,27 @@ namespace MobileGL::MG_Util::SelfTest {
report.checks.push_back({Move(name), "PASS", Move(detail), RankPass});
}
// FAIL on a capability the backend cannot run without: the backend summary becomes
// UNSUPPORTED.
void Fail(String name, String detail) {
fatalFailed = true;
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
}
// FAIL on a capability with no substitute that the backend can nonetheless run
// without. The row is as red as any other FAIL - an application using it does not
// work - but the backend summary degrades rather than declaring the whole backend
// unusable.
void FailOptional(String name, String detail) {
warnUnmet = true;
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
}
void Warn(String name, String detail) {
warnUnmet = true;
report.checks.push_back({Move(name), "WARN", Move(detail), RankWarn});
}
void Info(String name, String detail) {
report.checks.push_back({Move(name), "INFO", Move(detail), RankInfo});
}
// A "Backend driver reported ..." identity string straight from the device
// driver; rendered after the regular rows.
void DriverReported(String name, String detail) {
@@ -156,17 +189,19 @@ namespace MobileGL::MG_Util::SelfTest {
// applications DO, not just what they can do: with the extension advertised, Iris
// and Sodium batch their pipeline compiles and poll GL_COMPLETION_STATUS_KHR.
//
// PASS when it is on (the intended configuration once the default flips), INFO when
// it is off - "off" is a supported configuration, not a degradation, so it must not
// colour the verdict. Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a
// user reading a POST page can tell which side of the switch they are on and how to
// change it.
// PASS when it is on (the intended configuration once the default flips), WARN when it
// is off: the capability is not advertised, and what stands in for it - compiling on
// the calling thread - produces exactly the same programs, just without the overlap.
// Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a user reading a POST page
// can tell which side of the switch they are on and how to change it.
void AppendAsyncShaderCompileRow(ReportBuilder& builder) {
constexpr const char* rowName = "Asynchronous shader compilation";
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
builder.Info(rowName,
"off; glCompileShader and glLinkProgram run on the calling thread and "
"GL_KHR_parallel_shader_compile is not advertised (set environment variable "
builder.Warn(rowName,
"off; GL_KHR_parallel_shader_compile is not advertised and "
"glCompileShader/glLinkProgram run on the calling thread instead. The "
"programs are identical - only the overlap is lost, so a shaderpack load "
"takes as long as its compiles do (set environment variable "
"MOBILEGL_ASYNC_SHADER_COMPILE=1 to enable it)");
return;
}
@@ -299,22 +334,30 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("Polygon mode",
"glPolygonMode GL_LINE/GL_POINT available via GL_NV/ANGLE_polygon_mode");
} else {
builder.Warn("Polygon mode",
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL");
builder.FailOptional("Polygon mode",
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT silently "
"falls back to GL_FILL. There is no substitute - wireframe and point "
"rasterization would have to be rebuilt out of line/point primitives - "
"so an application asking for either gets solid triangles instead");
}
if (caps.SupportsIndexedColorMask) {
builder.Pass("Indexed color mask",
"per-draw-buffer glColorMaski available (ES 3.2 core or draw_buffers_indexed)");
} else {
builder.Warn("Indexed color mask",
"no indexed glColorMaski; per-draw-buffer color masks fall back to draw buffer 0");
builder.FailOptional("Indexed color mask",
"no indexed glColorMaski; every per-draw-buffer colour mask collapses "
"onto draw buffer 0's, so an MRT pass that masks its attachments "
"differently writes the wrong channels to all but one of them, with "
"nothing to substitute");
}
if (caps.SupportsDualSourceBlend) {
builder.Pass("Dual-source blend",
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
} else {
builder.Warn("Dual-source blend",
"no GL_EXT_blend_func_extended; GL_SRC1_* dual-source blend factors hard-fail at draw");
builder.FailOptional("Dual-source blend",
"no GL_EXT_blend_func_extended; a draw using a GL_SRC1_* blend factor "
"hard-fails, and a second fragment output cannot be produced any other "
"way");
}
if (es31) {
@@ -326,10 +369,13 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("Vertex shader storage blocks",
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}", maxVertexSsboBlocks));
} else {
builder.Warn("Vertex shader storage blocks",
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the Flywheel/Create indirect draw "
"machinery cannot read indirect command buffers from the vertex stage",
maxVertexSsboBlocks));
builder.FailOptional(
"Vertex shader storage blocks",
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the vertex stage cannot read a "
"storage buffer at all, and there is nothing to read one with instead - the "
"Flywheel/Create indirect draw machinery, which fetches its per-instance data "
"from a vertex-stage SSBO, cannot run",
maxVertexSsboBlocks));
}
if (caps.MaxShaderStorageBufferBindings >= 8) {
@@ -348,14 +394,15 @@ namespace MobileGL::MG_Util::SelfTest {
if (caps.SupportsPersistentMapping) {
builder.Pass("GL_EXT_buffer_storage", "supported (persistent buffer mapping)");
} else {
builder.Info("GL_EXT_buffer_storage",
"not supported; no impact today: the frontend fully emulates persistent "
"mapping regardless of this extension");
builder.Warn("GL_EXT_buffer_storage",
"not supported; the frontend emulates persistent mapping with its own "
"shadow storage instead, so glBufferStorage and a GL_MAP_PERSISTENT_BIT "
"mapping behave correctly - at the cost of the shadow copy");
}
if (caps.SupportsBaseInstance) {
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
} else {
builder.Info("GL_EXT_base_instance",
builder.Warn("GL_EXT_base_instance",
"not supported; direct baseInstance draws are emulated by shifting the "
"instanced arrays' attribute offsets, and gl_BaseInstance by a uniform. "
"The one gap is an INDIRECT draw whose command carries a non-zero "
@@ -364,15 +411,17 @@ namespace MobileGL::MG_Util::SelfTest {
// Both multi-draw rows gate on the capability flags, not the entry-point pointers:
// eglGetProcAddress may hand back a non-NULL stub for these on drivers without the
// extension (NVIDIA ES does, and its glMultiDrawElementsBaseVertexEXT stub silently
// drops every draw), so the pointers prove nothing. Absence is INFO in both cases
// because MobileGL falls back to an equivalent per-draw loop.
// drops every draw), so the pointers prove nothing. Absence is WARN in both cases:
// MobileGL falls back to an equivalent per-draw loop, so the output is identical and
// only the command count changes.
if (caps.SupportsMultiDrawIndirect) {
builder.Pass("Multi-draw indirect",
"glMultiDrawArrays/ElementsIndirectEXT available via GL_EXT_multi_draw_indirect");
} else {
builder.Info("Multi-draw indirect",
"GL_EXT_multi_draw_indirect not supported; no impact today: multi-draw "
"indirect is decomposed into per-command indirect draws regardless");
builder.Warn("Multi-draw indirect",
"GL_EXT_multi_draw_indirect not supported; MobileGL decomposes a multi-draw "
"indirect batch into per-command indirect draws, which renders the same "
"thing for one driver call per command instead of one per batch");
}
if (caps.SupportsMultiDrawElementsBaseVertex) {
builder.Pass("Multi-draw base vertex",
@@ -380,7 +429,7 @@ namespace MobileGL::MG_Util::SelfTest {
"with GL_EXT_multi_draw_arrays); glMultiDrawElementsBaseVertex batches into one "
"driver call");
} else {
builder.Info("Multi-draw base vertex",
builder.Warn("Multi-draw base vertex",
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
"takes the next emulation tier instead, with identical output - see "
@@ -405,9 +454,12 @@ namespace MobileGL::MG_Util::SelfTest {
"available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a "
"whole batch into one draw");
} else {
builder.Info("Compute shaders",
"not available (pre-ES 3.1); no impact on the default multi-draw tiers, which "
"never use compute");
builder.FailOptional("Compute shaders",
"not available (pre-ES 3.1); MobileGL advertises "
"GL_ARB_compute_shader on an OpenGL 4.x context and there is no way to "
"run a glDispatchCompute without the ES counterpart, so a program with "
"a compute shader cannot be built at all. The default multi-draw tiers "
"never use compute, so nothing else is lost");
}
{
// The same resolution the backend runs, over the capabilities probed here.
@@ -418,45 +470,58 @@ namespace MobileGL::MG_Util::SelfTest {
// was consulted.
using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier;
String resolution;
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
builder.Info("Multi-draw elements tier",
"glMultiDrawElements(BaseVertex) emulation: " + resolution +
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE");
const MG_Config::GLESMultiDrawMode tier =
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
const String detail = "glMultiDrawElements(BaseVertex) emulation: " + resolution +
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE";
// PASS only on the tier that hands the whole batch to the driver in one call.
// Every other tier is a MobileGL substitute: the output is identical, the
// command count is not.
if (tier == MG_Config::GLESMultiDrawMode::Ext) {
builder.Pass("Multi-draw elements tier", detail);
} else {
builder.Warn("Multi-draw elements tier",
detail + " - the batch is replayed rather than handed over whole, "
"which renders the same thing for more driver calls");
}
}
if (caps.SupportsTextureBorderClamp) {
builder.Pass("Texture border clamp",
"supported (GL_TEXTURE_BORDER_COLOR reaches the driver, so "
"GL_CLAMP_TO_BORDER samples the colour the application set)");
} else {
builder.Warn("Texture border clamp",
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_border_clamp); "
"GL_TEXTURE_BORDER_COLOR is not synced to the driver at all, so anything "
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
"border instead of the requested colour");
builder.FailOptional(
"Texture border clamp",
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_border_clamp); "
"GL_TEXTURE_BORDER_COLOR is not synced to the driver at all, so anything "
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
"border instead of the requested colour, and no wrap mode substitutes for it");
}
if (caps.SupportsTextureCubeMapArray) {
builder.Pass("Texture cube map array",
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
"attached to a framebuffer)");
} else {
builder.Warn("Texture cube map array",
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
"map array texture gets no driver storage at all, so sampling one reads nothing "
"and rendering to one does not reach the screen");
builder.FailOptional(
"Texture cube map array",
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
"map array texture gets no driver storage at all, so sampling one reads nothing "
"and rendering to one does not reach the screen. Nothing substitutes: the "
"shaders that declare a samplerCubeArray do not compile either");
}
// WARN, not FAIL, and the choice is deliberate. The consequence is severe - buffer
// textures are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
// application may use one without asking, and nothing degrades gracefully: the
// texture gets no driver storage, and every shader declaring a samplerBuffer fails
// to compile outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer :
// require` for it below ESSL 320, so the program never links and every draw using it
// silently draws nothing. That is how Minecraft 26.3, whose cloud layer is built
// entirely from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its
// clouds. But FAIL means "this backend cannot run on this driver", and that is not
// true: such a device runs everything that does not touch a buffer texture. It is
// also exactly the shape of the "Texture cube map array" row above, which loses its
// shaders to the same SPIRV-Cross `: require` mechanism and is a WARN - two adjacent
// rows with one consequence must not carry two severities.
// FAIL, and specifically FailOptional. The consequence is severe - buffer textures
// are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an application
// may use one without asking, and nothing degrades gracefully: the texture gets no
// driver storage, and every shader declaring a samplerBuffer fails to compile
// outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require`
// for it below ESSL 320, so the program never links and every draw using it silently
// draws nothing. That is how Minecraft 26.3, whose cloud layer is built entirely
// from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its clouds.
// There is no substitute, which is what makes the row FAIL; the backend still RUNS
// everything that does not touch a buffer texture, which is what keeps the failure
// out of the backend summary. It is exactly the shape of the "Texture cube map
// array" row above, which loses its shaders to the same SPIRV-Cross `: require`
// mechanism - two adjacent rows with one consequence must carry one severity.
// The limit is stated on every tier because it is the one number an application can
// read, and on the None tier it is knowingly a fiction (see below).
{
@@ -493,16 +558,17 @@ namespace MobileGL::MG_Util::SelfTest {
break;
case Tier::None:
default:
builder.Warn("Buffer textures",
format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); "
"glTexBuffer does not exist, so a buffer texture gets no storage, "
"and any shader declaring a samplerBuffer fails to compile and "
"leaves its program unlinked - every draw using it is a silent "
"no-op. MobileGL still reports GL_MAX_TEXTURE_BUFFER_SIZE = {}: "
"the value is a floor it cannot honour, kept because an OpenGL "
"4.x context may not answer 0 and GL has no way to say that a "
"core feature is missing",
advertisedLimit));
builder.FailOptional(
"Buffer textures",
format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); "
"glTexBuffer does not exist, so a buffer texture gets no storage, "
"and any shader declaring a samplerBuffer fails to compile and "
"leaves its program unlinked - every draw using it is a silent "
"no-op. MobileGL still reports GL_MAX_TEXTURE_BUFFER_SIZE = {}: "
"the value is a floor it cannot honour, kept because an OpenGL "
"4.x context may not answer 0 and GL has no way to say that a "
"core feature is missing",
advertisedLimit));
break;
}
}
@@ -511,7 +577,10 @@ namespace MobileGL::MG_Util::SelfTest {
// either answer, and the rows exist so the two halves of the loss are named at
// startup instead of discovered as a shader that will not compile or an
// unexplained GL_INVALID_OPERATION at draw setup.
builder.Pass("fp64", AppendFp64AdvertisementNote(
// WARN, not PASS: ESSL has no 64-bit float type, so this backend does not support
// fp64 directly at all. What it has is a complete substitute - the shaders build and
// run - which is exactly what WARN means.
builder.Warn("fp64", AppendFp64AdvertisementNote(
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
@@ -529,10 +598,11 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
} else {
builder.Warn("Tessellation patch parameters",
"glPatchParameteri missing (pre-ES 3.2 without GL_EXT_tessellation_shader); "
"GL_PATCH_VERTICES stays at the driver default of 3 and a patch draw of any "
"other size renders nothing");
builder.FailOptional("Tessellation patch parameters",
"glPatchParameteri missing (pre-ES 3.2 without "
"GL_EXT_tessellation_shader); GL_PATCH_VERTICES stays at the driver "
"default of 3 and a patch draw of any other size renders nothing - "
"the patch size cannot be communicated any other way");
}
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
@@ -548,7 +618,9 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("GL_EXT_texture_norm16", "supported");
} else {
builder.Warn("GL_EXT_texture_norm16",
"not supported; 16-bit normalized texture formats need emulation");
"not supported; MobileGL substitutes a wider format for every 16-bit "
"normalized texture, so the texels are still readable at their declared "
"precision at the cost of the extra storage");
}
if (caps.SupportsRenderSnorm) {
builder.Pass("GL_EXT_render_snorm",
@@ -581,23 +653,31 @@ namespace MobileGL::MG_Util::SelfTest {
"render targets (Iris reports GL_FRAMEBUFFER_UNSUPPORTED and refuses to load)");
}
// INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on
// its own threads, and MobileGL's asynchronous compilation does not depend on it
// in the slightest - the pool parallelises GLSL -> SPIR-V -> ESSL translation,
// which is where a shaderpack load actually spends its time, and it does that on
// a driver that has never heard of the extension. The row exists so that the day
// the driver-side half is overlapped too, the POST already says which devices can.
builder.Info("Driver GL_KHR_parallel_shader_compile",
caps.SupportsParallelShaderCompile
? "supported; the device driver can also compile the translated ESSL off-thread"
: "not supported; the device driver compiles the translated ESSL on the calling "
"thread (MobileGL's own compile pool is unaffected)");
// WARN and never FAIL when it is absent: this is the HOST driver's ability to
// compile its own ESSL on its own threads, and MobileGL's own compile pool stands in
// for all of it that matters - the pool parallelises GLSL -> SPIR-V -> ESSL
// translation, which is where a shaderpack load actually spends its time, and it
// does that on a driver that has never heard of the extension. The row exists so
// that the day the driver-side half is overlapped too, the POST already says which
// devices can.
if (caps.SupportsParallelShaderCompile) {
builder.Pass("Driver GL_KHR_parallel_shader_compile",
"supported; the device driver can also compile the translated ESSL off-thread");
} else {
builder.Warn("Driver GL_KHR_parallel_shader_compile",
"not supported; the device driver compiles the translated ESSL on the calling "
"thread. MobileGL's own compile pool substitutes for the expensive half of the "
"work (GLSL -> SPIR-V -> ESSL) and is unaffected, so loads still overlap");
}
builder.Info("Indirect gl_InstanceID semantics",
caps.IndirectDrawInstanceIdIncludesBaseInstance
? "includes baseInstance (ANGLE-style; MobileGL's shader rewrite keeps gl_InstanceID "
"zero-based)"
: "conforming (zero-based)");
if (caps.IndirectDrawInstanceIdIncludesBaseInstance) {
builder.Warn("Indirect gl_InstanceID semantics",
"includes baseInstance (ANGLE-style), which is not what GL promises; "
"MobileGL's shader rewrite subtracts it back out so gl_InstanceID stays "
"zero-based and instanced indirect draws index their arrays correctly");
} else {
builder.Pass("Indirect gl_InstanceID semantics", "conforming (zero-based)");
}
builder.DriverReported("Backend driver reported GL_VENDOR", caps.GLESVendorString);
builder.DriverReported("Backend driver reported GL_RENDERER", caps.GLESRendererString);
@@ -612,17 +692,21 @@ namespace MobileGL::MG_Util::SelfTest {
const MG_External::GLESFunctionsTable& glesFuncs) {
const String disabledNote = TimerQueryDisabledNote();
if (!caps.SupportsDisjointTimerQuery) {
builder.Warn("Timer queries",
"GL_EXT_disjoint_timer_query not supported; timer queries unavailable; "
"Minecraft F3 GPU% will not show" +
disabledNote);
builder.FailOptional("Timer queries",
"GL_EXT_disjoint_timer_query not supported; there is no way to time "
"GPU work from the client, so glBeginQuery(GL_TIME_ELAPSED) has "
"nothing to stand in for it and Minecraft's F3 GPU% will not show" +
disabledNote);
return;
}
// Every emit carries the extension-presence fact the old standalone
// GL_EXT_disjoint_timer_query row showed, plus the probe outcome.
const String extensionPresent = "GL_EXT_disjoint_timer_query extension present";
// FailOptional: a driver that advertises the extension and then cannot serve a
// query is broken in a way nothing substitutes for, but timing GPU work is not
// something the backend needs in order to run.
const auto fail = [&](const String& detail) {
builder.Fail("Timer queries", extensionPresent + "; but " + detail + disabledNote);
builder.FailOptional("Timer queries", extensionPresent + "; but " + detail + disabledNote);
};
if (!glesFuncs.glGenQueries || !glesFuncs.glDeleteQueries || !glesFuncs.glBeginQuery ||
@@ -756,8 +840,11 @@ namespace MobileGL::MG_Util::SelfTest {
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
"gl_FragCoord.w emulation path)";
// FailOptional: a shaderpack that declares a noperspective varying renders it wrong
// and nothing stands in for the interpolation, but everything that does not use one
// is unaffected, so the backend still runs.
const auto fail = [&](const String& detail) {
builder.Fail("noperspective interpolation", pathNote + "; " + detail);
builder.FailOptional("noperspective interpolation", pathNote + "; " + detail);
};
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
@@ -1155,6 +1242,11 @@ namespace MobileGL::MG_Util::SelfTest {
MG_Backend::DirectGLES::PopulateFormatCapabilities(
glesFuncs, caps, builder.report.formatCapabilities.value());
ReportThreeChannelColorAttachments(builder, caps, builder.report.formatCapabilities.value());
// The "Known Driver Bugs" section. Deliberately last, and deliberately not a
// builder.Pass/Warn/Fail row: these are not capability checks and they must not move
// the backend verdict, which is about whether the backend can RUN on this driver.
// Only bugs the device actually has come back, so a clean driver adds nothing here.
builder.report.knownDriverBugs = CollectGlesKnownDriverBugs(glesFuncs);
} while (false);
}
@@ -1250,8 +1342,10 @@ namespace MobileGL::MG_Util::SelfTest {
const String timestampFacts =
format("timestampValidBits = {} on the graphics queue family; timestampPeriod = {} ns per tick",
timestampValidBits, timestampPeriod);
// FailOptional, for the same reason as the GLES row: the backend does not need to
// time GPU work in order to run.
const auto fail = [&](const String& detail) {
builder.Fail("Timer queries", timestampFacts + "; but " + detail + disabledNote);
builder.FailOptional("Timer queries", timestampFacts + "; but " + detail + disabledNote);
};
const auto vkCreateDeviceFn =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
@@ -1471,7 +1565,13 @@ namespace MobileGL::MG_Util::SelfTest {
Bool subgroupPropertiesAvailable,
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
constexpr const char* RowName = "Subgroup first-reduction witness";
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
// FailOptional, not Fail. The witness reports whether the NATIVE subgroup
// first-reduction works; when it does not, the renderer takes its non-subgroup
// iteration path and draws the same image. Both an Adreno 830 and Mesa lavapipe
// fail this row's topology check today while running the DirectVulkan backend
// perfectly well, so a fatal verdict here would have the screen announce that a
// backend the user is looking at through that very backend cannot run.
const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); };
if (!subgroupPropertiesAvailable) {
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
@@ -1498,7 +1598,13 @@ namespace MobileGL::MG_Util::SelfTest {
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
builder.Info(RowName, eligibility.detail);
// WARN, not FAIL: there is nothing to witness on a device with no native
// subgroup contract, and the renderer takes its non-subgroup iteration path,
// which produces the same image.
builder.Warn(RowName,
eligibility.detail +
"; the renderer takes its non-subgroup iteration path instead, which "
"renders the same thing without the first-reduction shortcut");
return;
}
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
@@ -2197,20 +2303,23 @@ namespace MobileGL::MG_Util::SelfTest {
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
} else {
builder.Info("multiDrawIndirect",
"unsupported; multi-draw batches fall back to one draw per command (tier "
"\"indirect\" of the multi-draw dispatch is unavailable)");
builder.Warn("multiDrawIndirect",
"unsupported; MobileGL unrolls a multi-draw batch into one draw per command "
"(tier \"indirect\" of the multi-draw dispatch is unavailable), which renders "
"the same thing for more commands");
}
if (features.drawIndirectFirstInstance == VK_TRUE) {
builder.Pass("drawIndirectFirstInstance", "indirect commands may carry a non-zero firstInstance");
} else {
builder.Warn("drawIndirectFirstInstance",
"unsupported; indirect commands with a non-zero baseInstance cannot run natively");
builder.FailOptional("drawIndirectFirstInstance",
"unsupported; an indirect command carrying a non-zero baseInstance "
"cannot run, and the offset cannot be folded into the command from the "
"CPU because the command is on the GPU");
}
// Multi-draw dispatch tiers (ext -> indirect -> unroll). INFO on the missing
// pieces: every tier has a fallback, nothing is lost, only batched into more
// commands. The renderer resolves the same chain at device creation, clamped
// by MOBILEGL_MAGMA_MULTIDRAW_MODE.
// Multi-draw dispatch tiers (ext -> indirect -> unroll). WARN on the missing
// pieces: every tier has a fallback that renders the same thing, only batched
// into more commands. The renderer resolves the same chain at device creation,
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE.
{
Bool multiDrawExtUsable = false;
if (HasVkExtension(deviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) &&
@@ -2227,8 +2336,9 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("VK_EXT_multi_draw",
"supported; a glMultiDraw* batch runs as one vkCmdDrawMulti(Indexed)EXT");
} else {
builder.Info("VK_EXT_multi_draw",
"unsupported; glMultiDraw* batches use the indirect or unrolled tier");
builder.Warn("VK_EXT_multi_draw",
"unsupported; glMultiDraw* batches take the indirect or unrolled tier "
"instead, with identical output");
}
const char* resolvedTier = multiDrawExtUsable ? "ext"
: features.multiDrawIndirect == VK_TRUE ? "indirect"
@@ -2241,32 +2351,46 @@ namespace MobileGL::MG_Util::SelfTest {
: multiDrawMode == MG_Config::MultiDrawMode::Indirect ? "indirect"
: "unroll");
}
builder.Info("Multi-draw dispatch tier", tierDetail);
// PASS only on the tier that hands the whole batch to the driver in one command.
if (multiDrawExtUsable) {
builder.Pass("Multi-draw dispatch tier", tierDetail);
} else {
builder.Warn("Multi-draw dispatch tier",
tierDetail + "; the batch is replayed rather than handed over whole, which "
"renders the same thing for more commands");
}
}
if (features.vertexPipelineStoresAndAtomics == VK_TRUE) {
builder.Pass("vertexPipelineStoresAndAtomics",
"supported by driver (not currently enabled by the DirectVulkan backend)");
} else {
builder.Warn("vertexPipelineStoresAndAtomics",
"unsupported; shaders that write storage buffers from the vertex stage will not work");
builder.FailOptional("vertexPipelineStoresAndAtomics",
"unsupported; a shader that writes a storage buffer or runs an atomic "
"from the vertex stage cannot build a pipeline, and the write cannot be "
"moved to another stage without changing what the shader does");
}
if (features.fillModeNonSolid == VK_TRUE) {
builder.Pass("fillModeNonSolid", "glPolygonMode GL_LINE/GL_POINT rasterization supported");
} else {
builder.Warn("fillModeNonSolid",
"unsupported; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL (no wireframe/point "
"rasterization)");
builder.FailOptional("fillModeNonSolid",
"unsupported; glPolygonMode GL_LINE/GL_POINT silently falls back to "
"GL_FILL, and wireframe/point rasterization cannot be rebuilt out of "
"the triangle pipeline");
}
if (features.independentBlend == VK_TRUE) {
builder.Pass("independentBlend", "per-draw-buffer glColorMaski and indexed blend state supported");
} else {
builder.Warn("independentBlend",
"unsupported; per-draw-buffer glColorMaski falls back to draw buffer 0 for all attachments");
builder.FailOptional("independentBlend",
"unsupported; every attachment takes draw buffer 0's colour mask and "
"blend state, so an MRT pass that configures them separately writes the "
"wrong channels to all but one attachment");
}
if (features.dualSrcBlend == VK_TRUE) {
builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported");
} else {
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
builder.FailOptional("dualSrcBlend",
"unsupported; a draw using a GL_SRC1_* blend factor hard-fails, and a "
"second fragment output cannot be produced any other way");
}
// The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be
// read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is
@@ -2306,10 +2430,11 @@ namespace MobileGL::MG_Util::SelfTest {
"back on its own; a format that refuses the flag is detected at image "
"creation and declines per-slice attachment)");
} else {
builder.Warn("2D-array-compatible 3D images",
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
"slice past the first");
builder.FailOptional("2D-array-compatible 3D images",
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour "
"attachments; glFramebufferTextureLayer on a GL_TEXTURE_3D texture "
"is declined for every slice past the first, and a 3D slice cannot "
"be rendered into any other way");
}
}
if (features.imageCubeArray == VK_TRUE) {
@@ -2317,31 +2442,43 @@ namespace MobileGL::MG_Util::SelfTest {
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
"attached to a framebuffer per layer");
} else {
builder.Warn("imageCubeArray",
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
"one reads nothing and glFramebufferTextureLayer on one is declined");
builder.FailOptional("imageCubeArray",
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, "
"so sampling one reads nothing and glFramebufferTextureLayer on one is "
"declined - there is no substitute image type");
}
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
// and Mali both report VK_FALSE, so the demoted row is what a real phone prints; lavapipe
// reports VK_TRUE and gets real doubles.
if (features.shaderFloat64 == VK_TRUE) {
builder.Pass("fp64", AppendFp64AdvertisementNote(
"native (device shaderFloat64 = supported) - every double / dvec / dmat in "
"a shader keeps its declared width, blocks keep the layout glslang computed "
"for them, and glUniform*d stores 8-byte components. The one exception is a "
"VERTEX stage that declares a 64-bit float INPUT: there is no 64-bit vertex "
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
"device without the feature"));
} else {
// WARN rather than PASS: the device does not support fp64 at all here, and what
// stands in for it is a MobileGL pass that narrows the shader. It runs, at single
// precision - the definition of a substitute.
builder.Warn("fp64", AppendFp64AdvertisementNote(
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
"shaders BUILD AND RUN at single precision instead of failing to create a "
"shader module. A block containing a double is re-laid-out for the narrowed "
"members, so an application that hard-codes std140 offsets computed for "
"doubles must query them instead"));
}
// Reported whichever way the device answers, because MobileGL no longer follows the
// device here: every 64-bit float is narrowed to 32 bits before any module reaches this
// backend (DemoteFloat64Pass), so the Float64 capability is never declared and a device
// that HAS the feature gains nothing from it. The device's own answer is still worth
// printing - it is the reason the demotion is unconditional.
builder.Pass("fp64", AppendFp64AdvertisementNote(
format("demoted to fp32 (device shaderFloat64 = {}) - every double / dvec / "
"dmat in a shader is narrowed to 32 bits before pipeline creation, so "
"such shaders BUILD AND RUN at single precision on every device "
"instead of failing to create a shader module on the ones without the "
"feature. A block containing a double is re-laid-out for the narrowed "
"members, so an application that hard-codes std140 offsets computed "
"for doubles must query them instead",
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
builder.Warn("64-bit vertex attributes",
"narrowed to float32; there is no 64-bit shader input left to feed after the fp64 "
"demotion above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most "
"devices anyway. glVertexAttribLFormat succeeds, its state is queryable, and an "
"ENABLED 64-bit array IS fetched - the source doubles are deinterleaved into a "
"float32 stream at draw, so values outside float32's range or precision are "
"rounded rather than exact");
"narrowed to float32 on every device, whatever the row above says: there is no "
"VK_FORMAT_R64*_SFLOAT vertex fetch here, and the format is chosen from the VAO "
"attribute, which does not know what type the shader declared - which is why a "
"vertex stage with a 64-bit float INPUT is narrowed whole even where fp64 is native. "
"glVertexAttribLFormat succeeds, its state is queryable, and an ENABLED 64-bit array "
"IS fetched - the source doubles are deinterleaved into a float32 stream at draw, so "
"values outside float32's range or precision are rounded rather than exact");
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
@@ -2359,8 +2496,10 @@ namespace MobileGL::MG_Util::SelfTest {
if (shaderDrawParameters) {
builder.Pass("shaderDrawParameters", "gl_DrawID/gl_BaseVertex/gl_BaseInstance shaders supported");
} else {
builder.Warn("shaderDrawParameters",
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
builder.FailOptional("shaderDrawParameters",
"unavailable; a shader reading gl_DrawID, gl_BaseVertex or "
"gl_BaseInstance has no SPIR-V builtin to read them from, so such "
"shaders do not work and nothing supplies the values instead");
}
summary.shaderDrawParametersSupported = shaderDrawParameters;
@@ -2397,10 +2536,12 @@ namespace MobileGL::MG_Util::SelfTest {
"supported; flat varyings take GL's last vertex and transform feedback records "
"strip/fan triangles in GL's vertex order");
} else {
builder.Warn("provokingVertexLast",
"unsupported; flat-shaded varyings take a primitive's first vertex instead of GL's "
"last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated "
"(e.g. 0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
builder.FailOptional("provokingVertexLast",
"unsupported; flat-shaded varyings take a primitive's first vertex "
"instead of GL's last, and transform feedback records "
"TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated (e.g. 0,1,2 / 1,3,2 "
"instead of 0,1,2 / 2,1,3). Rewriting the convention would mean "
"reordering every index buffer, which MobileGL does not do");
}
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
builder.Warn("transformFeedbackPreservesProvokingVertex",
@@ -2429,9 +2570,10 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("primitiveTopologyListRestart",
"primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)");
} else {
builder.Warn("primitiveTopologyListRestart",
"unsupported; primitive restart works on strip/fan topologies only, list-topology restart "
"hard-fails at draw");
builder.FailOptional("primitiveTopologyListRestart",
"unsupported; primitive restart works on strip/fan topologies only, and "
"a list-topology draw with GL_PRIMITIVE_RESTART enabled hard-fails - "
"splitting the index stream on the CPU is not done");
}
// Core 1.0 features the backend turns GL stages into pipeline stages with.
@@ -2441,9 +2583,10 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("tessellationShader",
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
} else {
builder.Warn("tessellationShader",
"unsupported; a program with a tessellation control/evaluation shader cannot build a "
"pipeline, so GL_PATCHES draws render nothing");
builder.FailOptional("tessellationShader",
"unsupported; a program with a tessellation control/evaluation shader "
"cannot build a pipeline, so GL_PATCHES draws render nothing and there "
"is no stage to run the tessellation on instead");
}
Bool vertexAttributeInstanceRateDivisor = false;
@@ -2461,10 +2604,11 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Pass("vertexAttributeInstanceRateDivisor",
"supported (glVertexAttribDivisor advances an attribute every N instances)");
} else {
builder.Warn("vertexAttributeInstanceRateDivisor",
"unsupported; Vulkan's instance input rate can only advance once per instance, so "
"every non-zero glVertexAttribDivisor behaves as 1 and instanced attributes meant to "
"change every N instances change every one");
builder.FailOptional("vertexAttributeInstanceRateDivisor",
"unsupported; Vulkan's instance input rate can only advance once per "
"instance, so every non-zero glVertexAttribDivisor behaves as 1 and "
"instanced attributes meant to change every N instances change every "
"one - silently wrong geometry, with no substitute fetch rate");
}
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
@@ -2488,11 +2632,18 @@ namespace MobileGL::MG_Util::SelfTest {
format("basic subgroup operations in compute, subgroup size {}",
subgroupProperties.subgroupSize));
} else {
builder.Warn("Compute shader subgroup",
"basic subgroup operations are not usable from compute shaders");
builder.FailOptional("Compute shader subgroup",
"basic subgroup operations are not usable from compute shaders, so "
"MobileGL withholds GL_KHR_shader_subgroup and the subgroup "
"iteration-render-pass path cannot run; there is no scalar rewrite "
"that stands in for a subgroup reduction");
}
} else {
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
builder.FailOptional("Compute shader subgroup",
"subgroup properties could not be queried (no "
"vkGetPhysicalDeviceProperties2, or a pre-1.1 device), so MobileGL "
"withholds GL_KHR_shader_subgroup and the subgroup paths are "
"unavailable whatever the hardware can actually do");
}
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
@@ -2512,9 +2663,9 @@ namespace MobileGL::MG_Util::SelfTest {
if (indexTypeUint8) {
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
} else {
builder.Warn("Index type uint8",
"not supported; GL_UNSIGNED_BYTE index buffers cannot be drawn (the backend "
"has no conversion fallback and asserts on uint8 index draws)");
builder.FailOptional("Index type uint8",
"not supported; a GL_UNSIGNED_BYTE index buffer cannot be drawn - the "
"backend has no widening conversion and asserts on uint8 index draws");
}
builder.DriverReported("Backend driver reported device", String(properties.deviceName));
builder.DriverReported("Backend driver reported driver version", driverVersionString + " (vendor-encoded)");
@@ -2532,12 +2683,14 @@ namespace MobileGL::MG_Util::SelfTest {
ProbeVulkanTimerQuery(builder, getInstanceProcAddr, instance, physicalDevice,
graphicsQueueFamilyIndex, graphicsQueueTimestampValidBits, timestampPeriod);
} else {
builder.Warn("Timer queries",
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
"per tick; timestamps unsupported on the graphics queue; timer queries "
"unavailable",
timestampPeriod) +
TimerQueryDisabledNote());
builder.FailOptional(
"Timer queries",
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
"per tick; the graphics queue cannot write a timestamp at all, so there is "
"nothing to time GPU work with and glBeginQuery(GL_TIME_ELAPSED) has no "
"substitute",
timestampPeriod) +
TimerQueryDisabledNote());
}
if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) {
MG_External::VulkanCapabilities formatProbeCapabilities{};
+33 -5
View File
@@ -7,26 +7,43 @@
// End of Source File Header
#pragma once
#include "DriverBugProbes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::SelfTest {
// One row of a backend power-on self-test (POST) report.
//
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL; INFO IS FOR IDENTITY ONLY.
// PASS - the backend supports the capability directly.
// WARN - not directly, but a MobileGL quirk substitutes and the application still sees
// correct behaviour; the detail names the substitute and what it costs.
// FAIL - unsupported with no substitute; an application that uses it gets wrong output, a
// failed draw, or nothing.
// INFO - identity only: renderer name, API version, driver strings, and the strings
// MobileGL itself reports to applications. Never a capability answer.
// A FAIL row does not by itself mean the backend cannot run - see BackendPostReport::verdict.
struct PostCheck {
String name;
String status; // "PASS" | "WARN" | "FAIL" | "INFO"
String detail;
// Display ordering rank within a backend section (lower renders first): FAIL,
// WARN, PASS, INFO, then the device-driver identity strings, then the strings
// WARN, PASS, then the device-driver identity strings, then the strings
// MobileGL itself reports to applications. Rows are stable-sorted by this rank
// before the report is returned; it is not serialized to JSON.
Int displayRank = 0;
};
// Verdict for one backend's device driver.
// - UNSUPPORTED: a fatal check failed; the backend cannot run on this driver.
// - DEGRADED: every fatal check passed but at least one soft expectation is unmet.
// - OK: all expectations met.
// Verdict for one backend's device driver, derived from the rows.
// - UNSUPPORTED: a REQUIRED capability failed; the backend cannot run on this driver.
// - DEGRADED: every required capability is present, but at least one row is WARN or is a
// FAIL on an optional capability - the backend runs, and something an application might
// ask for is substituted or missing.
// - OK: every row passed.
// So a section can carry FAIL rows and still be DEGRADED rather than UNSUPPORTED: a device
// with no dual-source blend still runs. Which capabilities are required is decided at the
// row (ReportBuilder::Fail vs ReportBuilder::FailOptional in DriverPost.cpp).
// available is false when no probeable driver exists at all (library missing, display
// uninitializable, zero Vulkan physical devices, ...).
struct BackendPostReport {
@@ -34,6 +51,17 @@ namespace MobileGL::MG_Util::SelfTest {
String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED"
String rendererInfo;
Vector<PostCheck> checks;
// The "Known Driver Bugs" section, kept apart from `checks` on purpose. `checks` asks
// whether a feature is there and roughly works; these are core features the driver
// claims, accepts, and then does not perform - a separate question, from a separate
// inventory (campaign findings, not the extension string). See DriverBugProbes.h.
//
// Only bugs this device ACTUALLY HAS appear here: a probe that comes back clean
// contributes no entry, so an unaffected driver renders the section empty rather than
// as a list of reassurances. That is also why the verdict vocabulary is FIXED /
// UNFIXABLE rather than PASS / FAIL - every row is a bug that is present, and the
// verdict says whether MobileGL can do anything about it.
Vector<DriverBugFinding> knownDriverBugs;
Optional<MG_Backend::FormatCapabilityCache> formatCapabilities;
};
@@ -144,6 +144,27 @@ namespace {
out << '}';
}
out << ']';
// The "Known Driver Bugs" section, separate from "checks" because it answers a
// different question and uses a different verdict vocabulary (FIXED | UNFIXABLE).
// Every entry is a bug the device HAS - a clean probe contributes nothing - so an
// unaffected driver serializes an empty array and the screen renders no section.
out << ",\"knownDriverBugs\":[";
for (SizeT i = 0; i < report.knownDriverBugs.size(); ++i) {
const MobileGL::MG_Util::SelfTest::DriverBugFinding& bug = report.knownDriverBugs[i];
if (i != 0) {
out << ',';
}
out << "{\"name\":";
AppendJsonString(out, bug.name);
out << ",\"verdict\":";
AppendJsonString(out, bug.verdict == MobileGL::MG_Util::SelfTest::DriverBugVerdict::Fixed
? "FIXED"
: "UNFIXABLE");
out << ",\"detail\":";
AppendJsonString(out, bug.detail);
out << '}';
}
out << ']';
if (report.formatCapabilities.has_value()) {
AppendFormatCapabilitiesJson(out, report.formatCapabilities.value());
}
+18 -2
View File
@@ -117,8 +117,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// preprocessed text is in the L1 key verbatim, a strictly finer discriminator
// than the extension list. (E_GL_ARB_gpu_shader_fp64 is never read by the front
// end at all: MOBILEGL_ADVERTISE_FP64 only adds it to the extension STRING the
// application queries, and DemoteFloat64Pass runs unconditionally either way, so
// fp64 GLSL translates identically with the flag on or off.)
// application queries, and glslang parses `double` the same way either way.)
// * params.SupportsShaderFloat64, i.e. ConsumesFloat64Natively(). glslang produces
// the SAME SPIR-V under it - a `double` parses, reflects and generates as a
// 64-bit float regardless - so it is not a front-end input and putting it here
// would also cost L1c (the parse-verdict memo, which keys on this fingerprint and
// is genuinely independent of it) a false miss per backend. It DOES change what
// SanitizeAndOptimizeBinary produces, and L1's payload is post-Sanitize, so it
// rides in L1's key as a field of its own; see SpirvTranslationKeyInputs.
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
// the backends, never by the parse, the link or reflection.
// * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
@@ -135,6 +141,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; }
// Whether the backend this env was captured against can CONSUME a module that still
// declares 64-bit floats - the one thing that decides whether the transpile keeps
// `double` or narrows it (FlattenFloat64StorageBlockPass + DemoteFloat64Pass).
//
// The no-backend case answers FALSE, deliberately opposite to IsExtensionAdvertised's
// permissive fallback: an extension the frontend cannot gate against is best assumed
// present, but a hardware capability nothing has declared must be assumed absent. The
// demoted module is the one that works everywhere, so it is what a standalone compile
// (an internal shader object, a unit test) gets.
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
// Matches the historical rule exactly: with no active backend every extension counts
// as advertised, because the frontend then has nothing to gate against.
Bool IsExtensionAdvertised(GLExtension extension) const {
@@ -19,6 +19,7 @@
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/DemoteFloat64Pass.h"
#include "SpirvPasses/FlattenFloat64StorageBlockPass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/LowerViewportIndexPass.h"
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
@@ -700,6 +701,57 @@ namespace MobileGL {
return false;
}
namespace {
// The leaf-width test behind ModuleDeclaresFloat64VertexInput, and it is a LEAF
// test rather than a shape test on purpose: a `dmat4` input is an OpTypeMatrix of
// OpTypeVector of OpTypeFloat 64, and it is as unfetchable as a bare `double`.
Bool TypeHoldsFloat64(const spvtools::opt::analysis::Type* type) {
if (type == nullptr) return false;
if (const auto* scalar = type->AsFloat()) return scalar->width() == 64;
if (const auto* vector = type->AsVector()) return TypeHoldsFloat64(vector->element_type());
if (const auto* matrix = type->AsMatrix()) return TypeHoldsFloat64(matrix->element_type());
if (const auto* array = type->AsArray()) return TypeHoldsFloat64(array->element_type());
return false;
}
} // namespace
Bool ShaderCompiler::ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64VertexInput"),
spirv.data(), spirv.size());
if (!context) {
return false;
}
// Vertex only. Every other stage's inputs come from another stage's outputs, which
// MobileGL never re-formats, so a 64-bit varying between two stages is the driver's
// business and not this question's.
auto entryPoints = context->module()->entry_points();
if (entryPoints.begin() == entryPoints.end()) return false;
const spvtools::opt::Instruction& entryPoint = *entryPoints.begin();
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
spv::ExecutionModel::Vertex) {
return false;
}
auto* typeManager = context->get_type_mgr();
auto* defUseManager = context->get_def_use_mgr();
for (const spvtools::opt::Instruction& variable : context->module()->types_values()) {
if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) continue;
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
const spvtools::opt::Instruction* pointerType = defUseManager->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->NumInOperands() < 2) continue;
if (TypeHoldsFloat64(typeManager->GetType(pointerType->GetSingleWordInOperand(1)))) {
return true;
}
}
return false;
}
Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
@@ -750,7 +802,8 @@ namespace MobileGL {
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool validateOutput,
const bool enableSpirvValidation) {
const bool enableSpirvValidation,
const bool nativeFloat64) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
@@ -787,17 +840,43 @@ namespace MobileGL {
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
// No mobile GPU has 64-bit floats: Adreno and Mali both report shaderFloat64 ==
// VK_FALSE, and ESSL has no fp64 type for SPIRV-Cross to emit. Demoting here - in
// the one chain every module goes through, on both backends, at link - is what
// makes `double` compile at all, and makes it behave the SAME everywhere, which
// matters because the GL frontend's uniform storage cannot be per-backend: the
// glUniform*d shadow narrows to float unconditionally to match this. Runs last so
// no earlier pass ever has to reason about a width it will not see in the output;
// in particular it runs before the backends' PackDoubleVertexInputsPass, whose
// OpBitcast this one would otherwise decline on. Costs one types_values() walk on
// the overwhelming majority of modules, which declare no 64-bit float at all.
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
// The fp64 tail, and the ONE part of this chain that is not the same on every
// backend. Both passes are skipped when the backend can consume Float64 itself
// (`nativeFloat64`, i.e. VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan):
// there is nothing to emulate then, and narrowing would only throw away precision
// the driver was willing to give. That is DirectVulkan-on-lavapipe today and
// nothing else - Adreno and Mali both report shaderFloat64 == VK_FALSE, and
// DirectGLES can never qualify because GLSL ES has no fp64 type for SPIRV-Cross to
// emit at all, so on every real mobile device this branch is not taken and the two
// passes run exactly as they always have.
//
// Demoting here - in the one chain every module goes through, at link - is what
// makes `double` compile at all where the hardware has none, and makes it behave
// the SAME across both backends of such a device, which matters because the GL
// frontend's uniform storage is per PROGRAM rather than per call: the glUniform*d
// shadow narrows to float to match this. Runs last so no earlier pass ever has to
// reason about a width it will not see in the output; in particular it runs before
// the backends' PackDoubleVertexInputsPass, whose OpBitcast this one would
// otherwise decline on. Costs one types_values() walk on the overwhelming majority
// of modules, which declare no 64-bit float at all.
// ...but demoting a double that lives in a SHADER STORAGE BLOCK also repacks that
// block, and the bytes an application put in the buffer do not move with it. This
// runs first and takes those blocks out of the demotion's hands: each becomes a
// flat `uint` array whose index arithmetic carries the std140/std430 offsets
// glslang computed WITH the doubles in place, so the layout survives byte for byte
// and only the VALUES narrow. Gated on a block actually holding a 64-bit float, so
// every other module pays one types_values() walk and nothing else, and it declines
// (leaving the block for the demotion to handle the old way) on any shape it cannot
// re-address exactly. See FlattenFloat64StorageBlockPass.h. It is skipped with the
// demotion rather than kept: its whole purpose is to preserve the byte layout ACROSS
// a narrowing that is no longer happening, and flattening a block a native driver
// would have laid out correctly by itself only costs the shader its index
// arithmetic.
if (!nativeFloat64) {
optimizer.RegisterPass(
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass());
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
}
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
outputBinary, validateOutput, enableSpirvValidation);
@@ -945,6 +1024,16 @@ namespace MobileGL {
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
}
bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized) {
return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax,
outSignedNormalized);
}
Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat);
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -23,10 +23,22 @@ namespace MobileGL {
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
static Result<SharedPtr<glslang::TProgram>> LinkProgram(const ProgramAttrib& attrib);
static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
// `nativeFloat64` is the caller's FINAL verdict, not a capability read: true means
// the two fp64 passes at the tail of the chain are skipped and real doubles reach
// the driver. False - which is DirectGLES always, every mobile device, and the
// no-backend default - runs the chain exactly as it always has. It is the ONE
// argument of this function that changes the output bytes, which is why it is
// also a field of the L1 memo's key.
//
// Production sets it in ProgramSpirvTask::GenerateSpirv, which takes the verdict
// for the WHOLE program (CompileEnv::ConsumesFloat64Natively() minus the
// 64-bit-vertex-input exception) before touching any module. Do not re-derive it
// per module: the global UBO is one buffer every stage reads.
static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool validateOutput = true,
bool enableSpirvValidation = false);
bool enableSpirvValidation = false,
bool nativeFloat64 = false);
// Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
// (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
// Only for backends without native draw-parameter support (DirectGLES).
@@ -286,6 +298,17 @@ namespace MobileGL {
// Channels a GL image internal format really has (1-4), 0 when it is not one of
// the forty image formats.
static Uint ImageFormatChannelCount(Uint glInternalFormat);
// Whether the carrier holds the format's channels as the INTEGER CODES of a
// normalized value, and the largest code each channel can hold. See
// WidenImageFormatsPass::NormalizedImageCarrierCodes - DirectGLES needs it for
// both halves of the transfer, which no longer share the frontend format's
// component class with the ES storage.
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
// The single-channel core format a non-core BUFFER image is SPLIT into, or 0. See
// WidenImageFormatsPass::SplitCoreEsslBufferImageFormat - DirectGLES asks it for
// glTexBuffer's internal format and for glBindImageTexture's.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
@@ -432,6 +455,18 @@ namespace MobileGL {
// what the backends report: no mobile driver can build such a module.
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
// True when the module is a VERTEX stage that declares a 64-bit float INPUT
// variable - `in double`, `in dvec2`, `in dmat3` and so on.
//
// Asked only on a backend with native fp64, and it is what keeps that backend's
// vertex path consistent. No backend here can FETCH 64 bits (VK_FORMAT_R64*_SFLOAT
// is optional and lavapipe advertises none of them), and the format is chosen from
// the VAO attribute, which does not know what the shader declared - so a module
// that keeps a Float64 input would be fed a narrowed float32 stream, or a packed
// uint pair with no matching format. Such a module is demoted WHOLE instead, which
// is exactly what every other backend does to it.
static Bool ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv);
// True when the module declares an Input variable carrying a Location - i.e. a
// user-defined varying or a per-patch input, as opposed to a built-in.
//
@@ -29,6 +29,16 @@ namespace MobileGL {
// Espryt path never even reaches the driver. Demotion is what makes `double` in an
// application's GLSL compile and run everywhere, at fp32 precision.
//
// WHEN IT RUNS AT ALL. This pass is CAPABILITY-GATED at its one production caller,
// ShaderCompiler::SanitizeAndOptimizeBinary: a backend that can consume Float64 itself
// (DynamicBackendParameters::SupportsShaderFloat64, i.e. shaderFloat64 on DirectVulkan
// - lavapipe today and nothing else) skips it, and the module keeps its doubles.
// DirectGLES can never qualify, and neither can any real mobile device, so everything
// below still describes what happens there - which is everywhere that ships. The one
// exception that survives the capability: a VERTEX stage declaring a 64-bit float
// INPUT demotes the whole program regardless, because no backend here can FETCH 64
// bits (see ProgramSpirvTask::GenerateSpirv).
//
// BLOCK LAYOUT IS RE-DERIVED, NOT PRESERVED, and that was not the first choice - see
// BlockRelayout in the .cpp for the measurement that forced it. Preserving the 64-bit
// offsets (float + 4 bytes of padding in each slot) keeps the application's byte layout
@@ -50,12 +60,10 @@ namespace MobileGL {
// for the same reason - writes exactly where the demoted shader reads. Blocks with no
// 64-bit member anywhere are never touched.
//
// THE MEASURED COST, so the next wave does not re-diagnose it. Four GL 4.3 conformance
// cases fail on BOTH backends and on both an Adreno 830 and a Mali G925 - i.e. on every
// device, because no device has shaderFloat64 and the demotion therefore always runs:
// WHAT RE-DERIVING STILL COSTS, so the next wave does not re-diagnose it. Two GL 4.3
// conformance cases fail on BOTH backends and on every device, because no device has
// shaderFloat64 and the demotion therefore always runs:
//
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-cs
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-vs
// KHR-GL43.compute_shader.fp64-case1
// KHR-GL43.compute_shader.fp64-case3
//
@@ -63,44 +71,30 @@ namespace MobileGL {
// against it: it is blocked on GLSL subroutines ("FP64 support - subroutines"), which
// glslang deletes when targeting SPIR-V, and is out of scope by standing instruction.
//
// The other three fail in the two ways this comment predicts and in no other.
// stdLayout-case3 copies a block byte for byte: the output matches the input for
// bytes [0, 76) and is zero from there on, which is exactly the block's size once
// every double became a float and the layout repacked tightly. Re-derived byte-exactly
// in 2026-08: the block is `int data0; float data1[5]; mat3x2 data2; double data3;
// double data4[2]; int data5; dvec3 data6`, and demoting every double to float and
// repacking std430 gives data0@0, data1@4..23, data2@24..47, data3@48, data4@52..59,
// data5@60, data6@64..75 - 76 bytes. EVERY mismatching byte the QPA reports is >= 76
// and every expected-non-zero byte below 76 matched, on both the std140 output and the
// std430 one.
//
// ONE TRAP FOR THE NEXT READER, because it reads as evidence AGAINST demotion and is
// not: in the std430 output the doubles below the boundary appear to have round-tripped
// BIT-EXACTLY, which looks like fp64 surviving. It is an artifact. The shader reads and
// writes through the SAME demoted offset, so those four bytes are copied verbatim
// whatever they are interpreted as - the copy proves nothing about the width.
//
// fp64-case1 reports ceil(2.2) as 2: the uniform's double 2.0 is 0x4000000000000000,
// the demoted read takes its low 32 bits (0.0), ceil(0.0 + 0.2) = 1.0f = 0x3F800000
// lands in the low half of the 8-byte output slot and the whole thing prints as 2.
// Index 0 of the same case PASSES by accident, for the same reason - writing 0.0f into
// the low half of 1.0 leaves it unchanged - so a partial pass here is not progress.
// Fixing it means carrying a double in the DEFAULT UNIFORM block without re-deriving
// its layout - which is precisely what the capability gate now does where the backend
// allows it: fp64-case1 PASSES on DirectVulkan/lavapipe (measured) and still fails on
// Espryt and on every device without shaderFloat64, where this pass runs. There is no
// fix for the demoted path itself; the value simply does not fit.
// compute_shader.fp64-case2 passes in both regimes and any attempt has to keep it
// green.
//
// Both backends produce a CHARACTER-FOR-CHARACTER identical QPA byte list, which is
// the cheapest available proof that the defect is in this shared pass and in neither
// backend. A future wave that wants to re-open this should start by re-checking that
// identity rather than by re-deriving the layout.
//
// Fixing them means NOT demoting a double that lives in a buffer block, and carrying
// it as a uvec2 word pair instead - preserving the application's byte layout exactly,
// unpacking to fp32 for arithmetic and repacking on store. That is a large pass with
// the same dmat problem the paragraph above describes (a uvec2 representation cannot
// express a matrix stride either, so it would have to decline dmat types), and the
// default-uniform routing above reflects the demoted module, so a representation
// change there ripples into every glUniform*d. THREE actionable cases of 16085 (the
// fourth, fp64-case3, is subroutine-blocked and unreachable from here); deliberately
// not attempted, and re-confirmed as not worth attempting in the 2026-08 wave.
// compute_shader.fp64-case2 passes today and any attempt has to keep it green.
// SHADER STORAGE BLOCKS ARE NO LONGER IN THAT LIST, and the two cases that used to be
// (shader_storage_buffer_object.basic-stdLayout-case3-cs and -vs, which copy a block
// byte for byte and used to come back zero from the first double's slot onwards) pass
// on both backends. FlattenFloat64StorageBlockPass runs immediately before this one
// and takes every storage block holding a 64-bit float out of its hands, rewriting the
// block into a flat `uint` array whose index arithmetic carries the offsets glslang
// computed WITH the doubles in place. A flat array has no layout for SPIRV-Cross to
// re-derive, which is what makes it expressible where a padded struct is not, and an
// offset in an address computation has none of the dmat trouble the paragraph above
// describes. See that pass's header. Everything below still describes what happens to
// every OTHER block, and to the doubles in the function bodies of all of them.
//
// Declines (leaves the module byte-identical, so the caller's existing "this module
// still declares Float64" failure path reports it) when the module contains an
@@ -325,9 +325,31 @@ namespace MobileGL {
// appending it at the end of the section would make the module invalid. A
// duplicate OpTypeArray is legal (SPIR-V 2.8 exempts aggregates from the
// uniqueness rule, and so does spirv-val), so no search for an existing one is
// needed; the LENGTH CONSTANT is not exempt, and if the module already declares
// it after the block there is nowhere legal to put the array - the block is then
// declined and keeps today's behaviour. Returns 0 for that.
// needed; the LENGTH CONSTANT is not exempt, so when the module already declares
// it the pass has to work with the one instruction that exists.
//
// That instruction is not always in a usable place. GetDefiningInstruction only
// honours `position` when it MINTS the constant; when the module already has one
// it hands back the existing instruction wherever it happens to sit, and glslang
// emits constants in first-use order, so a shader whose first use of the value is
// below the counter block declares it below the block. The flattened array would
// then forward-reference its own length.
//
// KHR-GL43.compute_shader.pipeline-compute-chain is exactly that shader: two
// counters at offset 8 need a 4-element array, and its `%uint_4` is first used by
// a later declaration, so it lands AFTER gl_AtomicCounterBlock_1. Declining there
// - which is what this used to do - left the offsets in place, and SPIRV-Cross
// then refused the whole stage with "Push constant block cannot be expressed as
// neither std430 nor std140", so the chain's first kernel never reached the
// driver and every resource it writes stayed at its initial value.
//
// Moving the constant UP to just before the block is always legal, which is why
// this is a relocation and not a second declaration: an OpConstant's only operand
// is its result TYPE, and that type already precedes the block (it is the element
// type of the counter array the block declares). Every existing use sits after
// the constant's old position and therefore after its new one too, so no use is
// left dangling - moving a definition earlier in the types/constants section
// cannot invalidate anything. Ordering is all that changes; def-use is untouched.
uint32_t CreateCounterArrayTypeBefore(IRContext* context, Instruction* structType,
uint32_t uintTypeId, uint32_t length) {
auto* constantMgr = context->get_constant_mgr();
@@ -341,7 +363,12 @@ namespace MobileGL {
if (position == context->types_values_end()) return 0;
Instruction* lengthInst = constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
if (lengthInst == nullptr) return 0;
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) return 0;
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) {
// Pre-existing constant, declared below the block. Relocate it; see above
// for why that is sound. InsertBefore unlinks it from its current spot
// first, so this is a move rather than an aliasing second entry.
lengthInst->InsertBefore(structType);
}
const uint32_t arrayTypeId = context->TakeNextId();
if (arrayTypeId == 0) return 0;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.h
// Copyright (c) 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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Rewrites a SHADER STORAGE BLOCK that contains a 64-bit float into a flat
// `uint` word array, and turns every access to it into address arithmetic over
// that array. The application's byte layout survives exactly; the VALUES are
// still narrowed to 32-bit floats, because that is all the target has.
//
// Registered ONLY on the demoting path, immediately before DemoteFloat64Pass, and
// capability-gated with it (ShaderCompiler::SanitizeAndOptimizeBinary). Where the
// backend consumes 64-bit floats itself there is no narrowing for this to preserve a
// layout across, and flattening a block the driver would have laid out correctly by
// itself would only cost the shader its index arithmetic.
//
// WHY THIS EXISTS. DemoteFloat64Pass rewrites `double` to `float` in place and
// lets SPIRV-Cross re-derive the block's packing from the declared types, because
// GLSL ES has no member `layout(offset=)` and SPIRV-Cross refuses any block whose
// stated offsets it cannot express as std140 or std430. That re-derivation moves
// every member past the first double: the block a shader reads and writes stops
// being the block the application filled. Byte-for-byte, on the shape
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3 uses, the output
// matched the input up to the first double's slot and was zero from there on -
// the demoted block is simply shorter than the one that was bound.
//
// A flat `uint[]` has no layout to re-derive: one member, offset 0, ArrayStride 4,
// which IS std430, so SPIRV-Cross prints it unconditionally and the driver lays it
// out the only way it can. Every member's real byte offset - the std140 or std430
// one glslang computed WITH the doubles in place - then lives in the index
// arithmetic this pass emits, not in the declaration. The two ways the earlier
// attempt at this was blocked both disappear with it:
//
// * dmat: a `uvec2`-per-double representation cannot express a MatrixStride, so
// it would have had to decline matrices of doubles. Here a stride is a number
// in an address computation and nothing else, so dmat needs no special case.
// * the default-uniform block: its routing is built by reflecting the DEMOTED
// module (ProgramSpirvTask::BuildGlobalUboRouting), so changing how a double
// is carried there would ripple into every glUniform*d. This pass touches
// StorageBuffer blocks only and never that one.
//
// WHAT GL SEES IS UNCHANGED, and becomes CORRECT rather than merely unchanged:
// glGetProgramResourceiv answers from glslang's reflection of the pre-demotion
// module (ProgramInterface.cpp reads TObjectReflection::offset), i.e. the true
// fp64 offsets. Before this pass those offsets described a layout no shader used;
// now they describe the one it does.
//
// PRECISION, stated plainly. A double still becomes a float: the load narrows the
// stored binary64 to binary32 and the store widens it back, so a value that does
// not survive a round trip through 32 bits does not survive this either. The
// narrowing truncates the discarded mantissa bits rather than rounding to nearest,
// and flushes what binary32 can only hold as a subnormal to a signed zero; NaN
// stays NaN and an out-of-range magnitude becomes an infinity. That is the same
// fp32 promise DemoteFloat64Pass already makes - what changes is only that the
// BYTES around the value stay where the application put them.
//
// DECLINES, leaving the block exactly as it was for DemoteFloat64Pass to handle the
// old way, whenever it meets something it cannot rewrite exactly:
// - a block whose variable is used as anything but an access-chain base (loaded
// whole, handed to a function, asked its OpArrayLength);
// - an access chain that is not rooted at the variable, or whose result feeds
// anything but a plain OpLoad / OpStore (an atomic, OpCopyMemory, a further
// chain);
// - a non-constant index into a struct, a runtime array anywhere in the block, a
// RowMajor matrix (its columns are not contiguous, so a whole-column access is
// not one range), a member width other than 32 or 64 bits, or an offset or
// stride that is not a multiple of 4;
// - a load or store whose type decomposes into more scalars than the cap below,
// so legalizing a block can never explode the module.
//
// ORDERING: must run BEFORE DemoteFloat64Pass, which is what turns the doubles this
// pass leaves in the function body into floats - the OpFConvert pairs emitted here
// are width-preserving by then and collapse to their operands. It emits only 32-bit
// OpBitcasts, so it never trips that pass's "bitcast across the 64-bit boundary"
// decline.
class FlattenFloat64StorageBlockPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-flatten-float64-storage-block"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFlattenFloat64StorageBlockPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
File diff suppressed because it is too large Load Diff
@@ -54,12 +54,29 @@ namespace MobileGL {
// alone survives storage this shader never wrote (glTexStorage with no upload, whose
// surplus channels are undefined).
//
// The other NINE (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16, r16,
// rgba16_snorm, rg16_snorm, r16_snorm) have NO same-width core carrier and are
// deliberately NOT widened here: every carrier for them is either lossy or changes the
// numeric domain of the texture a `sampler2D` would read from it. They keep the honest
// r11f_g11f_b10f has no same-width core carrier either, and takes rgba16f anyway,
// because that carrier is still LOSSLESS: 11f is e5m6 and 10f is e5m5 against a half's
// s1e5m10 - the SAME 5-bit exponent with a strictly longer mantissa - so every value
// the packed format can hold has an exact half. Only the reverse direction differs
// (the carrier also holds negatives, which 11f and 10f cannot sign, and mantissa bits
// finer than the 6 and 5 they quantise to, so a value written through the image and
// then SAMPLED lands on half's grid rather than the packed format's). That is measured
// against the alternative, which is not a truer quantisation but no program at all:
// the SPIRV-Cross throw takes the whole stage, every image uniform declared beside it
// included.
//
// rgb10_a2ui takes rgba16ui for a simpler reason still: its channels are 10, 10, 10 and
// 2 bits of UNSIGNED INTEGER, and rgba16ui gives each of them sixteen. Same component
// type, same channel COUNT, every value representable - so no access is rewritten at
// all, and only the TRANSFER differs (its shadow is one packed 32-bit word per texel,
// which the upload splits into four shorts).
//
// The other SEVEN (rgb10_a2, rgba16, rg16, r16, rgba16_snorm, rg16_snorm, r16_snorm)
// are deliberately NOT widened here: core ESSL has no 16-bit normalized format at all
// and no 10-bit one, so every carrier for them either loses range or changes the
// component TYPE the texture a `sampler2D` would read presents. They keep the honest
// "no GLSL ES spelling" diagnostic instead of silently changing an application's
// quantisation behaviour.
// numeric domain.
//
// MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the
// ES texture behind the image has to be allocated in the carrier format too, and
@@ -119,6 +136,31 @@ namespace MobileGL {
// forty image formats. The count the widened accesses are masked back to.
static Uint ImageFormatChannelCount(Uint glInternalFormat);
// Whether the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value rather than as the values themselves - true for the seven
// 16-bit and 10-bit normalized formats and nothing else. `outChannelMax` takes the
// largest code each channel can hold (2^b - 1 unsigned, 2^(b-1) - 1 signed), which
// is the denominator of GL 4.6 2.3.5 for that channel; `outSignedNormalized` says
// which of the two conversions applies.
//
// DirectGLES asks this on both sides of the transfer: the upload pads a missing
// alpha with outChannelMax[3] rather than the transfer type's own 1 (through a
// uint carrier "one" is the saturated CODE, not the integer one), and
// glGetTexImage divides the codes back out, because the ES storage is an integer
// texture the client still expects to read as floats.
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
// The core-ESSL single-channel format a non-core BUFFER image is SPLIT into, or 0
// when the format needs no split or has no core single-channel base. A buffer
// image cannot be WIDENED - its texels are the application's buffer object, which
// has no room to restride - but rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the shader reads and writes each component
// by itself at 2i and 2i+1 instead. DirectGLES asks this for glTexBuffer's
// internal format and for glBindImageTexture's, which have to name the same view
// the shader addresses.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
bool onlyFormatsSpirvCrossRefusesToPrint = false);
@@ -442,6 +442,60 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
spvc_result SpvcSession::DropDefaultFragmentOutputColorIndex() {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &list, &count));
for (size_t i = 0; i < count; ++i) {
const spvc_reflected_resource& resource = list[i];
if (!spvc_compiler_has_decoration(compiler, resource.id, SpvDecorationIndex)) continue;
if (spvc_compiler_get_decoration(compiler, resource.id, SpvDecorationIndex) != 0u) continue;
spvc_compiler_unset_decoration(compiler, resource.id, SpvDecorationIndex);
}
SPVC_CHK_RETURN
}
spvc_result SpvcSession::RelaxReadWriteExclusiveStorageBuffers() {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
resources, SPVC_RESOURCE_TYPE_STORAGE_BUFFER, &list, &count));
for (size_t i = 0; i < count; ++i) {
const spvc_reflected_resource& resource = list[i];
// The variable itself, for a block the application qualified as a whole.
if (spvc_compiler_has_decoration(compiler, resource.id, SpvDecorationNonReadable) &&
spvc_compiler_has_decoration(compiler, resource.id, SpvDecorationNonWritable)) {
spvc_compiler_unset_decoration(compiler, resource.id, SpvDecorationNonReadable);
spvc_compiler_unset_decoration(compiler, resource.id, SpvDecorationNonWritable);
}
// ...and each member, which is where the qualifiers usually sit and where
// SPIRV-Cross reads them from before hoisting the ones every member shares.
const spvc_type blockType = spvc_compiler_get_type_handle(compiler, resource.base_type_id);
if (blockType == nullptr) continue;
const unsigned memberCount = spvc_type_get_num_member_types(blockType);
for (unsigned member = 0; member < memberCount; ++member) {
if (!spvc_compiler_has_member_decoration(compiler, resource.base_type_id, member,
SpvDecorationNonReadable) ||
!spvc_compiler_has_member_decoration(compiler, resource.base_type_id, member,
SpvDecorationNonWritable)) {
continue;
}
spvc_compiler_unset_member_decoration(compiler, resource.base_type_id, member,
SpvDecorationNonReadable);
spvc_compiler_unset_member_decoration(compiler, resource.base_type_id, member,
SpvDecorationNonWritable);
}
}
SPVC_CHK_RETURN
}
spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
@@ -120,6 +120,46 @@ namespace MobileGL {
// `outGlBindings` is appended to, so one vector can collect a whole program's
// stages; it may repeat a binding declared by several of them.
spvc_result SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings);
// Drops the Index decoration from every fragment output that carries the DEFAULT
// colour index 0, so the emitted ESSL does not print `index = 0`.
//
// Index 0 is what every single-source fragment output already is, in GL and in
// ESSL alike, and SPIR-V carries the decoration only because the application
// spelled the qualifier out - `layout(location = 0, index = 0) out vec4 c;` is
// legal desktop GLSL and says nothing. Printing it back into ESSL is NOT
// harmless: GLSL ES has no `index` layout qualifier in core, so the driver
// answers "index layout qualifier requires EXT_blend_func_extended" and refuses
// the stage. The program then links nothing and every draw with it renders
// NOTHING - verified on Mesa 26.1.4 llvmpipe with no MobileGL in the process,
// and it is why KHR-GL43.shader_atomic_counters.basic-program-query read back a
// black render target.
//
// A NON-zero index is left exactly as it is: that one really does select the
// second dual-source input and cannot be expressed without the extension, so it
// must keep reaching the driver (the frontend's own glBindFragDataLocationIndexed
// path already emits only non-zero indices for the same reason).
spvc_result DropDefaultFragmentOutputColorIndex();
// Drops `readonly` and `writeonly` from every shader storage block - and every
// block member - that carries BOTH of them.
//
// GL 4.6 core 4.10 lets a buffer variable be declared readonly AND writeonly at
// once: it then cannot be read or written at all, and the only thing left that
// it can be used for is `.length()`. The pair is therefore inert by
// construction - the frontend has already rejected any access to it - so
// dropping it cannot change what the shader does.
//
// Emitting it does change whether the shader EXISTS. SPIRV-Cross hoists the
// qualifiers every member shares onto the block, and Mesa's ES compiler rejects
// that spelling outright ("Interface block sets both readonly and writeonly",
// verified on Mesa 26.1.4 llvmpipe with no MobileGL in the process, against the
// exact source this transpiler emitted). The stage then never compiles, the
// program links without it, and every dispatch or draw is a silent no-op -
// which is how KHR-GL43.shader_storage_buffer_object.basic-readonly-writeonly
// read back 0 instead of the array length.
//
// 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();
spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;
@@ -30,7 +30,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// key (that map is an output of mapIO, not an input to it), and L1c's PAYLOAD gained
// the explicit uniform locations - so a blob written under 3 describes a differently
// shaped answer at both levels even where the bytes would have matched.
constexpr Uint32 kKeyLayoutVersion = 4u;
// 5: L1 gained nativeFloat64. SanitizeAndOptimizeBinary's fp64 tail is now capability-
// gated, so one L1 key shape can describe two materially different module sets (real
// doubles vs demoted-and-flattened) and a blob written under 4 says nothing about
// which one it holds.
constexpr Uint32 kKeyLayoutVersion = 5u;
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
@@ -123,6 +127,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(inputs.frontendFingerprint);
builder.Value(inputs.shaderCompileFlags);
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
builder.Value(static_cast<Uint8>(inputs.nativeFloat64));
builder.Value(static_cast<Uint64>(inputs.stages.size()));
for (const auto& stage : inputs.stages) {
builder.Value(static_cast<Uint32>(stage.type));
@@ -169,7 +174,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
TranslationKeyBuilder builder;
AppendCommonKeyPrefix(builder, kEsslKeyTag);
builder.Value(static_cast<Uint32>(inputs.shaderType));
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
builder.Value(static_cast<Uint8>(inputs.viewportIndexLoweringArmed));
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
builder.Value(inputs.maxColorTextureSamples);
@@ -341,22 +341,26 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
//
// The cached artifact is the module AFTER SanitizeAndOptimizeBinary, not the
// raw GlslangToSpv output. That is a deliberate choice and it is safe:
// SanitizeAndOptimizeBinary is a fixed 11-pass spirv-opt chain with no
// arguments but the module, and its two remaining parameters (`validateOutput`,
// `enableSpirvValidation`) only decide whether the OUTPUT is handed to the
// validator and logged - RunOptimizerChecked runs the optimizer first and
// identically either way. Nothing between GlslangToSpv and Sanitize reads
// backend state. So caching after Sanitize saves the 96 us/stage the chain
// costs on top of the 40 us GlslangToSpv, and gives the backends exactly the
// bytes they would have got.
// SanitizeAndOptimizeBinary is a fixed spirv-opt chain whose only
// output-changing argument is `nativeFloat64` (below), and whose two other
// parameters (`validateOutput`, `enableSpirvValidation`) only decide whether
// the OUTPUT is handed to the validator and logged - RunOptimizerChecked runs
// the optimizer first and identically either way. Nothing between GlslangToSpv
// and Sanitize reads backend state. So caching after Sanitize saves the 96
// us/stage the chain costs on top of the 40 us GlslangToSpv, and gives the
// backends exactly the bytes they would have got.
//
// L1 IS BACKEND-AGNOSTIC BY CONTRACT. Two contexts on different GPUs compiling
// the same GLSL share one L1 entry: nothing that merely steers a BACKEND
// transpile (backend identity, GLES/Vulkan capability bits, driver extension
// strings, GPU vendor) is allowed in this key - all of that lives in L2's key,
// where it belongs. What IS here is the subset of the environment that changes
// what glslang itself produces; see CompileEnv::frontendFingerprint for the
// field-by-field classification and the evidence behind each call.
// L1 IS BACKEND-AGNOSTIC BY CONTRACT, WITH EXACTLY ONE DECLARED EXCEPTION.
// Two contexts on different GPUs compiling the same GLSL share one L1 entry:
// nothing that merely steers a BACKEND transpile (backend identity, GLES/Vulkan
// capability bits, driver extension strings, GPU vendor) is allowed in this key
// - all of that lives in L2's key, where it belongs. What IS here is the subset
// of the environment that changes what glslang itself produces (see
// CompileEnv::frontendFingerprint for the field-by-field classification), PLUS
// `nativeFloat64`, the one capability bit that reaches INSIDE
// SanitizeAndOptimizeBinary and therefore changes the cached bytes themselves.
// A capability bit belongs in this key if and only if it does that; anything
// that only changes what a backend does with the finished module still does not.
//
// WHAT IS IN THE KEY (each one is an input that can change the modules):
// * CompileEnv::frontendFingerprint - the glslang resource limits
@@ -376,7 +380,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the ShaderCompileBits the parse ran under (always 0 in production; in
// the key so a future non-zero value cannot alias);
// * the SPIR-V validation switch (byte-identical output either way, but it
// costs one byte to be sure).
// costs one byte to be sure);
// * nativeFloat64 - CompileEnv::ConsumesFloat64Natively(). The fp64 tail of
// SanitizeAndOptimizeBinary (FlattenFloat64StorageBlockPass +
// DemoteFloat64Pass) is skipped when the backend can build a pipeline from
// a module that still declares OpCapability Float64, so the SAME GLSL
// produces MATERIALLY DIFFERENT modules under the two answers - one with
// real doubles, one narrowed to 32 bits with its storage blocks flattened.
// Not folded into frontendFingerprint on purpose: glslang produces the same
// thing either way, so it is not a front-end input, and L1c shares that
// fingerprint and would take a false miss per backend for nothing.
//
// The key is a PROGRAM-level key, not a per-stage one, and that is forced:
// glslang's mapIO resolves a fragment stage's input Locations against the
@@ -398,6 +411,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
Uint32 shaderCompileFlags = 0;
Bool enableSpirvValidation = false;
// CompileEnv::ConsumesFloat64Natively() - the fp64 tail of the sanitize chain. The
// one backend capability bit in this key; see the note above for why it has to be.
Bool nativeFloat64 = false;
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
// When the payload was SPIR-V alone these were provably irrelevant: transform
// feedback is resolved by READING the linked intermediates and never writes an XFB
@@ -556,7 +572,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the GL stage enum - three passes are stage-gated (draw parameters and
// array vertex inputs on vertex, fragment-output index legalization on
// fragment);
// * SupportsViewportArray - arms LowerViewportIndexForEssl;
// * the viewport-index lowering arming bit - GL_OES_viewport_array's absence OR the
// routing emulation being on - which arms LowerViewportIndexForEssl;
// * the four sample ceilings (color / integer / depth / advertised) - both
// ARM ClampMultisampleFetchesForEssl and PARAMETERIZE it;
// * SupportsNoperspectiveInterpolation - arms EmulateNoPerspectiveForEssl;
@@ -583,7 +600,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// armed by nothing, so the SPIR-V already in this key covers them completely.
//
// THE TEST FOR THAT CLAIM IS NOT THE SIGNATURE. LowerViewportIndexForEssl is equally
// module-only to look at, yet SupportsViewportArray is in this key because that bit ARMS
// module-only to look at, yet the arming bit is in this key because it ARMS
// it at the call site. So a new pass needs BOTH checks - what it takes, and what decides
// whether it runs - before "no key material" is a conclusion rather than an assumption.
// Note also where an application-authored value can hide: the atomic-counter
@@ -612,7 +629,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
GLenum shaderType = 0;
// --- driver capability bits that arm or steer a pass ---
Bool supportsViewportArray = false;
// Whether LowerViewportIndexForEssl runs on this module. NOT the raw
// GL_OES_viewport_array bit any more: the routing emulation arms the pass even where the
// extension exists (Config.h, ViewportArrayEmulation), so the extension alone no longer
// decides, and a key carrying only it would serve a module lowered under one setting to a
// link made under the other.
Bool viewportIndexLoweringArmed = false;
Bool supportsNoperspectiveInterpolation = false;
// GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage
// image whose format GLSL ES core cannot spell in the core format that carries it and
@@ -239,13 +239,34 @@ public final class PostActivity extends Activity {
nativeLoaded = true;
}
/**
* Writes the whole report to logcat. The report is the only machine-readable form of the
* POST, and logcat drops everything past roughly 4000 bytes of a single entry - which is
* less than one backend section, so a one-call log silently truncated the report to about
* the first dozen rows. Each chunk is prefixed with its index so a reader can reassemble
* them in order (concatenate the payloads after the "] " separator).
*/
private static void logReport(String json) {
if (json == null) {
Log.i(TAG, "<null report>");
return;
}
final int chunkSize = 3000;
final int chunks = (json.length() + chunkSize - 1) / chunkSize;
for (int index = 0; index < chunks; ++index) {
final int start = index * chunkSize;
final int end = Math.min(start + chunkSize, json.length());
Log.i(TAG, "[" + (index + 1) + "/" + chunks + "] " + json.substring(start, end));
}
}
private static void runDriverPost() {
String json = null;
Throwable failure = null;
try {
ensureNativeLoaded();
json = nativeRunDriverPost();
Log.i(TAG, json == null ? "<null report>" : json);
logReport(json);
} catch (Throwable error) {
Log.e(TAG, "Driver POST failed", error);
failure = error;
@@ -385,9 +406,54 @@ public final class PostActivity extends Activity {
}
}
renderKnownDriverBugs(backend.optJSONArray("knownDriverBugs"));
renderFormatCapabilities(backend.optJSONObject("formatCapabilities"));
}
/**
* The "Known Driver Bugs" section: core functionality this driver advertises, accepts,
* and then does not perform. Separate from the capability checks above because it answers
* a different question and uses its own vocabulary.
*
* Only bugs the device actually HAS are reported, so a clean driver renders no section at
* all rather than a list of reassurances - which is why the verdicts are FIXED (a MobileGL
* quirk makes application behaviour correct anyway) and UNFIXABLE (no substitute; the
* one-liner says what MobileGL does defensively), never PASS/FAIL.
*/
private void renderKnownDriverBugs(JSONArray bugs) {
if (bugs == null || bugs.length() == 0) {
return;
}
addText("Known driver bugs", 14, COLOR_TEXT, true, dp(16));
LinearLayout table = new LinearLayout(this);
table.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams tableParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
tableParams.topMargin = dp(6);
contentLayout.addView(table, tableParams);
int rowIndex = 0;
for (int i = 0; i < bugs.length(); ++i) {
JSONObject bug = bugs.optJSONObject(i);
if (bug == null) {
continue;
}
// addCheckRow renders name + chip + collapsible detail, which is exactly this
// section's shape; the chip text is the verdict rather than a status.
JSONObject row = new JSONObject();
try {
row.put("name", bug.optString("name", "unnamed bug"));
row.put("status", bug.optString("verdict", "UNFIXABLE"));
row.put("detail", bug.optString("detail", ""));
} catch (JSONException ignored) {
continue;
}
addCheckRow(table, row, rowIndex++);
}
}
/** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */
private static String backendTypeForSection(String sectionName) {
switch (sectionName.toLowerCase(Locale.ROOT)) {
@@ -731,6 +797,13 @@ public final class PostActivity extends Activity {
return COLOR_FAIL;
case "INFO":
return COLOR_INFO;
// The "Known driver bugs" section's own vocabulary. Every row there is a defect
// this device HAS, so neither verdict is reassuring: FIXED means MobileGL papers
// over it and applications still behave correctly, UNFIXABLE means they do not.
case "FIXED":
return COLOR_WARN;
case "UNFIXABLE":
return COLOR_FAIL;
default:
return COLOR_TEXT;
}