mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
666f150202 | ||
|
|
2f62970dd5 | ||
|
|
dcb568d445 | ||
|
|
a5f36c8f8d | ||
|
|
1ebe9d11c5 | ||
|
|
a8bb63950d | ||
|
|
7d68a17774 | ||
|
|
415645ccdd | ||
|
|
e6d03eb2a1 | ||
|
|
5e29e7d266 | ||
|
|
7eac33d17b | ||
|
|
8f19ce6fa7 | ||
|
|
d0f7fb99db | ||
|
|
d4247db6c3 | ||
|
|
e4f41e0fd3 | ||
|
|
9cf340cbef | ||
|
|
348a30a816 | ||
|
|
b5e0ada97e | ||
|
|
cb27ac7761 | ||
|
|
38c56a3d38 | ||
|
|
908172ba0f | ||
|
|
e18bac8cb2 | ||
|
|
7b0f443d3a | ||
|
|
f1b4a5e07f | ||
|
|
f3cd4091bf | ||
|
|
529d26f38f | ||
|
|
9bd125aeec | ||
|
|
ece9491d4b | ||
|
|
51b4abd801 | ||
|
|
cbb616093b | ||
|
|
e5846569ca | ||
|
|
194c2f189b | ||
|
|
7de7cfc6eb | ||
|
|
03e69fc9ef | ||
|
|
ee74c8ea3a | ||
|
|
54bbe805e5 | ||
|
|
6e2a3b3496 | ||
|
|
f5a0779385 | ||
|
|
86fdc68efa | ||
|
|
1185265e22 | ||
|
|
77c05b151a | ||
|
|
fdbe0b3117 | ||
|
|
9c9739e1c3 | ||
|
|
87548ae78a | ||
|
|
2a902ff58c | ||
|
|
a79eadd724 | ||
|
|
518e9c7796 | ||
|
|
a4c11f2603 | ||
|
|
37a656dedc | ||
|
|
3cc6b88767 | ||
|
|
b32c35a113 | ||
|
|
8587b83be3 | ||
|
|
0cef345d61 | ||
|
|
31367de628 | ||
|
|
bc4b62026a | ||
|
|
50da7de737 | ||
|
|
d9def5c1bb | ||
|
|
21a4c8aa95 | ||
|
|
6317066add | ||
|
|
02b59bef80 | ||
|
|
668f3e90c9 | ||
|
|
07d6277f87 | ||
|
|
b164692387 | ||
|
|
80ea44573e | ||
|
|
2a7d6f2e16 | ||
|
|
3a12f6d4f3 | ||
|
|
36b9d26b9d |
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 6f12598784...fa562bb911
+5
-1
@@ -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
|
||||
@@ -293,7 +294,9 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
@@ -301,12 +304,13 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -1463,8 +1463,66 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
|
||||
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
|
||||
const GLint layer = layerable ? imageBinding.Layer : 0;
|
||||
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
||||
layered, layer, imageBinding.Access, imageBinding.Format);
|
||||
// The bind half of the image-format widening. SyncTextureObjectToBackend has just
|
||||
// allocated this texture's storage in the core carrier of its format (the call above
|
||||
// is the one that marks it image-bindable), and glBindImageTexture's `format` has to
|
||||
// name the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on
|
||||
// Adreno for nineteen of the twenty-six non-core formats and on both Malis for
|
||||
// twenty-five, and every driver that DOES accept a narrow texture through a wide
|
||||
// image accepts it silently, reading and writing out of bounds. The frontend's own
|
||||
// ImageTextureBinding keeps the application's format untouched, so
|
||||
// GL_IMAGE_BINDING_FORMAT still answers what was passed in.
|
||||
//
|
||||
// Widened from the format the APPLICATION named rather than from the texture's own,
|
||||
// because GL lets the two differ inside one format class and the shader was widened
|
||||
// from the class the application named too (an r32ui view of an r32f image is a legal
|
||||
// reinterpretation). The two carriers always have the same texel size - every format
|
||||
// in a class widens to the four-channel form of that same class - so the storage
|
||||
// still describes what the bind claims. Gated on the TEXTURE having been widened, so
|
||||
// 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;
|
||||
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, bindTextureId, imageBinding.Level,
|
||||
layered, layer, imageBinding.Access, bindFormat);
|
||||
}
|
||||
|
||||
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
||||
@@ -2366,7 +2424,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// different one makes what was built wrong. Asked of the twin because only it
|
||||
// knows which units its own images address - and answered by an empty-vector
|
||||
// test for every program that declares its formats, which is nearly all of them.
|
||||
!twin->ImageUnitFormatsStillMatch()) {
|
||||
!twin->ImageUnitFormatsStillMatch() ||
|
||||
// A fifth of the same shape, for the programs ES will not link at all: one whose
|
||||
// tessellation evaluation stage has no control stage gets a synthesized
|
||||
// pass-through one, and GL_PATCH_VERTICES is compiled INTO it as
|
||||
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
|
||||
// built program wrong. -1 is "this program needed no such stage", which compares
|
||||
// equal to itself and costs every other program one integer test.
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
@@ -3224,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;
|
||||
}
|
||||
@@ -3281,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) {
|
||||
@@ -3294,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);
|
||||
@@ -3335,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) {
|
||||
@@ -3346,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);
|
||||
@@ -3574,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) {
|
||||
@@ -3590,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) {
|
||||
@@ -3601,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);
|
||||
}
|
||||
|
||||
@@ -3627,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);
|
||||
}
|
||||
@@ -3864,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
|
||||
@@ -3894,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);
|
||||
}
|
||||
@@ -3909,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);
|
||||
}
|
||||
|
||||
@@ -3919,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) {
|
||||
@@ -3963,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) {
|
||||
@@ -7430,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;
|
||||
@@ -7455,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;
|
||||
@@ -7475,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)};
|
||||
@@ -7537,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).
|
||||
@@ -8184,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
|
||||
@@ -8293,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;
|
||||
}
|
||||
@@ -8316,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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -1189,7 +1327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendProgramObjectImpl();
|
||||
~BackendProgramObjectImpl();
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
void Use() const;
|
||||
void Use();
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
@@ -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
|
||||
@@ -1222,6 +1368,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// counter sync at one empty-vector test.
|
||||
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
||||
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
||||
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
|
||||
// for, or -1 when this program needed no such stage. Another of the same shape as the
|
||||
// signatures above: the value is compiled INTO the synthesized stage as
|
||||
// `layout(vertices = N) out`, so a program built for one patch size is stale for
|
||||
// another and the draw path has to say so. -1 compares equal to itself for every
|
||||
// program that has a control stage of its own, i.e. for all but a handful.
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1270,6 +1425,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
||||
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
||||
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
|
||||
// stage has been attached and before the link; see the definition for why it cannot
|
||||
// regress a program that works today.
|
||||
void AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl);
|
||||
|
||||
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||
@@ -1297,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;
|
||||
@@ -1307,8 +1473,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||
Vector<Int> m_atomicCounterGlBindings;
|
||||
Int m_atomicCounterEsslBindingTop = -1;
|
||||
// -1 for every program that has a tessellation control stage of its own (or none at
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||
// next Use(). Use() dedupes on a GL program NAME, and a relink replaces the
|
||||
// executable behind that name without changing it - see the note at the
|
||||
// glLinkProgram in SyncToBackend for what the driver runs otherwise.
|
||||
Bool m_rebindAfterRelink = false;
|
||||
|
||||
Int m_globalUboBackendBlockIndex = -1;
|
||||
Int m_globalUboBackendBlockSize = 0;
|
||||
@@ -1398,6 +1573,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||
Bool needsExtendedImageFormats = false;
|
||||
// Some DECLARED format in play is one WidenImageFormatsForEssl will re-declare in a
|
||||
// core carrier. Answered from the uniform reflection rather than from a module parse
|
||||
// on purpose: the widening is armed on every driver, so a per-stage BuildModule to
|
||||
// find out would land on every stage of every program - which is the cost
|
||||
// SpirvGateFeatures exists to avoid. Program-wide, so it can over-arm a stage that
|
||||
// declares no image; the pass then finds nothing, reports no change, and the caller
|
||||
// keeps the module it already had.
|
||||
Bool declaresWidenableImageFormat = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
@@ -233,6 +234,105 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) {
|
||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const auto carrier = static_cast<GLenum>(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested));
|
||||
if (carrier == 0) {
|
||||
return {};
|
||||
}
|
||||
// EXACTLY the arming WidenImageFormatsForEssl uses, and it has to be: the shader, the
|
||||
// storage and the bind must all widen or none of them may, or the shader addresses a
|
||||
// texel size the storage does not have (which every driver tested accepts silently,
|
||||
// reading and writing out of bounds).
|
||||
//
|
||||
// A driver WITH GL_NV_image_formats can spell the narrow format - but only for the
|
||||
// formats SPIRV-Cross will actually print. It throws for its is_desktop_only_format
|
||||
// set instead of emitting a token, and the throw loses the stage whatever the driver
|
||||
// would have accepted: on Mesa, which advertises the extension, `layout(r8ui)
|
||||
// uimage2D` still lost its whole program until the widening ran for it too.
|
||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
||||
return {};
|
||||
}
|
||||
ImageBindableStorageWidening widening;
|
||||
widening.InternalFormat = carrier;
|
||||
widening.SourceChannels =
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ImageFormatChannelCount(requested);
|
||||
switch (carrier) {
|
||||
case GL_RGBA32UI:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RGBA32I:
|
||||
case GL_RGBA16I:
|
||||
case GL_RGBA8I:
|
||||
widening.IntegerData = true;
|
||||
break;
|
||||
default:
|
||||
widening.IntegerData = false;
|
||||
break;
|
||||
}
|
||||
// The carrier is a core ES format in every case, so it needs no fallback options of
|
||||
// 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) {
|
||||
@@ -700,6 +800,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, const Bool input) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Deliberately a scan for the DECLARATION rather than a regex over the whole text:
|
||||
// "gl_PerVertex" also appears inside the block's own body in some emissions, and the
|
||||
// direction keyword has to be the one immediately preceding the name for the match to
|
||||
// mean what this needs it to mean.
|
||||
const auto isIdentifierChar = [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
||||
};
|
||||
const String keyword = input ? String("in") : String("out");
|
||||
SizeT pos = 0;
|
||||
while ((pos = essl.find("gl_PerVertex", pos)) != String::npos) {
|
||||
// Walk back over whitespace to the direction keyword.
|
||||
SizeT before = pos;
|
||||
while (before > 0 && std::isspace(static_cast<unsigned char>(essl[before - 1]))) --before;
|
||||
const Bool matches = before >= keyword.size() &&
|
||||
essl.compare(before - keyword.size(), keyword.size(), keyword) == 0 &&
|
||||
(before == keyword.size() ||
|
||||
!isIdentifierChar(essl[before - keyword.size() - 1]));
|
||||
if (!matches) {
|
||||
pos += 1;
|
||||
continue;
|
||||
}
|
||||
const SizeT open = essl.find('{', pos);
|
||||
if (open == String::npos) return std::nullopt;
|
||||
const SizeT close = essl.find('}', open);
|
||||
if (close == String::npos) return std::nullopt;
|
||||
return essl.substr(open + 1, close - open - 1);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Tessellation is core in ES 3.2 and reachable in 3.1 only through
|
||||
// GL_EXT_tessellation_shader. The caller has already established that the driver runs
|
||||
// the evaluation stage at all, so the only question here is which spelling to use.
|
||||
const Bool core = esslVersion >= 320;
|
||||
String source = "#version " + std::to_string(core ? 320u : 310u) + " es\n";
|
||||
if (!core) {
|
||||
source += "#extension GL_EXT_tessellation_shader : require\n";
|
||||
}
|
||||
source += "precision highp float;\n";
|
||||
source += "precision highp int;\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// Mirrored, never invented. An empty member list means the neighbouring stage did not
|
||||
// redeclare the block either, and the driver's own built-in declaration is then what
|
||||
// both sides agree on - redeclaring here would be the thing that broke the match.
|
||||
if (!inPerVertexMembers.empty()) {
|
||||
source += "in gl_PerVertex {" + inPerVertexMembers + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
}
|
||||
if (!outPerVertexMembers.empty()) {
|
||||
source += "out gl_PerVertex {" + outPerVertexMembers + "} gl_out[];\n";
|
||||
}
|
||||
source += "void main() {\n";
|
||||
// Only gl_Position is forwarded. That is the whole of what the pass-through owes the
|
||||
// evaluation stage: a program whose evaluation stage reads anything else per-vertex
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsImagePassIdentifierChar(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
@@ -811,6 +987,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
struct ImageUniformDecl {
|
||||
String name;
|
||||
String aliasName; // the repair-tagged name the rewritten declaration takes; empty
|
||||
// for a declaration this pass leaves alone
|
||||
String writeName; // the writeonly half's name, when split
|
||||
String layout; // raw contents of layout(...)
|
||||
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
||||
@@ -818,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
|
||||
@@ -855,11 +1039,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return out;
|
||||
}
|
||||
|
||||
// A name for the writeonly half that no identifier in the shader (and no other
|
||||
// half already minted) can collide with.
|
||||
String MakeImageWriteAliasName(const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
|
||||
// A name for a rewritten declaration that no identifier in the shader (and no other
|
||||
// alias already minted for this stage) can collide with.
|
||||
String MakeImageAliasName(const String& prefix, const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = prefix + name;
|
||||
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
||||
// that already starts with '_' would otherwise produce.
|
||||
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
||||
@@ -905,6 +1089,236 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
// The digits of an array extent or of an element subscript, or -1 for "not a plain
|
||||
// decimal literal".
|
||||
//
|
||||
// One trailing `u`/`U` is PART of the literal rather than grounds for rejection.
|
||||
// SPIRV-Cross prints an index in the type SPIR-V gave it, and
|
||||
// LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
||||
// index it replaced (ConstantLikeIndex reads that index's own type_id), so an image
|
||||
// array reached through anything unsigned - `for (uint i = 0u; i < 4u; ++i)`, or any
|
||||
// expression on gl_LocalInvocationIndex, which is uint by definition - arrives here
|
||||
// spelled `g_image[0u]`. Reading that as "not a literal" declined the array and left
|
||||
// it on one layout(binding = N), which hands its elements the consecutive units
|
||||
// N, N+1, ... - exactly the silently-wrong-units defect the split exists to remove.
|
||||
Int ParseNonNegativeIntLiteral(const String& text) {
|
||||
if (text.empty()) return -1;
|
||||
SizeT digitCount = text.size();
|
||||
if (text[digitCount - 1] == 'u' || text[digitCount - 1] == 'U') --digitCount;
|
||||
if (digitCount == 0) return -1;
|
||||
Int value = 0;
|
||||
for (SizeT i = 0; i < digitCount; ++i) {
|
||||
const char c = text[i];
|
||||
if (c < '0' || c > '9') return -1;
|
||||
value = value * 10 + (c - '0');
|
||||
if (value > 4096) return -1; // no image array is anywhere near this
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (outDeclined != nullptr) outDeclined->clear();
|
||||
if (plans.empty() || glslCode.find("image") == String::npos) return glslCode;
|
||||
|
||||
// Same declaration shape as the split pass reads, with the array extent captured.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]*)\s*\])?\s*;)");
|
||||
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
|
||||
|
||||
struct StageImageDecl {
|
||||
String name;
|
||||
String layout;
|
||||
String qualifiers;
|
||||
String type;
|
||||
Int elementCount = 1;
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
};
|
||||
// Every image declaration in the stage; the plans are program-wide and name arrays
|
||||
// this stage may not declare at all.
|
||||
Vector<StageImageDecl> decls;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
StageImageDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
decl.qualifiers = NormalizeDeclarationSpacing(match[2].str());
|
||||
decl.type = match[3].str();
|
||||
decl.name = match[4].str();
|
||||
decl.elementCount = match[5].matched ? ParseNonNegativeIntLiteral(match[5].str()) : 1;
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenNames;
|
||||
for (const ImageArrayUnitPlan& plan : plans) {
|
||||
const auto decline = [&](const char* why) {
|
||||
if (outDeclined != nullptr) outDeclined->push_back(plan.name + ": " + why);
|
||||
};
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
const StageImageDecl* decl = nullptr;
|
||||
for (const auto& candidate : decls) {
|
||||
if (candidate.name == plan.name) {
|
||||
decl = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (decl == nullptr) {
|
||||
// Absent from this stage entirely is the normal outcome - the reflection is
|
||||
// program-wide and this pass runs per stage. Named but not RECOGNIZED is not:
|
||||
// it means the declaration is spelled in some shape the regex above does not
|
||||
// read, and staying quiet about that is how the wrong units got shipped.
|
||||
if (ContainsIdentifier(glslCode, plan.name)) {
|
||||
decline("the stage names it but declares it in a shape this pass cannot read");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (decl->elementCount < 0 || static_cast<SizeT>(decl->elementCount) != plan.units.size()) {
|
||||
decline("the emitted array extent disagrees with the reflected element count");
|
||||
continue;
|
||||
}
|
||||
|
||||
Bool consecutive = true;
|
||||
Bool everyElementHasAUnit = true;
|
||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
||||
const Int unit = plan.units[element];
|
||||
if (unit < 0) {
|
||||
everyElementHasAUnit = false;
|
||||
break;
|
||||
}
|
||||
if (unit != plan.units[0] + static_cast<Int>(element)) consecutive = false;
|
||||
}
|
||||
if (!everyElementHasAUnit) {
|
||||
decline("an element has no image unit");
|
||||
continue;
|
||||
}
|
||||
// Already exactly what ESSL would do on its own. The caller filters these out;
|
||||
// repeating the test here keeps the pass correct on its own terms.
|
||||
if (consecutive) continue;
|
||||
|
||||
// Every use has to be `name[<literal>]`. The literal is what the split turns
|
||||
// into a name, and by the time this runs there is always one:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every
|
||||
// dynamic image-array subscript in the module, because ESSL forbids one
|
||||
// outright ("image arrays indexed with non-constant expressions are forbidden
|
||||
// in GLSL ES"). A subscript that is still an expression here is therefore a
|
||||
// stage that was never going to compile, and guessing which element it meant
|
||||
// would only change which unit it addressed wrongly.
|
||||
struct ElementUse {
|
||||
SizeT start; // the first character of the name
|
||||
SizeT length; // through the closing ']'
|
||||
SizeT element;
|
||||
};
|
||||
Vector<ElementUse> uses;
|
||||
const char* refusal = nullptr;
|
||||
for (SizeT pos = glslCode.find(plan.name); pos != String::npos;
|
||||
pos = glslCode.find(plan.name, pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue;
|
||||
const SizeT after = pos + plan.name.size();
|
||||
if (after < glslCode.size() && IsImagePassIdentifierChar(glslCode[after])) continue;
|
||||
if (pos >= decl->declStart && pos < decl->declStart + decl->declLength) {
|
||||
continue; // the declaration's own name
|
||||
}
|
||||
const SizeT open = glslCode.find_first_not_of(" \t\r\n", after);
|
||||
if (open == String::npos || glslCode[open] != '[') {
|
||||
refusal = "it is reached by something other than a subscript, so there is no "
|
||||
"element index to rewrite";
|
||||
break;
|
||||
}
|
||||
Int depth = 0;
|
||||
SizeT scan = open;
|
||||
for (; scan < glslCode.size(); ++scan) {
|
||||
if (glslCode[scan] == '[') {
|
||||
++depth;
|
||||
} else if (glslCode[scan] == ']' && --depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scan >= glslCode.size() || open + 1 >= scan) {
|
||||
refusal = "it is reached by something other than a subscript, so there is no "
|
||||
"element index to rewrite";
|
||||
break;
|
||||
}
|
||||
const Int element = ParseNonNegativeIntLiteral(
|
||||
NormalizeDeclarationSpacing(glslCode.substr(open + 1, scan - open - 1)));
|
||||
if (element < 0 || element >= decl->elementCount) {
|
||||
refusal = "its subscript is not a literal element index, so which unit the "
|
||||
"access reaches cannot be decided here";
|
||||
break;
|
||||
}
|
||||
uses.push_back({pos, scan + 1 - pos, static_cast<SizeT>(element)});
|
||||
}
|
||||
if (refusal != nullptr) {
|
||||
decline(refusal);
|
||||
continue;
|
||||
}
|
||||
|
||||
// One SCALAR declaration per element, each carrying its own binding. ESSL nails
|
||||
// an ARRAY's elements to consecutive units and offers no way to move them, so
|
||||
// the only spelling that reaches an arbitrary set of units is one declaration
|
||||
// per unit - and with every subscript a literal, every use has exactly one of
|
||||
// them to be rewritten to.
|
||||
//
|
||||
// It costs precisely the image uniforms the application declared, which is why
|
||||
// there is no budget test here: an array of four elements becomes four scalars
|
||||
// however far apart their units are.
|
||||
const SizeT elementCount = plan.units.size();
|
||||
Vector<String> elementNames;
|
||||
String replacement;
|
||||
for (SizeT element = 0; element < elementCount; ++element) {
|
||||
const String elementName =
|
||||
MakeImageAliasName(IMAGE_ARRAY_ELEMENT_PREFIX,
|
||||
plan.name + "_" + std::to_string(element), glslCode, takenNames);
|
||||
takenNames.push_back(elementName);
|
||||
elementNames.push_back(elementName);
|
||||
|
||||
String layout = decl->layout;
|
||||
const String bindingText = "binding = " + std::to_string(plan.units[element]);
|
||||
if (std::regex_search(layout, bindingValueRegex)) {
|
||||
layout = std::regex_replace(layout, bindingValueRegex, bindingText);
|
||||
} else {
|
||||
layout = bindingText + (layout.empty() ? String() : ", " + layout);
|
||||
}
|
||||
if (element != 0) replacement += '\n';
|
||||
replacement += "layout(" + layout + ") uniform ";
|
||||
if (!decl->qualifiers.empty()) {
|
||||
replacement += decl->qualifiers;
|
||||
replacement += ' ';
|
||||
}
|
||||
replacement += decl->type + " " + elementName + ";";
|
||||
}
|
||||
edits.push_back({decl->declStart, decl->declLength, Move(replacement)});
|
||||
|
||||
// `name[k]` -> the scalar declared for element k, subscript and all.
|
||||
for (const ElementUse& use : uses) {
|
||||
edits.push_back({use.start, use.length, elementNames[use.element]});
|
||||
}
|
||||
}
|
||||
if (edits.empty()) return glslCode;
|
||||
|
||||
// Back to front, so an earlier edit's offsets stay valid. No two edits overlap: each
|
||||
// one covers either a whole declaration or a whole `name[k]`, the declaration's own
|
||||
// name is skipped when the uses are collected, and one occurrence of a name yields at
|
||||
// most one edit.
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
||||
String result = glslCode;
|
||||
for (const ImageSourceEdit& edit : edits) {
|
||||
result.replace(edit.start, edit.length, edit.text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -929,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;
|
||||
@@ -941,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();
|
||||
@@ -954,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()) {
|
||||
@@ -968,13 +1390,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
|
||||
// Walk every `image*(` call and attribute its first argument to a declaration.
|
||||
struct StoreSite {
|
||||
// EVERY recognized use is recorded, not only the stores: a declaration this pass
|
||||
// renames has to take all of its uses with it, and the "every occurrence was one I
|
||||
// saw" check below is what makes the recorded set provably the complete set.
|
||||
struct ImageUseSite {
|
||||
SizeT declIndex;
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
SizeT callOpen; // the '(' of the call this argument belongs to
|
||||
Bool stores; // an imageStore, i.e. the use a split redirects to the write half
|
||||
};
|
||||
Vector<StoreSite> storeSites;
|
||||
Vector<ImageUseSite> useSites;
|
||||
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
||||
SizeT tokenEnd = pos;
|
||||
@@ -1019,12 +1445,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
switch (ClassifyImageBuiltin(builtin)) {
|
||||
case ImageBuiltinAccess::Load:
|
||||
decl.loaded = true;
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
||||
break;
|
||||
case ImageBuiltinAccess::Store:
|
||||
decl.stored = true;
|
||||
storeSites.push_back({declIndex, argStart, argEnd - argStart, openParen});
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, true});
|
||||
break;
|
||||
case ImageBuiltinAccess::None:
|
||||
// imageSize/imageSamples touch nothing, but they still NAME the variable, so
|
||||
// a rename has to reach them.
|
||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
||||
break;
|
||||
default:
|
||||
decl.unknownUse = true;
|
||||
@@ -1041,12 +1471,74 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenAliases;
|
||||
Vector<String> takenNames;
|
||||
for (auto& decl : decls) {
|
||||
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
||||
// EVERY declaration this pass rewrites is also RENAMED, under the prefix of the
|
||||
// repair it is about to receive - the qualifier below is a decision about ONE
|
||||
// STAGE's accesses, and GLSL requires a uniform declared in two stages to be
|
||||
// declared IDENTICALLY (GLSL 4.3 4.3.9 / GLSL ES 3.20 4.3.9). A shader that
|
||||
// stores to an image in the vertex stage and loads it in the fragment stage gets
|
||||
// `writeonly` on one and `readonly` on the other, and on Adreno the linker merges
|
||||
// the two same-named declarations and SILENTLY DISCARDS the vertex-stage stores:
|
||||
// no GL error, no link log, LINK_STATUS = 1, and the image still holding its
|
||||
// initial contents afterwards
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation, and any
|
||||
// shader pack that writes an image in one stage to read it in another).
|
||||
//
|
||||
// Keyed on the REPAIR and not on the stage, which is what makes the rename
|
||||
// exactly as wide as the problem. Two stages that use the image the same way
|
||||
// reach the same prefix and emit byte-identical declarations, so they keep ONE
|
||||
// shared uniform and there is nothing mismatched to merge; two that use it
|
||||
// differently reach different prefixes and cannot be merged at all. Tagging by
|
||||
// stage instead also broke the merge - but it broke it for the agreeing stages
|
||||
// too, turning one image uniform into one PER STAGE that names it, and Adreno
|
||||
// allocates image locations per distinct uniform: the five stages of
|
||||
// KHR-GL43.shading_language_420pack.binding_images_texture_type_* went from 6
|
||||
// image uniforms to 30 and the link failed outright with "Error: Image Image
|
||||
// location or component exceeds max allowed." on an Adreno 830, where Mali and
|
||||
// Mesa both accept the same text.
|
||||
//
|
||||
// Nothing downstream reads these names: the two passes that key on the GL uniform
|
||||
// name (RebindImageUniformsToFrontendUnits, BakeImageFormatQualifiers) both run
|
||||
// BEFORE this one, RemoveLayoutBinding recognises an image declaration by its TYPE
|
||||
// token, and CacheResourceLocations skips image uniforms outright because ES image
|
||||
// units come only from layout(binding=N). The declarations this pass LEAVES ALONE -
|
||||
// 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;
|
||||
decl.aliasName = MakeImageAliasName(aliasPrefix, decl.name, glslCode, takenNames);
|
||||
takenNames.push_back(decl.aliasName);
|
||||
if (decl.loaded && decl.stored) {
|
||||
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||
takenAliases.push_back(decl.writeName);
|
||||
// Minted from the ALREADY access-tagged name, so the write half of a split
|
||||
// can never collide with the single declaration another stage's repair mints
|
||||
// for the same image.
|
||||
decl.writeName =
|
||||
MakeImageAliasName(IMAGE_WRITE_ALIAS_PREFIX, decl.aliasName, glslCode, takenNames);
|
||||
takenNames.push_back(decl.writeName);
|
||||
decl.split = true;
|
||||
if (outSplitCount != nullptr) ++*outSplitCount;
|
||||
// Both halves carry `coherent`; see BuildImageDeclaration. The
|
||||
@@ -1054,24 +1546,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// there is no visibility to restore and no reason to pay for the cache
|
||||
// behaviour.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
|
||||
BuildImageDeclaration(decl, "readonly", decl.aliasName,
|
||||
/*forceCoherent=*/true) +
|
||||
"\n" +
|
||||
BuildImageDeclaration(decl, "writeonly", decl.writeName,
|
||||
/*forceCoherent=*/true)});
|
||||
} else if (decl.stored) {
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||
BuildImageDeclaration(decl, "writeonly", decl.aliasName)});
|
||||
} else {
|
||||
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
||||
// the qualifier that keeps every one of those legal.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name)});
|
||||
BuildImageDeclaration(decl, "readonly", decl.aliasName)});
|
||||
}
|
||||
}
|
||||
for (const StoreSite& site : storeSites) {
|
||||
for (const ImageUseSite& site : useSites) {
|
||||
const ImageUniformDecl& decl = decls[site.declIndex];
|
||||
if (!decl.split) continue;
|
||||
edits.push_back({site.start, site.length, decl.writeName});
|
||||
// Empty exactly when the declaration was poisoned above and left untouched; its
|
||||
// uses must keep naming the variable that is still called that.
|
||||
if (decl.aliasName.empty()) continue;
|
||||
edits.push_back(
|
||||
{site.start, site.length, decl.split && site.stores ? decl.writeName : decl.aliasName});
|
||||
if (!decl.split || !site.stores) continue;
|
||||
// ...and an explicit barrier behind it. `coherent` on both halves is what makes
|
||||
// the store VISIBLE to a load through the other variable, but it says nothing
|
||||
// about ORDER within one invocation - and the whole reason a declaration is split
|
||||
|
||||
@@ -60,6 +60,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
|
||||
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
||||
// GLSL ES cannot spell as an image is carried by one it can.
|
||||
//
|
||||
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
||||
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
||||
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
||||
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
||||
// Seventeen have a core format of the SAME per-channel width and component type,
|
||||
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
||||
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so the carrier's surplus channels
|
||||
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
||||
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
||||
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
||||
//
|
||||
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
||||
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
||||
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
||||
// diagnostic rather than a silent approximation), and on a driver that HAS
|
||||
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
||||
// happen behind it.
|
||||
//
|
||||
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
||||
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
||||
// attachment behaviour, so the image constraint wins. In practice that only bites
|
||||
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
||||
// signed-normalized instead of becoming the half float that fallback would have picked -
|
||||
// so an image-bound texture in one of those two formats is no longer attachable, and
|
||||
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
||||
// does not update. Accepted deliberately: before the widening, an image binding in either
|
||||
// format was refused outright by every driver tested and the stage that declared it never
|
||||
// compiled at all, so nothing that works today is being given up.
|
||||
//
|
||||
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
||||
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
||||
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
||||
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
||||
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
||||
// 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;
|
||||
GLenum Type = GL_UNKNOWN_MGL;
|
||||
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
||||
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
||||
Uint SourceChannels = 0;
|
||||
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
||||
// 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
|
||||
@@ -178,9 +287,113 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
||||
// image array into; the suffix is the array's own name and the element's index.
|
||||
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
||||
// One image ARRAY whose elements the application pointed at units that are not
|
||||
// consecutive-from-element-zero.
|
||||
struct ImageArrayUnitPlan {
|
||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||
Vector<Int> units; // the frontend image unit element k has to reach
|
||||
};
|
||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||
// wrong value and three were never written, with no GL error and no link log. The same
|
||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
||||
// INVALID_OPERATION.
|
||||
//
|
||||
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
||||
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
||||
// element k. One declaration carries one binding, so one declaration per unit is the
|
||||
// only spelling that reaches an arbitrary set of them.
|
||||
//
|
||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||
// widening the array to cover the whole span of units and routing each subscript through
|
||||
// a `const highp int` offset table - was written before that pass covered images, and
|
||||
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
||||
// The split also costs exactly the image uniforms the application declared, where the
|
||||
// widening cost the whole SPAN (seven for the four elements of
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
||||
// fail to fit in.
|
||||
//
|
||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||
// caller to report - when the emitted extent disagrees with the reflection, when the
|
||||
// array is reached by anything other than a subscript, or when a subscript is not a
|
||||
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
||||
//
|
||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
||||
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
||||
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
||||
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined = nullptr);
|
||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||
// `out` one.
|
||||
//
|
||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||
// which members, depends on what the application's shader touched; a synthesized stage
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||
// bound in its place, and every draw silently renders nothing.
|
||||
//
|
||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
||||
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
||||
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
||||
// two that use it differently arrive at different ones - which is exactly the property
|
||||
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
||||
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
||||
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
||||
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||
@@ -192,18 +405,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||
// up as a device compile failure - and then as a silently no-op draw.
|
||||
//
|
||||
// Restores a legal declaration:
|
||||
// * loaded only -> add `readonly`
|
||||
// * stored only -> add `writeonly`
|
||||
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
||||
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
||||
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `coherent readonly <name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, point
|
||||
// same type, `coherent readonly
|
||||
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
||||
// every imageStore at the second one, and follow each of
|
||||
// those stores with `memoryBarrierImage();`. Several image
|
||||
// variables may share an image unit as long as they have
|
||||
// the same type and format, which is exactly what the pair
|
||||
// is.
|
||||
//
|
||||
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
||||
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
||||
// declared in two stages to be declared identically - so a shader that stores an image from
|
||||
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
||||
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
||||
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
||||
// still reads back its initial contents
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
||||
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
||||
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
||||
//
|
||||
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
||||
// point: two stages that use an image the same way emit byte-identical declarations, so
|
||||
// letting them keep one shared name costs nothing and merging them is correct, while two
|
||||
// stages that use it differently land on different prefixes and cannot be merged at all.
|
||||
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
||||
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
||||
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
||||
// binding_images_texture_type_* declares three read+write images in each of its five
|
||||
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// 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
|
||||
// one when both are coherent, and the split is what makes a same-variable
|
||||
@@ -227,7 +482,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||
// already rewritten to and must run before those bindings are stripped - see the call
|
||||
// site in Managers.cpp.
|
||||
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
||||
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
||||
//
|
||||
// `outSplitCount`, when given, receives the number of declarations that were actually
|
||||
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool isMember = false;
|
||||
};
|
||||
|
||||
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders);
|
||||
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages);
|
||||
|
||||
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||
@@ -614,15 +614,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void ReflectStageInterface(ShaderStage targetStage,
|
||||
Bool reflectInputs,
|
||||
const Vector<SharedPtr<ShaderObject>>& shaders,
|
||||
const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
StageInterfaceSummary& outSummary,
|
||||
Uint programExternalIndex,
|
||||
const char* stageLabel) {
|
||||
outSummary.slotSignatures.fill(0);
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != targetStage) {
|
||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (stages[moduleIndex] != targetStage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -690,11 +690,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void ValidateRasterizationStageInterface(const Vector<SharedPtr<ShaderObject>>& shaders,
|
||||
void ValidateRasterizationStageInterface(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
ProgramFactory::VkProgramObject& entry,
|
||||
Uint programExternalIndex) {
|
||||
const ShaderStage producerStage = PickClipFixupStage(shaders);
|
||||
const ShaderStage producerStage = PickClipFixupStage(stages);
|
||||
entry.rasterizationProducerStage = producerStage;
|
||||
entry.producerOutputComponentCount = 0;
|
||||
entry.fragmentInputComponentCount = 0;
|
||||
@@ -703,8 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Bool hasFragmentStage = false;
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader && shader->GetShaderStage() == ShaderStage::Fragment) {
|
||||
for (const ShaderStage stage : stages) {
|
||||
if (stage == ShaderStage::Fragment) {
|
||||
hasFragmentStage = true;
|
||||
break;
|
||||
}
|
||||
@@ -715,9 +715,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
StageInterfaceSummary producerOutputs{};
|
||||
StageInterfaceSummary fragmentInputs{};
|
||||
ReflectStageInterface(producerStage, false, shaders, spirv, producerOutputs, programExternalIndex,
|
||||
ReflectStageInterface(producerStage, false, stages, spirv, producerOutputs, programExternalIndex,
|
||||
"producer");
|
||||
ReflectStageInterface(ShaderStage::Fragment, true, shaders, spirv, fragmentInputs, programExternalIndex,
|
||||
ReflectStageInterface(ShaderStage::Fragment, true, stages, spirv, fragmentInputs, programExternalIndex,
|
||||
"fragment");
|
||||
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
|
||||
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
|
||||
@@ -1719,14 +1719,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return success;
|
||||
}
|
||||
|
||||
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders) {
|
||||
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages) {
|
||||
Bool hasGeometry = false;
|
||||
Bool hasTessEval = false;
|
||||
Bool hasVertex = false;
|
||||
|
||||
for (const auto& shader : shaders) {
|
||||
if (!shader) continue;
|
||||
const auto stage = shader->GetShaderStage();
|
||||
for (const ShaderStage stage : stages) {
|
||||
hasGeometry |= (stage == ShaderStage::Geometry);
|
||||
hasTessEval |= (stage == ShaderStage::TessEval);
|
||||
hasVertex |= (stage == ShaderStage::Vertex);
|
||||
@@ -2094,7 +2092,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2.
|
||||
// This value becomes the storage image VIEW's format while the image itself was created
|
||||
// from the texture's internal format, so the two must name the same bit layout or the
|
||||
// shader reads the texel through a different component order than the host wrote it.
|
||||
// GL_RGB10_A2 with GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, G in 10-19, B in
|
||||
// 20-29 and A in 30-31, which is Vulkan's A2B10G10R10; A2R10G10B10 transposes R and B.
|
||||
// KHR-GL43.shader_image_load_store.basic-allFormats-store read back [2,1,0,3] for an
|
||||
// rgb10_a2ui image stored as [0,1,2,3] while these two converters disagreed.
|
||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||
@@ -2117,7 +2123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2B10G10R10_UINT_PACK32; // see Rgb10A2 above
|
||||
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||
@@ -2308,15 +2314,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ProgramFactory::ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeVertexInputLocationMask = 0;
|
||||
entry.vertexInputTypes.fill(0);
|
||||
entry.readsBaseVertexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (stages[moduleIndex] != ShaderStage::Vertex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2393,14 +2399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||
// pipeline, and a false positive costs only a wider viewportCount.
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.writesViewportIndexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex]) continue;
|
||||
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
const ShaderStage stage = stages[moduleIndex];
|
||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||
|
||||
const auto& module = spirv[moduleIndex];
|
||||
@@ -2428,15 +2433,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeFragmentOutputLocationMask = 0;
|
||||
entry.fragmentOutputTypes.fill(0);
|
||||
entry.fragmentReplacesDepth = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (stages[moduleIndex] != ShaderStage::Fragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3142,7 +3147,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& entry = m_cache[hash];
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrame = m_frameCounter;
|
||||
auto& shaders = program.GetAttachedShaders();
|
||||
// The EXECUTABLE's stage list, not GetAttachedShaders(): `spirv` is a link artifact with
|
||||
// one module per linked stage, while the attach list is live and grows on
|
||||
// glAttachShader, which GL 4.6 core 7.3 says does not reach the executable until the
|
||||
// next link. Sizing this loop by the attach list therefore ran it past the end of both
|
||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
@@ -3150,14 +3160,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(shaders);
|
||||
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
||||
|
||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||
// Both lists come from the same Link(), so they agree by construction; the min() is what
|
||||
// makes that an assumption this loop does not have to bet the process on.
|
||||
const SizeT moduleCount = std::min(stages.size(), spirv.size());
|
||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||
auto& spv = spirv[i];
|
||||
if (spv.empty()) continue;
|
||||
|
||||
// Apply position fixup if needed
|
||||
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
||||
if (fixupStage != ShaderStage::Unknown && stages[i] == fixupStage) {
|
||||
const Vector<Uint>* fixupInput = &spv;
|
||||
Vector<Uint> xfbSpirv;
|
||||
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
@@ -3173,16 +3186,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
|
||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && stages[i] == ShaderStage::Fragment) {
|
||||
Vector<Uint> explicitLodSpirv;
|
||||
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
||||
moduleSpirvs[i] = Move(explicitLodSpirv);
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] &&
|
||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
|
||||
Vector<Uint> fragCoordSpirv;
|
||||
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
||||
moduleSpirvs[i] = Move(fragCoordSpirv);
|
||||
@@ -3193,7 +3204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// operations execute natively; module repairs keep the GL contract intact
|
||||
// around them. The opt-in emulation path replaces them only on devices with no
|
||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
||||
if (stages[i] == ShaderStage::Compute) {
|
||||
// Program 203 broadcasts the first reduction through
|
||||
// prefixSumCache[0], then lets the second reduction overwrite that
|
||||
// scratch without first rendezvousing all readers. Patch that exact
|
||||
@@ -3296,8 +3307,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
||||
// the builtin but shaderDrawParameters is missing) rides along with
|
||||
// ReflectVertexInputs, which already reflects this stage.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||
m_shaderDrawParametersEnabled) {
|
||||
if (stages[i] == ShaderStage::Vertex && m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv, enableSpirvValidation)) {
|
||||
@@ -3314,8 +3324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||
// and for the baseVertex word of an indexed indirect command.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
if (stages[i] == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||
zeroedSpirv, enableSpirvValidation)) {
|
||||
@@ -3338,7 +3347,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||
// double input - garbage with no diagnostic anywhere.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
if (stages[i] == ShaderStage::Vertex) {
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||
@@ -3377,17 +3386,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
||||
|
||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3399,7 +3408,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
|
||||
|
||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
ShaderStage shaderStage = shaders[i]->GetShaderStage();
|
||||
ShaderStage shaderStage = stages[i];
|
||||
stage.stage = ToVkStage(shaderStage);
|
||||
stage.module = module;
|
||||
stage.pName = "main";
|
||||
@@ -3434,12 +3443,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Reflect and create layout as part of the program object
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
ValidateRasterizationStageInterface(stages, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
#endif
|
||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||
ReflectVertexInputs(stages, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(stages, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(stages, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(stages, moduleSpirvs, entry);
|
||||
ReflectLayout(program, moduleSpirvs, entry);
|
||||
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
||||
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
||||
@@ -3651,7 +3660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
||||
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.needsPassthroughTessControl = false;
|
||||
@@ -3660,9 +3669,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool hasTessEval = false;
|
||||
Bool hasTessControl = false;
|
||||
SizeT tessEvalModuleIndex = 0;
|
||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||
if (!shaders[i]) continue;
|
||||
const auto stage = shaders[i]->GetShaderStage();
|
||||
for (SizeT i = 0; i < stages.size(); ++i) {
|
||||
const ShaderStage stage = stages[i];
|
||||
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
||||
if (stage == ShaderStage::TessEval) {
|
||||
hasTessEval = true;
|
||||
|
||||
@@ -499,13 +499,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
||||
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
||||
// keeps the program's live attach list, which is a longer and differently-indexed list
|
||||
// the moment a glAttachShader lands after the link, from being passed here by mistake.
|
||||
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
@@ -513,7 +517,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
// records what the evaluation stage's input interface is made of.
|
||||
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -4765,10 +4767,11 @@ void main() {
|
||||
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
||||
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
|
||||
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
|
||||
for (const auto& shader : program.GetAttachedShaders()) {
|
||||
if (shader && shader->GetShaderStage() == ShaderStage::Geometry) return true;
|
||||
}
|
||||
return false;
|
||||
// Both halves are link-time properties, so both are asked of the LAST LINK. Reading the
|
||||
// live attach list would let a glAttachShader that has not been linked in yet - which GL
|
||||
// 4.6 core 7.3 says changes nothing about what the program runs - flip a property this
|
||||
// pipeline is cached under, for an executable with no geometry stage in it.
|
||||
return program.HasLinkedShaderStage(ShaderStage::Geometry);
|
||||
}
|
||||
|
||||
VkPipeline VulkanRenderer::GetOrCreatePipeline(
|
||||
|
||||
@@ -45,7 +45,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
// Of the EXECUTABLE, not the live attach list: attaching a compute shader to an
|
||||
// already-linked graphics program does not give that program a compute stage to
|
||||
// dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the
|
||||
// attach hands the backend a program whose SPIR-V has no compute module in it.
|
||||
if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
@@ -111,7 +115,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// A geometry stage writes what it emits, not what the draw assembled, and the
|
||||
// amplification factor lives in the shader. Record that this span contained such
|
||||
// a draw so the transform feedback queries keep their backend result for it.
|
||||
if (program->GetShaderIndexByStage(ShaderStage::Geometry) >= 0) {
|
||||
if (program->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
||||
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
|
||||
}
|
||||
// Capacity in captured vertices = the tightest bound buffer.
|
||||
@@ -208,8 +212,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The EVALUATION stage is what decides: a control stage cannot run without one, and a
|
||||
// program carrying only an evaluation stage still tessellates, through GL's
|
||||
// fixed-function pass-through control stage (11.2.2).
|
||||
const Bool tessellationActive =
|
||||
currentProgram && currentProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0;
|
||||
// Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a
|
||||
// tessellation evaluation shader to an already-linked program does not put it in the
|
||||
// executable, so reading the live list here would reject every non-GL_PATCHES draw
|
||||
// against a program that does not tessellate - and keep rejecting them, since a detach
|
||||
// is likewise deferred to the next link.
|
||||
const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
||||
if (tessellationActive && mode != GL_PATCHES) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -230,8 +238,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||
// converted, so it is not constrained here.
|
||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||
//
|
||||
// "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
|
||||
// primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
|
||||
// is indistinguishable from no geometry shader by its reflected input type alone. The
|
||||
// sentinel test this replaces therefore skipped the whole rule for exactly the geometry
|
||||
// shaders whose input is the most restrictive one - every mode but GL_POINTS was
|
||||
// accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
|
||||
// program with GL_LINES and requires INVALID_OPERATION).
|
||||
//
|
||||
// And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so
|
||||
// pairing it with the live attach list would re-point the very same 0-aliasing rather
|
||||
// than remove it. In the window after glAttachShader(GS) on a linked program the live
|
||||
// list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and
|
||||
// the switch below would silently reject every mode but GL_POINTS.
|
||||
const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry);
|
||||
const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||
if (geometryActive && mode != GL_PATCHES) {
|
||||
Bool compatible = false;
|
||||
switch (gsInput) {
|
||||
case GL_POINTS:
|
||||
@@ -271,9 +294,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// can only ever be GL_PATCHES. A paused span is exempt: it captures nothing,
|
||||
// so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3).
|
||||
const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
// Both stage tests are asked of the last link, for the same reason as the two guards
|
||||
// above: what relocates the constraint is a stage the program actually RUNS, and an
|
||||
// attach that has not been linked in yet gives it none.
|
||||
const Bool feedbackModeIsProgramDriven =
|
||||
feedbackProgram && (feedbackProgram->GetShaderIndexByStage(ShaderStage::Geometry) >= 0 ||
|
||||
feedbackProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0);
|
||||
feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
||||
feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval));
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
!MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
|
||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||
|
||||
@@ -680,7 +680,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
|
||||
*params = programObject->GetActiveUniformBlocksCount();
|
||||
// Uniform blocks only. GetActiveUniformBlocksCount() is the internal block space,
|
||||
// which also carries the storage blocks and the synthesized atomic counter blocks.
|
||||
*params = programObject->GetGlUniformBlockCount();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
|
||||
@@ -700,7 +702,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
// "a linked program object with a compute shader" is one whose EXECUTABLE has the
|
||||
// stage: the local size below is a link artifact, so an attached-but-not-yet-linked
|
||||
// compute shader would answer this query with the previous link's (absent) value
|
||||
// instead of the INVALID_OPERATION GL 4.6 core 7.13 asks for.
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Compute)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
@@ -874,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;
|
||||
@@ -886,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) {
|
||||
@@ -923,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__,
|
||||
@@ -933,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));
|
||||
@@ -977,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__,
|
||||
@@ -989,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
|
||||
@@ -1281,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) {
|
||||
@@ -1300,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());
|
||||
@@ -1311,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());
|
||||
@@ -1362,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]);
|
||||
@@ -1712,7 +1819,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return GL_INVALID_INDEX;
|
||||
}
|
||||
|
||||
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
|
||||
// GetGlUniformBlockIndex, not GetUniformBlockIndex: the latter answers in the internal
|
||||
// block space, which also resolves storage blocks and the synthesized atomic counter
|
||||
// blocks. Neither is a uniform block (GL 4.6 core 7.6), so both are GL_INVALID_INDEX here.
|
||||
const auto index = programObject->GetGlUniformBlockIndex(uniformBlockName);
|
||||
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
||||
return index;
|
||||
}
|
||||
@@ -1726,7 +1836,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Program object" + std::to_string(program) + " that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1737,8 +1847,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(program) + "."));
|
||||
return;
|
||||
}
|
||||
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
||||
// block-keyed tables still carry; translate before touching them.
|
||||
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
||||
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
||||
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
|
||||
programObject->SetUniformBlockBinding(blockIndex, uniformBlockBinding);
|
||||
}
|
||||
|
||||
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
|
||||
@@ -1750,7 +1863,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Program object" + std::to_string(program) + " that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1761,61 +1874,68 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(program) + "."));
|
||||
return;
|
||||
}
|
||||
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
||||
// block-keyed tables still carry; every accessor below is indexed by the block space.
|
||||
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
||||
switch (pname) {
|
||||
case GL_UNIFORM_BLOCK_DATA_SIZE: {
|
||||
*params = (GLint)programObject->GetUBOSizeAt(uniformBlockIndex);
|
||||
*params = (GLint)programObject->GetUBOSizeAt(blockIndex);
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
|
||||
break;
|
||||
}
|
||||
case GL_UNIFORM_BLOCK_NAME_LENGTH: {
|
||||
*params = (GLint)programObject->GetUniformBlockName(uniformBlockIndex).length() + 1;
|
||||
*params = (GLint)programObject->GetUniformBlockName(blockIndex).length() + 1;
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
|
||||
break;
|
||||
}
|
||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
|
||||
*params = programObject->GetUniformBlockActiveUniformCount(uniformBlockIndex);
|
||||
*params = programObject->GetUniformBlockActiveUniformCount(blockIndex);
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
|
||||
break;
|
||||
}
|
||||
case GL_UNIFORM_BLOCK_BINDING: {
|
||||
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(uniformBlockIndex));
|
||||
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(blockIndex));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
|
||||
break;
|
||||
}
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangVertex));
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangVertex));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||
*params =
|
||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessControl));
|
||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessControl));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||
*params =
|
||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessEvaluation));
|
||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessEvaluation));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangGeometry));
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangGeometry));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangFragment));
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangFragment));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangCompute));
|
||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangCompute));
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
||||
// Member entries of an arrayed block are recorded against the first instance;
|
||||
// every instance of the array reports that shared member set (matches
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
|
||||
//
|
||||
// Both sides of the comparison are BLOCK indices: GetUniformBlockMemberOwnerIndex
|
||||
// answers in that space, so the scan uses GetActiveUniformOwnerBlockIndex rather
|
||||
// than the GL_UNIFORM_BLOCK-space GetActiveUniformBlockIndex.
|
||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(blockIndex));
|
||||
GLint uniformIndexCount = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
|
||||
if (programObject->GetActiveUniformOwnerBlockIndex(uniformIndex) != ownerIndex) {
|
||||
continue;
|
||||
}
|
||||
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
||||
@@ -1845,7 +1965,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
" is not a program object that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1855,7 +1975,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"not the index of an active uniform block in program."));
|
||||
return;
|
||||
}
|
||||
const auto& name = programObject->GetUniformBlockName(uniformBlockIndex);
|
||||
const auto& name = programObject->GetUniformBlockName(
|
||||
static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex)));
|
||||
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
|
||||
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
|
||||
length ? *length : 0);
|
||||
|
||||
@@ -305,13 +305,17 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||
// with them (which is exactly what the CTS does).
|
||||
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
||||
const Int glBlockCount = program.GetGlUniformBlockCount();
|
||||
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||
// The block-space index the block-keyed accessors want; the two spaces differ
|
||||
// whenever the program also has a storage or atomic counter block, which
|
||||
// glslang files under the same reflection list (no EShReflectionSeparateBuffers).
|
||||
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
|
||||
Resource resource;
|
||||
resource.name = program.GetUniformBlockName(glIndex);
|
||||
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||
resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
|
||||
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
|
||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
|
||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
|
||||
if (tIndex >= 0 && tIndex < blockCount) {
|
||||
resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
|
||||
stagesFromMembers, tIndex);
|
||||
@@ -324,15 +328,25 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
const ProgramObject::LinkArtifacts& reflection, Model& model,
|
||||
const Vector<BlockKind>& blockKind,
|
||||
const Vector<Int>& blockInterfaceIndex) {
|
||||
const Uint uniformCount = program.GetUniformCount();
|
||||
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||
// Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
|
||||
// uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
|
||||
// active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
|
||||
// this is the only place that does. GL uniforms keep their GL index as their
|
||||
// GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
|
||||
// the GL-visible entries in this order preserves the correspondence.
|
||||
const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
||||
for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
|
||||
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
|
||||
const auto& type = refl.type;
|
||||
const Int owner = refl.index;
|
||||
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||
? blockKind[owner]
|
||||
: BlockKind::GlobalUbo;
|
||||
const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
|
||||
// Everything except a buffer variable is enumerated through the GL space, so a
|
||||
// uniform the relaxed parse swept out of it (a declared-but-dead default-block
|
||||
// one) stays out of GL_UNIFORM too.
|
||||
if (kind != BlockKind::Storage && glIndex < 0) continue;
|
||||
|
||||
Resource resource;
|
||||
resource.name = refl.name;
|
||||
@@ -365,11 +379,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||
resource.location = -1;
|
||||
} else {
|
||||
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
||||
resource.offset = program.GetActiveUniformOffset(glIndex);
|
||||
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
||||
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
||||
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
||||
const Uint glUniformIndex = static_cast<Uint>(glIndex);
|
||||
resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
|
||||
resource.offset = program.GetActiveUniformOffset(glUniformIndex);
|
||||
resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
|
||||
resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
|
||||
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
|
||||
// A member of a named uniform block has no location, whatever the
|
||||
// frontend's own location table says (it hands one out to every uniform
|
||||
// so glUniform* can address block members through the global UBO).
|
||||
@@ -388,12 +403,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
static_cast<GLuint>(i));
|
||||
}
|
||||
}
|
||||
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
||||
for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
|
||||
// Members of an arrayed block are reflected once, against instance [0].
|
||||
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
||||
// GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
|
||||
// Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
|
||||
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
|
||||
const Int owner = program.GlUniformBlockIndexFromBlock(
|
||||
static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
|
||||
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||
if (model.uniforms[i].blockIndex == owner) {
|
||||
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||
model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,15 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/ImageFormatQualifierScenario.cpp
|
||||
Scenarios/NonCoreImageFormatScenario.cpp
|
||||
Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||
Scenarios/TessellationDrawModeScenario.cpp
|
||||
Scenarios/GeometryDrawModeScenario.cpp
|
||||
Scenarios/PostLinkAttachScenario.cpp
|
||||
Scenarios/FormatlessImageBakeScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
@@ -100,6 +104,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FormatlessImageBakeScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A FORMAT-LESS IMAGE UNIFORM WHOSE UNIT HOLDS A NON-CORE FORMAT.
|
||||
//
|
||||
// GLSL 4.20 lets a write-only image uniform omit its layout format; GLSL ES demands one, so
|
||||
// DirectGLES BAKES the format of whatever glBindImageTexture put on the unit into the
|
||||
// declaration. When that format is outside the GLSL ES core thirteen, the bake alone is not
|
||||
// enough - the baked declaration then has to go through the same channel-widening
|
||||
// WidenImageFormatsForEssl gives a DECLARED non-core format (see NonCoreImageFormatScenario for
|
||||
// the widening itself).
|
||||
//
|
||||
// The two routes had different arming. The declared route armed the widening on the format
|
||||
// alone; the baked route armed it only when the driver lacked GL_NV_image_formats. That reads
|
||||
// like an optimisation and is not one: SPIRV-Cross throws for its is_desktop_only_format set the
|
||||
// moment it targets ESSL, whatever the driver would have accepted, so on a driver that HAS the
|
||||
// extension the shader half of the widening stayed switched off while TextureImpl's storage/bind
|
||||
// half - which keys on SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran. The
|
||||
// stage threw, the program linked without it, and every dispatch silently did nothing.
|
||||
//
|
||||
// KHR-GL43.stencil_texturing.functional is where it surfaced: its compute half writes through a
|
||||
// format-less `uimage2D` bound to an R8UI texture, and returned zeros for every texel.
|
||||
//
|
||||
// DISCRIMINATING ONLY WHERE THE DRIVER ADVERTISES GL_NV_image_formats - Mesa does, which is what
|
||||
// the software lanes run and where this was found. On Adreno 830 and both Malis the extension is
|
||||
// absent, the old code already armed the widening, and these cases pass before and after; they
|
||||
// are kept running there as a guard against the opposite mistake.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 8;
|
||||
|
||||
// No layout format on uni_image on purpose: that is the whole subject. uni_source is a
|
||||
// plain integer texture so nothing but the image declaration is in play.
|
||||
const char* const kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
uniform usampler2D uni_source;
|
||||
void main()
|
||||
{
|
||||
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(uni_image, at, uvec4(texelFetch(uni_source, at, 0).r, 0u, 0u, 0u));
|
||||
}
|
||||
)";
|
||||
|
||||
class FormatlessImageBakeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsCompute()) {
|
||||
GTEST_SKIP() << "no compute stage on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsCompute() {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
DrainErrors();
|
||||
return maxImageUnits >= 2;
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint BuildCompute(const char* source, std::string& log) {
|
||||
const GLuint cs = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(cs, 1, &source, nullptr);
|
||||
glCompileShader(cs);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(cs, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char buffer[2048] = "";
|
||||
glGetShaderInfoLog(cs, sizeof(buffer), nullptr, buffer);
|
||||
log = buffer;
|
||||
glDeleteShader(cs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, cs);
|
||||
glLinkProgram(program);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||
glDeleteShader(cs);
|
||||
if (!ok) {
|
||||
char buffer[2048] = "";
|
||||
glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer);
|
||||
log = buffer;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// internalFormat is the NON-CORE image format under test; the destination texture and
|
||||
// the glBindImageTexture argument both use it, and the shader declares nothing.
|
||||
void RunCopy(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType) {
|
||||
std::vector<GLuint> expected(kExtent * kExtent);
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
expected[i] = static_cast<GLuint>(1 + i);
|
||||
}
|
||||
|
||||
// Source: a core-format integer texture holding 1..64.
|
||||
std::vector<GLubyte> sourceBytes(kExtent * kExtent);
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
sourceBytes[i] = static_cast<GLubyte>(expected[i]);
|
||||
}
|
||||
GLuint sourceTexture = 0;
|
||||
glGenTextures(1, &sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, kExtent, kExtent);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_BYTE,
|
||||
sourceBytes.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Destination: the format under test, zero-filled so "the dispatch did nothing"
|
||||
// and "the dispatch wrote zeros" are the same observation the CTS made.
|
||||
GLuint destTexture = 0;
|
||||
glGenTextures(1, &destTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
const std::vector<GLubyte> zeros(static_cast<std::size_t>(kExtent) * kExtent * 8, 0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, zeros.data());
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "destination storage";
|
||||
|
||||
std::string log;
|
||||
const GLuint program = BuildCompute(kComputeSource, log);
|
||||
ASSERT_NE(program, 0u) << "the format-less image program did not build: " << log;
|
||||
|
||||
glUseProgram(program);
|
||||
glBindImageTexture(1, destTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, internalFormat);
|
||||
glUniform1i(glGetUniformLocation(program, "uni_image"), 1);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "uni_source"), 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "binding";
|
||||
|
||||
glDispatchCompute(kExtent, kExtent, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "dispatch";
|
||||
|
||||
std::vector<GLuint> readback(kExtent * kExtent, 0xFFFFFFFFu);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, readback.data());
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "readback";
|
||||
|
||||
int offenders = 0;
|
||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||
if (readback[i] != expected[i]) ++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0) << "the dispatch wrote " << offenders << " of "
|
||||
<< (kExtent * kExtent) << " texels wrongly; texel 0 was "
|
||||
<< readback[0] << ", expected " << expected[0]
|
||||
<< ". A whole stage lost to the ESSL emitter looks exactly like this.";
|
||||
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteTextures(1, &sourceTexture);
|
||||
glDeleteTextures(1, &destTexture);
|
||||
DrainErrors();
|
||||
}
|
||||
};
|
||||
|
||||
// R8UI: one of the seven formats GLSL ES reaches only through GL_NV_image_formats AND one
|
||||
// SPIRV-Cross refuses to print for ESSL, so it needs the widening in both driver modes.
|
||||
TEST_F(FormatlessImageBakeScenario, R8uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE);
|
||||
}
|
||||
|
||||
// R16UI, from the same set, carried in RGBA16UI: the fix must not be R8UI-shaped.
|
||||
TEST_F(FormatlessImageBakeScenario, R16uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
||||
}
|
||||
|
||||
// The control: R32UI is in the GLSL ES core thirteen, so it is baked and never widened.
|
||||
// It passed before the fix and has to keep passing.
|
||||
TEST_F(FormatlessImageBakeScenario, CoreFormatBakedFromTheBoundUnitIsUnaffected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
RunCopy(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,413 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
|
||||
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
|
||||
//
|
||||
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
|
||||
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
|
||||
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
|
||||
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
|
||||
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
|
||||
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
|
||||
//
|
||||
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
|
||||
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
|
||||
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
|
||||
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
|
||||
// program is the control that proves the rule was not simply widened.
|
||||
//
|
||||
// Needs a real context: the validator returns before this rule when no backend object is
|
||||
// active, so the GPU-free negative-API suite cannot reach it.
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
|
||||
// `result` is here so the same program can be captured with transform feedback.
|
||||
const char* const kPointsInGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float result;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
result = 1.0;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kLinesInGeometrySource = R"(#version 420 core
|
||||
layout(lines) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class GeometryDrawModeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "); there is no input primitive to validate";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
|
||||
// driver without GL_EXT_geometry_shader and on a DirectVulkan device without the
|
||||
// geometryShader feature.
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
|
||||
const std::vector<std::pair<GLenum, const char*>> stages = {
|
||||
{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (!compiled) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (capturedVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
if (!linked) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// GL_POINTS is the only mode that decomposes into a points input primitive.
|
||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kPointsInGeometrySource);
|
||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
for (const GLenum mode :
|
||||
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
|
||||
glDrawArrays(mode, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "mode " << mode << " does not decompose into the geometry shader's points input";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The one mode that IS compatible still draws.
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
|
||||
// case asks about. The capture span is really completed first, so GL_POINTS comes back
|
||||
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
|
||||
// never-ended-a-span reason instead and the case could not tell the two apart.
|
||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
|
||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||
|
||||
GLuint feedback = 0;
|
||||
glGenTransformFeedbacks(1, &feedback);
|
||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
|
||||
|
||||
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
|
||||
DrainErrors();
|
||||
|
||||
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
|
||||
DrainErrors();
|
||||
|
||||
// The compatible mode replays the captured span with no error at all, which is what
|
||||
// makes the two assertions above about the MODE and not about the span.
|
||||
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "a compatible mode must still replay the captured span";
|
||||
DrainErrors();
|
||||
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
|
||||
glDeleteTransformFeedbacks(1, &feedback);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
|
||||
// the rule even before the fix. It must still accept the line modes and still reject the
|
||||
// others - a fix that widened the rule instead of repairing its guard breaks this.
|
||||
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = BuildProgram(kLinesInGeometrySource);
|
||||
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||
static_cast<GLenum>(GL_LINE_LOOP)}) {
|
||||
glDrawArrays(mode, 0, 2);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "mode " << mode << " decomposes into lines and must be accepted";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
|
||||
glDrawArrays(mode, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "mode " << mode << " does not decompose into lines";
|
||||
DrainErrors();
|
||||
}
|
||||
}
|
||||
|
||||
// The other half of "ask the stage": WHICH stage list is asked. gsInputPrimitive is a
|
||||
// LINK artifact, so pairing it with the live attach list re-points the GL_NONE/GL_POINTS
|
||||
// aliasing instead of removing it - inside the window between glAttachShader and the
|
||||
// next link, the live list says "geometry present" while the artifact still reads
|
||||
// GL_NONE, which is 0, which is GL_POINTS, so every mode but GL_POINTS is rejected.
|
||||
//
|
||||
// GL 4.6 core 7.3 makes that window legal and ordinary: an attach affects the program's
|
||||
// executable only at the next link, and leaves LINK_STATUS alone. The attached shader
|
||||
// need not even compile. Worse, it does not heal - glDetachShader defers the removal to
|
||||
// the next Link() too, so the program would keep failing every non-POINTS draw until the
|
||||
// application happened to relink for some unrelated reason.
|
||||
TEST_F(GeometryDrawModeScenario, AttachingAGeometryStageAfterTheLinkDoesNotConstrainTheDrawMode) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// Deliberately NOT BuildProgram: the executable under test has no geometry stage.
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
for (const auto& [stage, source] :
|
||||
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
glAttachShader(program, shader);
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "a program with no geometry stage must draw triangles";
|
||||
DrainErrors();
|
||||
|
||||
const GLuint geometry = glCreateShader(GL_GEOMETRY_SHADER);
|
||||
glShaderSource(geometry, 1, &kPointsInGeometrySource, nullptr);
|
||||
glCompileShader(geometry);
|
||||
glAttachShader(program, geometry);
|
||||
glDeleteShader(geometry);
|
||||
DrainErrors();
|
||||
|
||||
// Same executable as three lines ago - no relink has happened.
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "the attach does not reach the executable until the next link, so the geometry "
|
||||
"shader's points input must not constrain this draw";
|
||||
DrainErrors();
|
||||
|
||||
// And once it IS linked in, the rule applies - the fix must not have simply disabled it.
|
||||
glLinkProgram(program);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "the relink with the geometry stage failed";
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||
<< "now that the points-in geometry shader is in the executable, triangles must be rejected";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The tessellation guard above the geometry one had the identical defect, and it does not
|
||||
// even need the GL_NONE aliasing to misfire: it drives BOTH directions unconditionally, so
|
||||
// reading the live attach list rejects every non-GL_PATCHES draw the moment an evaluation
|
||||
// shader is attached, whether or not it was ever linked in.
|
||||
TEST_F(GeometryDrawModeScenario, AttachingATessEvalStageAfterTheLinkDoesNotForceGlPatches) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
GLint maxPatchVertices = 0;
|
||||
glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||
DrainErrors();
|
||||
if (maxPatchVertices < 3) GTEST_SKIP() << "no tessellation stage on this backend";
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
for (const auto& [stage, source] :
|
||||
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
glAttachShader(program, shader);
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
||||
|
||||
glUseProgram(program);
|
||||
DrainErrors();
|
||||
|
||||
static const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, ccw) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
const GLuint tessEval = glCreateShader(GL_TESS_EVALUATION_SHADER);
|
||||
glShaderSource(tessEval, 1, &kTessEvalSource, nullptr);
|
||||
glCompileShader(tessEval);
|
||||
glAttachShader(program, tessEval);
|
||||
glDeleteShader(tessEval);
|
||||
DrainErrors();
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "the executable still has no tessellation stage, so GL_PATCHES must not be required";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // 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
|
||||
|
||||
@@ -127,14 +127,23 @@ void main()
|
||||
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||
// spelling in a single declaration and cannot be expressed at all without splitting
|
||||
// the array into one declaration per element and rewriting every use of it.
|
||||
// spelling in a single declaration.
|
||||
//
|
||||
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
||||
// storage-block rebinding cases: the defect is per-backend and the frontend
|
||||
// mechanism these cases exist for - per-element units surviving the trip to the
|
||||
// pipeline composite - is fully exercised on Magma.
|
||||
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
// RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
|
||||
// image uniform per element, each carrying its own binding, which costs exactly the
|
||||
// four image uniforms the application declared. (It used to WIDEN the array to cover
|
||||
// the whole span instead, which cost seven for those four elements and had to be
|
||||
// declined on a stage that could not afford them - hence the budget gate that used
|
||||
// to be here.) DirectVulkan needs no rewrite at all.
|
||||
bool PerElementImageUnitsAreHonoured() const {
|
||||
if (Gl().BackendName() == "DirectVulkan") return true;
|
||||
GLint maxFragmentImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
// One per element of the four-element array either fragment program declares.
|
||||
return maxFragmentImageUniforms >= 4;
|
||||
}
|
||||
|
||||
// The scenarios below need image load/store at all; a driver without it should skip
|
||||
// rather than fail.
|
||||
@@ -164,7 +173,7 @@ void main()
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
GTEST_SKIP() << "fewer than 4 fragment image uniforms: the array under test does not fit";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
@@ -283,8 +292,12 @@ void main()
|
||||
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||
if (!PerElementImageUnitsAreHonoured()) {
|
||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
|
||||
// here are consecutive on purpose, so on Espryt this would exercise nothing the case
|
||||
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
|
||||
// it deliberately never triggers.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,325 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PostLinkAttachScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A PROGRAM'S LIVE ATTACH LIST IS NOT ITS EXECUTABLE, AND THE BACKENDS MAY NOT
|
||||
// INDEX ONE BY THE OTHER.
|
||||
//
|
||||
// GL 4.6 core 7.3: glAttachShader adds to the program's attach list immediately and affects
|
||||
// what the program RUNS only at the next link (glDetachShader defers its removal the same
|
||||
// way). So between an attach and the relink the two lists differ - the attach list is
|
||||
// strictly longer - and the program stays perfectly drawable throughout, with the executable
|
||||
// its last link produced.
|
||||
//
|
||||
// Both backends walked the attach list while indexing the LAST LINK's generated SPIR-V by
|
||||
// the same running index:
|
||||
//
|
||||
// DirectGLES BackendProgramObjectImpl::SyncToBackend - `shaderSpirvs[index]` over
|
||||
// `attachedShaders.size()`
|
||||
// DirectVulkan ProgramFactory::GetOrCreateProgram - `spirv[i]` and `moduleSpirvs[i]`
|
||||
// over `shaders.size()`
|
||||
//
|
||||
// One post-link attach therefore read one Vector past the end of the module array and
|
||||
// copied it, which is the SIGSEGV this scenario is the regression test for (the source
|
||||
// vector reported a capacity of 35177040171136). DirectGLES additionally derived
|
||||
// "does this program tessellate" from the same wrong list, which would synthesize a
|
||||
// pass-through tessellation control stage for an executable that does not tessellate.
|
||||
//
|
||||
// The repro needs the attach to land BEFORE the program's first backend build: the ES
|
||||
// twin's rebuild is gated on the link version (which an attach does not move), so a program
|
||||
// that was already drawn once keeps its built driver program and never re-reads the list.
|
||||
// Every case below therefore attaches first and draws second.
|
||||
//
|
||||
// Deliberately pinned with a PIXEL and not just with glGetError. "Reject the draw earlier"
|
||||
// would silence the crash while breaking the spec - GL requires this draw to execute - so
|
||||
// the assertion has to be that the frame really came out, not merely that nothing complained.
|
||||
//
|
||||
// Needs a real context: the crash is in a backend program build, which the GPU-free suites
|
||||
// never reach.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboWidth = 64;
|
||||
constexpr int kFboHeight = 64;
|
||||
|
||||
// A full-viewport triangle from gl_VertexID alone, so the scenario needs no vertex
|
||||
// buffer and every pixel of the target is covered by the one draw.
|
||||
const char* const kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The replacement fragment stage of the last case. A different colour, so "which
|
||||
// executable did this draw run" is answerable from the frame alone.
|
||||
const char* const kBlueFragmentSource = R"(#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr Rgba8 kGreen{0, 255, 0, 255};
|
||||
constexpr Rgba8 kBlue{0, 0, 255, 255};
|
||||
|
||||
// The extra attaches. Each declares a stage the executable ALREADY has and no main(),
|
||||
// which is what a real shader library looks like and what makes the relink at the end
|
||||
// of the second case legal. Their whole job here is to make the attach list longer
|
||||
// than the module array.
|
||||
const char* const kVertexHelperSource = R"(#version 330 core
|
||||
vec4 mgPostLinkAttachVertexHelper()
|
||||
{
|
||||
return vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentHelperSource = R"(#version 330 core
|
||||
vec4 mgPostLinkAttachFragmentHelper()
|
||||
{
|
||||
return vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A pass-through, so that once it IS linked in the same full-viewport triangle still
|
||||
// reaches the rasterizer and the final frame is still comparable to the first one.
|
||||
const char* const kGeometrySource = R"(#version 330 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gl_Position = gl_in[i].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
class PostLinkAttachScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
m_target = MakeColorFbo(kFboWidth, kFboHeight);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
|
||||
BindFbo(m_target);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
for (const GLuint shader : m_shaders) glDeleteShader(shader);
|
||||
m_shaders.clear();
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
// Kept alive until TearDown rather than flagged for deletion at attach time: a
|
||||
// deleted-but-attached shader is a second, unrelated lifetime rule, and this
|
||||
// scenario is about which LIST the backend reads.
|
||||
GLuint MakeShader(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
if (shader == 0) return 0;
|
||||
m_shaders.push_back(shader);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
// Vertex + fragment, linked. This is the executable every case draws with.
|
||||
// `outFragmentShader` is the stage that paints green, which the last case needs a
|
||||
// name for in order to detach it.
|
||||
GLuint LinkBaseProgram(GLuint* outFragmentShader = nullptr) {
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
const GLuint fragment = MakeShader(GL_FRAGMENT_SHADER, kFragmentSource);
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
glAttachShader(program, fragment);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) return 0;
|
||||
if (outFragmentShader != nullptr) *outFragmentShader = fragment;
|
||||
return program;
|
||||
}
|
||||
|
||||
// Clears to red, draws the full-viewport triangle, and hands back the frame. Red
|
||||
// is deliberately the clear colour: a draw that silently did not execute leaves a
|
||||
// red target, which is a different failure message from a draw that executed and
|
||||
// painted the wrong thing.
|
||||
//
|
||||
// `outDrawError` is sampled between the draw and the readback, so a rejected draw
|
||||
// is never confused with a readback that went wrong afterwards.
|
||||
Image DrawFullViewportTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
|
||||
glUseProgram(program);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrainErrors();
|
||||
glDrawArrays(mode, 0, 3);
|
||||
if (outDrawError != nullptr) *outDrawError = glGetError();
|
||||
return ReadPixels(kFboWidth, kFboHeight);
|
||||
}
|
||||
|
||||
// The clear colour is red and no shader here ever writes red, so "still red" reads
|
||||
// as "the draw did not execute" and any other wrong colour as "it executed against
|
||||
// the wrong modules" - two failures worth telling apart.
|
||||
static void ExpectFullyColored(const Image& frame, const Rgba8& expected, const char* what) {
|
||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
||||
for (const int y : {0, kFboHeight / 2, kFboHeight - 1}) {
|
||||
for (const int x : {0, kFboWidth / 2, kFboWidth - 1}) {
|
||||
EXPECT_EQ(frame.At(x, y), expected)
|
||||
<< what << ": pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
ColorFbo m_target{};
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_shaders;
|
||||
};
|
||||
|
||||
// THE REGRESSION. Up to four shaders attached after the link (the geometry one only
|
||||
// where the backend has that stage), two of them duplicating a stage the executable
|
||||
// already carries - so the attach list runs to five or six while the last link produced
|
||||
// two modules, and the old loops read indices 2..5 of a 2-element array.
|
||||
//
|
||||
// Duplicating a stage is the sharp case on purpose: it is the one shape under which a
|
||||
// "look the stage up in the attach list instead" repair still returns a valid-looking
|
||||
// index for a module that does not exist.
|
||||
TEST_F(PostLinkAttachScenario, DrawingAfterPostLinkAttachesStaysInsideTheGeneratedModules) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program = LinkBaseProgram();
|
||||
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
|
||||
|
||||
// Not drawn yet: the ES backend's rebuild is gated on the link version, so a draw
|
||||
// here would build the driver program from the 2-module executable and the attaches
|
||||
// below would never be re-read. The repro is the FIRST build seeing the long list.
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexHelperSource));
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentHelperSource));
|
||||
if (BackendHostsGeometry()) {
|
||||
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
|
||||
}
|
||||
// The stage that made DirectGLES synthesize a pass-through control stage for a
|
||||
// program whose executable does not tessellate. Attached whether or not this
|
||||
// backend can tessellate - an attach needs no support and no successful compile.
|
||||
const GLuint tessEval = MakeShader(GL_TESS_EVALUATION_SHADER, R"(#version 420 core
|
||||
layout(triangles, equal_spacing, ccw) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)");
|
||||
if (tessEval != 0) glAttachShader(program, tessEval);
|
||||
DrainErrors();
|
||||
|
||||
GLint attachedCount = 0;
|
||||
glGetProgramiv(program, GL_ATTACHED_SHADERS, &attachedCount);
|
||||
DrainErrors();
|
||||
ASSERT_GT(attachedCount, 2) << "the attaches did not land, so this case is not testing anything";
|
||||
|
||||
// Still the two-stage executable of three lines ago, and GL says it draws.
|
||||
GLenum drawError = GL_NO_ERROR;
|
||||
const Image frame = DrawFullViewportTriangle(program, GL_TRIANGLES, &drawError);
|
||||
EXPECT_EQ(drawError, static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "the attaches have not been linked in, so nothing about them may reject this draw";
|
||||
ExpectFullyColored(frame, kGreen, "the post-attach draw");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The same window, asked to prove something stronger than "it did not crash": WHICH
|
||||
// modules the draw in that window ran. Between the detach+attach and the relink the
|
||||
// program has three attached shaders and two modules, and GL 4.6 core 7.3 says the
|
||||
// executable is still the one the last link produced - so the frame must come out in
|
||||
// the OLD fragment shader's colour, not the newly attached one's and not garbage.
|
||||
//
|
||||
// This is also the other direction of the fix, so it cannot be "freeze the backend on
|
||||
// the first link": the relink really does swap the executable, and the very next draw
|
||||
// has to be rebuilt from it.
|
||||
TEST_F(PostLinkAttachScenario, TheWindowKeepsTheOldExecutableAndTheRelinkSwapsIt) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
GLuint greenFragment = 0;
|
||||
const GLuint program = LinkBaseProgram(&greenFragment);
|
||||
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
|
||||
|
||||
// Both of these are deferred to the next link, in opposite directions: the green
|
||||
// stage stays in the executable until then, and the blue one stays out of it.
|
||||
const GLuint blueFragment = MakeShader(GL_FRAGMENT_SHADER, kBlueFragmentSource);
|
||||
glDetachShader(program, greenFragment);
|
||||
glAttachShader(program, blueFragment);
|
||||
DrainErrors();
|
||||
|
||||
GLenum windowDrawError = GL_NO_ERROR;
|
||||
const Image inTheWindow = DrawFullViewportTriangle(program, GL_TRIANGLES, &windowDrawError);
|
||||
EXPECT_EQ(windowDrawError, static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "neither the detach nor the attach has been linked in, so the draw must execute";
|
||||
ExpectFullyColored(inTheWindow, kGreen, "the draw inside the attach window");
|
||||
DrainErrors();
|
||||
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "the relink onto the blue fragment stage failed";
|
||||
DrainErrors();
|
||||
|
||||
GLenum relinkedDrawError = GL_NO_ERROR;
|
||||
const Image afterRelink = DrawFullViewportTriangle(program, GL_TRIANGLES, &relinkedDrawError);
|
||||
EXPECT_EQ(relinkedDrawError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked program must draw";
|
||||
ExpectFullyColored(afterRelink, kBlue, "the draw after the relink");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,366 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RelinkStageSetScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A RELINK MAY CHANGE WHICH STAGES A PROGRAM HAS, AND EVERY DRAW AFTER IT RUNS
|
||||
// THE NEW STAGE SET.
|
||||
//
|
||||
// GL 4.6 core 7.3: glLinkProgram builds an executable out of whatever is attached at that
|
||||
// moment, so the stage set is a property of a LINK and not of a program. A program that
|
||||
// linked vertex+fragment, drew, then had a geometry shader attached and was relinked runs
|
||||
// three stages from that point on.
|
||||
//
|
||||
// DirectGLES rebuilds its driver program in place - same GL name, new executable - and the
|
||||
// per-draw bind dedupes on that name, so a relink that changed the stage set installed
|
||||
// nothing and the following draws rendered NOTHING at all: no GL error, LINK_STATUS true,
|
||||
// and a framebuffer that kept its clear colour. See the note at the glLinkProgram in
|
||||
// BackendProgramObjectImpl::SyncToBackend for what the driver does with such a relink.
|
||||
//
|
||||
// PostLinkAttachScenario pins the other half of the same rule - that the executable does
|
||||
// NOT move until the relink. This one pins what happens when it does, in all three
|
||||
// directions: a stage added, a stage removed, and a stage added that the ES backend has to
|
||||
// synthesize a partner for.
|
||||
//
|
||||
// Every case asserts on a SHAPE and not merely on "something came out". The geometry and
|
||||
// tessellation stages here halve the triangle, so a full-viewport green frame and a
|
||||
// half-size one say which executable ran - "still drew" and "drew the right stages" are
|
||||
// different claims and only the second one is worth pinning.
|
||||
//
|
||||
// Needs a real context: what is asserted is a rendered pixel out of a backend program build.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboWidth = 64;
|
||||
constexpr int kFboHeight = 64;
|
||||
|
||||
// A full-viewport triangle out of gl_VertexID alone, so no case here needs a vertex
|
||||
// buffer and one draw covers every pixel of the target.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Halves the triangle instead of passing it through: the centre pixel stays covered
|
||||
// and all four corners fall outside, so the frame alone says whether this stage ran.
|
||||
const char* const kGeometrySource = R"(#version 420 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gl_Position = vec4(gl_in[i].gl_Position.xy * 0.5, gl_in[i].gl_Position.zw);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// No control stage on purpose: OpenGL ES rejects that shape outright, so DirectGLES
|
||||
// synthesizes a pass-through one (AttachPassthroughTessControlStage) and DirectVulkan
|
||||
// does the same. Reading only gl_in[].gl_Position keeps this inside what such a
|
||||
// pass-through may forward. At the tessellation levels it sets (all 1.0) the patch
|
||||
// comes back out as one triangle whose gl_TessCoord values are the three corners, so
|
||||
// the barycentric sum reproduces the vertex stage's triangle - halved, for the same
|
||||
// reason the geometry stage above halves it.
|
||||
const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, ccw) in;
|
||||
void main()
|
||||
{
|
||||
vec4 p = gl_TessCoord.x * gl_in[0].gl_Position +
|
||||
gl_TessCoord.y * gl_in[1].gl_Position +
|
||||
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||
gl_Position = vec4(p.xy * 0.5, p.zw);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr Rgba8 kGreen{0, 255, 0, 255};
|
||||
constexpr Rgba8 kRed{255, 0, 0, 255};
|
||||
|
||||
class RelinkStageSetScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
m_target = MakeColorFbo(kFboWidth, kFboHeight);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
|
||||
BindFbo(m_target);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
for (const GLuint shader : m_shaders) glDeleteShader(shader);
|
||||
m_shaders.clear();
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// The same real-backend probes the other stage-gated scenarios use: 0 on a
|
||||
// DirectGLES driver without the extension and on a DirectVulkan device without
|
||||
// the feature.
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
if (length <= 0) return {};
|
||||
std::string log(static_cast<size_t>(length), '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length, nullptr, log.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length, nullptr, log.data());
|
||||
}
|
||||
log.resize(std::char_traits<char>::length(log.c_str()));
|
||||
return log;
|
||||
}
|
||||
|
||||
GLuint MakeShader(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
if (shader == 0) return 0;
|
||||
m_shaders.push_back(shader);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE) << "a scenario shader did not compile: " << InfoLog(shader, true);
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint MakeProgram() {
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
bool Link(GLuint program) {
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
ADD_FAILURE() << "the link failed: " << InfoLog(program, false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clears to red and draws. Red is the clear colour deliberately: nothing here ever
|
||||
// paints red inside the triangle, so a frame that is red where it should be green
|
||||
// says "this draw did not execute" while a frame that is green where it should be
|
||||
// red says "it executed against the wrong executable" - two failures worth telling
|
||||
// apart. The error is sampled between the draw and the readback so a rejected draw
|
||||
// is never confused with a readback that went wrong afterwards.
|
||||
Image DrawTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
|
||||
glUseProgram(program);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrainErrors();
|
||||
glDrawArrays(mode, 0, 3);
|
||||
if (outDrawError != nullptr) *outDrawError = glGetError();
|
||||
return ReadPixels(kFboWidth, kFboHeight);
|
||||
}
|
||||
|
||||
// The vertex stage's triangle covers the whole target, corners included.
|
||||
static void ExpectFullTriangle(const Image& frame, const char* what) {
|
||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
||||
for (const int y : {0, kFboHeight - 1}) {
|
||||
for (const int x : {0, kFboWidth - 1}) {
|
||||
ExpectPixel(frame, x, y, kGreen, what, "corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...and halved by a geometry or tessellation stage it no longer reaches any of
|
||||
// them, which is what makes the shape readable as "that stage ran".
|
||||
static void ExpectHalvedTriangle(const Image& frame, const char* what) {
|
||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
||||
for (const int y : {0, kFboHeight - 1}) {
|
||||
for (const int x : {0, kFboWidth - 1}) {
|
||||
ExpectPixel(frame, x, y, kRed, what, "corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ExpectPixel(const Image& frame, int x, int y, const Rgba8& expected, const char* what,
|
||||
const char* where) {
|
||||
EXPECT_EQ(frame.At(x, y), expected)
|
||||
<< what << ": " << where << " pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
ColorFbo m_target{};
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_shaders;
|
||||
};
|
||||
|
||||
// THE REGRESSION. Vertex+fragment, linked and DRAWN - which is what puts a built driver
|
||||
// program on the backend twin - then a geometry shader attached and the program
|
||||
// relinked. The halved frame is the assertion: the three-stage executable really is
|
||||
// what the next draw ran.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToAddAGeometryStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to add";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
||||
ExpectFullTriangle(before, "the draw before the relink");
|
||||
DrainErrors();
|
||||
|
||||
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked three-stage program must draw";
|
||||
ExpectHalvedTriangle(after, "the draw after the geometry stage was linked in");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The same move in the other direction, which no repair may confuse with "the stage
|
||||
// set did not change": the geometry stage leaves the executable, so the halving has to
|
||||
// stop with it.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToRemoveAGeometryStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to remove";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
const GLuint geometry = MakeShader(GL_GEOMETRY_SHADER, kGeometrySource);
|
||||
glAttachShader(program, geometry);
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
// Also the control for the case above: a three-stage program linked in ONE go and
|
||||
// never relinked draws its halved triangle.
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the three-stage draw must execute";
|
||||
ExpectHalvedTriangle(before, "the draw before the geometry stage was dropped");
|
||||
DrainErrors();
|
||||
|
||||
glDetachShader(program, geometry);
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked vertex+fragment program must draw";
|
||||
ExpectFullTriangle(after, "the draw after the geometry stage was dropped");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The third direction, and the one that asks the most of the rebuild: the added stage
|
||||
// is a tessellation evaluation shader with no control stage, so the ES backend has to
|
||||
// synthesize a pass-through control stage for an executable that had neither a moment
|
||||
// ago. GL_PATCHES becomes the only legal mode with it, which is also the only draw-mode
|
||||
// change any case here makes.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToAddATessEvalStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to add";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
||||
ExpectFullTriangle(before, "the draw before the relink");
|
||||
DrainErrors();
|
||||
|
||||
glAttachShader(program, MakeShader(GL_TESS_EVALUATION_SHADER, kTessEvalSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
// Three, which is already the default; spelled out because the synthesized control
|
||||
// stage's output patch size is compiled from it.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_PATCHES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked tessellating program must draw";
|
||||
ExpectHalvedTriangle(after, "the draw after the tessellation stage was linked in");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -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
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The eight vertex shaders of the conformance sweep, verbatim in shape. Each reads three
|
||||
// vec4 positions out of a storage block on binding 0 and emits them as a triangle that
|
||||
// covers the whole viewport.
|
||||
constexpr const char* kFormVS[8] = {
|
||||
// The eight vertex shaders of the conformance sweep, verbatim in shape, plus a ninth that
|
||||
// is not from the sweep (see form 8). Each reads three vec4 positions out of a storage
|
||||
// block on binding 0 and emits them as a triangle that covers the whole viewport.
|
||||
constexpr const char* kFormVS[9] = {
|
||||
// 0 - instance name, no binding qualifier, sized array member
|
||||
R"(#version 430 core
|
||||
layout(std430) buffer Buffer {
|
||||
@@ -127,6 +127,38 @@ void main() {
|
||||
case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break;
|
||||
}
|
||||
}
|
||||
)",
|
||||
// 8 - NOT from the conformance sweep. An unqualified storage block with a UNIFORM
|
||||
// BLOCK beside it, which is what makes the block's DEFAULT binding observable at all.
|
||||
//
|
||||
// GL 4.3 core 7.8 gives a storage block with no layout(binding = N) a buffer binding
|
||||
// of zero. Forms 0, 1, 3, 4 and 5 above are all unqualified and all pass, but they
|
||||
// cannot prove that rule holds: they are the only resource in their shader, so the
|
||||
// binding glslang's IO mapper invents for them happens to BE zero and the right answer
|
||||
// arrives for the wrong reason.
|
||||
//
|
||||
// Every shader here is parsed as a Vulkan client, so that mapper allocates out of ONE
|
||||
// flat space shared by samplers, images, uniform blocks, storage blocks and the
|
||||
// synthesized global-uniform block (iomapper.cpp resolveBinding takes the `ent.newSet`
|
||||
// branch, and every resource resolves to set 0), and then writes the result back into
|
||||
// the type's qualifier - so the reflection cannot tell an invented binding from a
|
||||
// declared one. Put anything live next to the block and it is pushed off zero, the
|
||||
// draw reads a binding point nothing was ever bound to, and the triangle collapses
|
||||
// with no GL error anywhere. That is
|
||||
// KHR-GL43.compute_shader.resource-ubo's whole failure, in a vertex stage.
|
||||
//
|
||||
// The uniform block is REBOUND explicitly with glUniformBlockBinding, exactly as that
|
||||
// conformance case does. That keeps this case about the storage block's default and
|
||||
// not about the uniform block's - the rebinding path has always worked, and the
|
||||
// uniform-block default is a separate (still open) question.
|
||||
R"(#version 430 core
|
||||
layout(std140) uniform ScaleBlock {
|
||||
vec4 factor;
|
||||
} g_scale;
|
||||
layout(std430) buffer Buffer {
|
||||
vec4 position[3];
|
||||
} g_input_buffer;
|
||||
void main() { gl_Position = g_input_buffer.position[gl_VertexID] * g_scale.factor; }
|
||||
)",
|
||||
};
|
||||
|
||||
@@ -197,6 +229,26 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error);
|
||||
ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error;
|
||||
|
||||
// Form 8 alone declares a uniform block, and it exists only to occupy a slot the
|
||||
// storage block must not be pushed onto. Bound to a buffer of ones so it scales
|
||||
// the positions by exactly 1 - the block's contribution to the IMAGE is nothing,
|
||||
// and its contribution to the TEST is that it is there at all.
|
||||
GLuint uniformBuffer = 0;
|
||||
if (form == 8) {
|
||||
const float ones[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenBuffers(1, &uniformBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
|
||||
glBufferData(GL_UNIFORM_BUFFER, sizeof(ones), ones, GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniformBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
const GLuint blockIndex = glGetUniformBlockIndex(program, "ScaleBlock");
|
||||
ASSERT_NE(blockIndex, GL_INVALID_INDEX) << "form 8: the uniform block is not active";
|
||||
// Explicit, so this case cannot fail on the uniform block's own default
|
||||
// binding - which is a separate question from the storage block's.
|
||||
glUniformBlockBinding(program, blockIndex, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "form 8: uniform block setup errored";
|
||||
}
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
@@ -221,6 +273,7 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteProgram(program);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
if (uniformBuffer != 0) glDeleteBuffers(1, &uniformBuffer);
|
||||
gl.EndFrame();
|
||||
}
|
||||
};
|
||||
@@ -241,6 +294,10 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock)
|
||||
MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne)
|
||||
MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout)
|
||||
// The form that makes the DEFAULT binding observable rather than accidental: forms 0/1/3/4/5
|
||||
// are unqualified too, but nothing competes with them for glslang's flat slot 0, so they
|
||||
// would keep passing even with the default wrong. See the comment on kFormVS[8].
|
||||
MGL_SSBO_FORM_CASE(8, NoBindingQualifierBesideAUniformBlock)
|
||||
// ---- the two forms that do not work yet ----
|
||||
//
|
||||
// Both carry an UNSIZED array that is not the block's sole trailing member, and both fail
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -129,6 +129,34 @@ namespace {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Blocks come out of reflection in three kinds and only one of them is a GL uniform block.
|
||||
// The same split ProgramInterface::ClassifyBlock makes (it reads the flattened
|
||||
// TypeFacts::isBuffer, which is this very qualifier), reachable here from the live TProgram
|
||||
// because the block index spaces are built before the reflection snapshot exists.
|
||||
|
||||
// The transpiler lowers every atomic_uint onto a synthesized "gl_AtomicCounterBlock_<binding>"
|
||||
// buffer block, which reflection then reports as an ordinary block. It is not one: GL
|
||||
// enumerates it through GL_ACTIVE_ATOMIC_COUNTER_BUFFERS instead.
|
||||
static MobileGL::Bool IsAtomicCounterBlockName(const MobileGL::String& name) {
|
||||
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
|
||||
const MobileGL::SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
|
||||
return name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
|
||||
}
|
||||
|
||||
// A shader storage block: GL enumerates it through GL_SHADER_STORAGE_BLOCK and its members
|
||||
// through GL_BUFFER_VARIABLE. The counter blocks above are buffer blocks too, hence the
|
||||
// exclusion. A block whose type reflection did not survive is treated as a uniform block,
|
||||
// which is what every caller assumed before this classification existed.
|
||||
static MobileGL::Bool IsStorageBlock(const glslang::TObjectReflection& block) {
|
||||
if (IsAtomicCounterBlockName(block.name)) return false;
|
||||
const glslang::TType* type = block.getType();
|
||||
return type != nullptr && type->getQualifier().storage == glslang::EvqBuffer;
|
||||
}
|
||||
|
||||
static MobileGL::Bool IsGlUniformBlock(const glslang::TObjectReflection& block) {
|
||||
return !IsAtomicCounterBlockName(block.name) && !IsStorageBlock(block);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters
|
||||
// may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past
|
||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the
|
||||
@@ -551,8 +579,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
if (!ValidateAttachedShaders()) return;
|
||||
|
||||
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
|
||||
// run before the L1 probe, which needs the merged opaque bindings in its key.
|
||||
// Reads the COMPILE snapshots only - no parsed shader - so it runs before the L1
|
||||
// probe: a conflicting explicit uniform location must fail the link whether or not
|
||||
// the memo has an answer for this program's sources.
|
||||
MergeShaderSideChannels();
|
||||
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
|
||||
|
||||
@@ -589,11 +618,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
|
||||
// The last two are OUT parameters that mapIO fills, not requests it honours: the IO
|
||||
// mapper's collect callback is the last point at which a resource's qualifier still
|
||||
// says what the SHADER declared rather than what glslang assigned, so both captures
|
||||
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
|
||||
ProgramAttrib attrib{.shaders = Move(shaders),
|
||||
.explicitVertexInLocations = in.explicitAttribLocations,
|
||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings};
|
||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
|
||||
|
||||
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
||||
auto result = ShaderCompiler::LinkProgram(attrib);
|
||||
@@ -611,6 +645,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// A compute program must have a fixed local group size, and GL states that as a
|
||||
// property of the PROGRAM: "at least one" of its compute shaders declares it (GL 4.6
|
||||
// core 7.13 / GLSL 4.30 4.4.1.4). MobileGL used to answer that question per SHADER,
|
||||
@@ -752,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;
|
||||
@@ -770,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);
|
||||
@@ -786,7 +828,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
keyInputs.explicitVertexInLocations = &in.explicitAttribLocations;
|
||||
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
|
||||
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
|
||||
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
|
||||
// In the key ONLY because the payload now carries the reflection: transform feedback
|
||||
// is resolved by reading the linked intermediates and never perturbs the generated
|
||||
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
|
||||
@@ -798,17 +839,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
return BuildSpirvTranslationKey(keyInputs);
|
||||
}
|
||||
|
||||
// The link rejections that need nothing but the compile snapshots. They run before the
|
||||
// The one link rejection that needs nothing but the compile snapshots. It runs before the
|
||||
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
|
||||
// The two lexical side channels the relaxed parse cannot provide, merged across stages:
|
||||
// explicit default-block uniform locations (which must agree, or the link fails) and
|
||||
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
|
||||
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
|
||||
// of the L1 memo key.
|
||||
//
|
||||
// Only the explicit default-block uniform locations are merged here, and only because they
|
||||
// are the one piece of relaxed-parse wreckage that has to be recovered at COMPILE time:
|
||||
// the snapshot is taken inside the parse, so it is per-shader by construction, and the
|
||||
// same uniform declared in several stages must agree or the program cannot link. The
|
||||
// opaque bindings and the unqualified storage blocks used to be merged alongside them;
|
||||
// both now arrive from mapIO during LinkProgram below, straight into `artifacts`, which is
|
||||
// both later and strictly better informed - the IO mapper sees macro-expanded declarations
|
||||
// and a per-shader lexer never could.
|
||||
void ProgramLinkTask::MergeShaderSideChannels() {
|
||||
// Merge the shaders' lexically extracted explicit uniform locations. The same
|
||||
// uniform declared in several stages must agree on its location (config-A glslang
|
||||
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
|
||||
for (const auto& shader : in.shaders) {
|
||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||
for (const auto& [name, location] : compiled.explicitUniformLocations) {
|
||||
@@ -822,14 +864,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Sampler/image layout(binding = N) initial units, likewise invisible to the
|
||||
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
|
||||
// name declared in several stages keeps the last stage's binding as before.
|
||||
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
|
||||
artifacts.explicitOpaqueUniformBindings[name] = binding;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// An L1 hit: the entire front end, published without constructing a TShader or a
|
||||
@@ -1002,6 +1037,30 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.glBlockIndexToTProgram.push_back(i);
|
||||
}
|
||||
|
||||
// The GL_UNIFORM_BLOCK subsequence of that space. MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection above, so glslang files BUFFER blocks
|
||||
// under indexToUniformBlock as well and the list just built also holds every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 says
|
||||
// GL_ACTIVE_UNIFORM_BLOCKS / glGetActiveUniformBlockiv / glGetUniformBlockIndex see
|
||||
// uniform blocks and nothing else; an atomic counter buffer is enumerated by
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS and a storage block by GL_SHADER_STORAGE_BLOCK.
|
||||
//
|
||||
// A SECOND space rather than a filter of the first, deliberately: the block space is
|
||||
// what the backends walk (DirectGLES hands out one ESSL uniform-buffer binding point per
|
||||
// entry as it goes) and what "tProgramBlockIndexToGl[i] < 0 means MGL_GLOBAL_UBO" reads,
|
||||
// and neither may move.
|
||||
artifacts.blockIndexToGlUniformBlock.assign(artifacts.glBlockIndexToTProgram.size(), -1);
|
||||
artifacts.glUniformBlockIndexToBlock.clear();
|
||||
for (SizeT blockIndex = 0; blockIndex < artifacts.glBlockIndexToTProgram.size(); ++blockIndex) {
|
||||
const auto& block = artifacts.program->getUniformBlock(artifacts.glBlockIndexToTProgram[blockIndex]);
|
||||
if (!IsGlUniformBlock(block)) continue;
|
||||
artifacts.blockIndexToGlUniformBlock[blockIndex] =
|
||||
static_cast<Int>(artifacts.glUniformBlockIndexToBlock.size());
|
||||
artifacts.glUniformBlockIndexToBlock.push_back(static_cast<Int>(blockIndex));
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Reflection - %zu block(s), %zu of them GL uniform blocks", in.externalIndex,
|
||||
artifacts.glBlockIndexToTProgram.size(), artifacts.glUniformBlockIndexToBlock.size());
|
||||
|
||||
// ------------ Uniforms (GL Plain) ----------------
|
||||
// The relaxed parse sweeps every DECLARED default-block uniform into
|
||||
// MGL_GLOBAL_UBO whether or not any stage reads it. GL requires a
|
||||
@@ -1016,6 +1075,26 @@ namespace MobileGL::MG_State::GLState {
|
||||
return uniform.index >= 0 && uniform.index < static_cast<Int>(artifacts.tProgramBlockIndexToGl.size()) &&
|
||||
artifacts.tProgramBlockIndexToGl[uniform.index] < 0;
|
||||
};
|
||||
// Member of a block GL can see - a named uniform block, a buffer block, or the
|
||||
// synthesized atomic-counter block. GL locations are a property of the DEFAULT uniform
|
||||
// block alone (GL 4.6 core 7.6.1), so these take none.
|
||||
const auto isNamedBlockMember = [&isGlobalUboMember](const glslang::TObjectReflection& uniform) {
|
||||
return uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
};
|
||||
// A member of a BUFFER block is a buffer variable, not a uniform: GL 4.6 core 7.3.1
|
||||
// gives it the GL_BUFFER_VARIABLE interface and 7.6 keeps it out of GL_ACTIVE_UNIFORMS,
|
||||
// glGetActiveUniform, glGetUniformIndices and glGetActiveUniformsiv. The relaxed parse
|
||||
// reflects it as a uniform anyway (no EShReflectionSeparateBuffers), so drop it from the
|
||||
// GL index space here - the same place the dead default-block uniforms are dropped, and
|
||||
// the counterpart of the location half already handled by isNamedBlockMember below.
|
||||
//
|
||||
// Atomic counters are NOT in this set even though their synthesized owner is a buffer
|
||||
// block: an atomic_uint IS a uniform (of type GL_UNSIGNED_INT_ATOMIC_COUNTER), and
|
||||
// KHR-GL43.shader_atomic_counters.basic-program-query enumerates it as one.
|
||||
const auto isBufferVariable = [this](const glslang::TObjectReflection& uniform) {
|
||||
if (uniform.index < 0 || uniform.index >= artifacts.program->getNumUniformBlocks()) return false;
|
||||
return IsStorageBlock(artifacts.program->getUniformBlock(uniform.index));
|
||||
};
|
||||
for (Int i = 0; i < tProgramUniformCount; i++) {
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
if (isGlobalUboMember(uniform) && uniform.stages == 0) {
|
||||
@@ -1024,6 +1103,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
if (isBufferVariable(uniform)) {
|
||||
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
|
||||
"surface",
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
artifacts.tProgramUniformIndexToGl[i] = static_cast<Int>(artifacts.glUniformIndexToTProgram.size());
|
||||
artifacts.glUniformIndexToTProgram.push_back(i);
|
||||
}
|
||||
@@ -1032,7 +1117,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.activeUniformCount, tProgramUniformCount);
|
||||
|
||||
// Effective explicit location per TProgram uniform, from two sources:
|
||||
// - the lexical side-channel for default-block uniforms - the relaxed parse
|
||||
// - the parse-time snapshot for default-block uniforms - the relaxed parse
|
||||
// dropped their layout(location = N) qualifiers when collecting them into
|
||||
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
||||
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
|
||||
@@ -1062,8 +1147,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
if (inNamedBlock) continue; // block members never take glUniform locations
|
||||
if (isNamedBlockMember(uniform)) continue; // block members never take glUniform locations
|
||||
|
||||
if (const Int* explicitLocation = findExplicitLocation(uniform.name)) {
|
||||
effectiveLocation[i] = static_cast<Uint>(*explicitLocation);
|
||||
@@ -1138,20 +1222,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
|
||||
}
|
||||
|
||||
// Counts ONLY default-block uniforms, which is the whole of what a GL uniform location
|
||||
// is and the whole of what GL_MAX_UNIFORM_LOCATIONS bounds (GL 4.6 core 7.6.1). A
|
||||
// named-block member used to be counted here too and used to be handed a location by the
|
||||
// first-fit pass below, which is a spec violation twice over: glGetUniformLocation must
|
||||
// answer -1 for it (glGetProgramResourceLocation already did), and every slot it took
|
||||
// pushed a real default-block uniform one location further up. On a program with a
|
||||
// buffer block that is exactly how a location EQUAL to the advertised maximum got minted
|
||||
// - the table's ceiling is raised to hold this count, so one extra block member raised it
|
||||
// to MAX and the first-fit pass then filled the last slot
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit-max, whose compute
|
||||
// program carries an SSBO; its -max-array sibling ran the pool out and failed to link).
|
||||
Int requiredUniformLocations = deadReservedLocationCount;
|
||||
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
|
||||
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
|
||||
// named-block members, which take a slot in this allocator's table (an implementation
|
||||
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
|
||||
// fail a link the spec allows.
|
||||
Int defaultBlockLocationDemand = deadReservedLocationCount;
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
auto& uniform = artifacts.program->getUniform(i);
|
||||
const Uint location = effectiveLocation[i];
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
requiredUniformLocations += locationSpan;
|
||||
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
|
||||
if (!isNamedBlockMember(uniform)) requiredUniformLocations += locationSpan;
|
||||
if (location != kNoLocation) {
|
||||
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||
}
|
||||
@@ -1170,11 +1257,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
|
||||
// A single uniform whose own span passes the ceiling was already rejected above; this is
|
||||
// the aggregate half of the same rule.
|
||||
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
|
||||
if (requiredUniformLocations > static_cast<Int>(kMaxUniformLocations)) {
|
||||
artifacts.infoLog =
|
||||
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
|
||||
"GL_MAX_UNIFORM_LOCATIONS is {}.",
|
||||
defaultBlockLocationDemand, kMaxUniformLocations);
|
||||
requiredUniformLocations, kMaxUniformLocations);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
@@ -1247,6 +1334,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// is demoted to the first-fit pass below instead of failing the link.
|
||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||
auto& uniform = artifacts.program->getUniform(i);
|
||||
// Same rule the effective-location loop applies: a block member has no GL location,
|
||||
// so it must not reach the first-fit pass either. Its uniformLocations entry stays
|
||||
// at kNoLocation, which glGetUniformLocation reads back as the -1 the spec wants.
|
||||
if (isNamedBlockMember(uniform)) continue;
|
||||
if (locationIsSourceExplicit[i]) continue;
|
||||
const Uint location = effectiveLocation[i];
|
||||
if (location == kNoLocation) {
|
||||
@@ -1453,14 +1544,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
// ---------- UBO ----------
|
||||
// GL-visible blocks only (MGL_GLOBAL_UBO was filtered out above).
|
||||
// The BLOCK space (MGL_GLOBAL_UBO was filtered out above, storage and atomic counter
|
||||
// blocks were not): these tables are what the backends index, and what the GL
|
||||
// uniform-block entry points reach after translating out of the GL_UNIFORM_BLOCK space.
|
||||
const Int uboCount = static_cast<Int>(artifacts.glBlockIndexToTProgram.size());
|
||||
MGLOG_D("ProgramObject %u: Reflection - uniform block count (UBO) = %d", in.externalIndex, uboCount);
|
||||
artifacts.uniformBlockBinding.resize(uboCount, -1);
|
||||
for (Int i = 0; i < uboCount; i++) {
|
||||
auto& ubo = artifacts.program->getUniformBlock(artifacts.glBlockIndexToTProgram[i]);
|
||||
artifacts.uniformBlockNameMaxLength =
|
||||
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length());
|
||||
// GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH is measured over the names
|
||||
// glGetActiveUniformBlockName can report, so only the GL uniform blocks count -
|
||||
// a long storage-block name must not size the caller's buffer.
|
||||
if (artifacts.blockIndexToGlUniformBlock[i] >= 0) {
|
||||
artifacts.uniformBlockNameMaxLength =
|
||||
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length());
|
||||
}
|
||||
artifacts.uniformBlockIndexByName[ubo.name] = i;
|
||||
// if there's binding defined in shader as layout(binding = ...),
|
||||
// retrieve it here.
|
||||
@@ -1505,6 +1603,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (Int i = 0; i < blockCount; ++i) {
|
||||
artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i)));
|
||||
}
|
||||
SeedDefaultStorageBlockBindings();
|
||||
|
||||
const Int uniformCount = program.getNumUniformVariables();
|
||||
artifacts.uniformReflection.clear();
|
||||
@@ -1553,6 +1652,67 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size());
|
||||
}
|
||||
|
||||
// GL 4.3 core 7.8: a shader storage block declared without a layout(binding = N) qualifier
|
||||
// has a buffer binding of ZERO. MobileGL could not report that, because by the time this
|
||||
// reflection is built the number in the block's qualifier is one glslang INVENTED.
|
||||
//
|
||||
// Every shader is parsed as a Vulkan client, so glslang's IO mapper takes the `set = openGl
|
||||
// ? resource : ent.newSet` branch with openGl == 0 (iomapper.cpp resolveBinding) - i.e. it
|
||||
// allocates out of ONE flat binding space shared by every sampler, image, uniform block,
|
||||
// storage block and the synthesized MGL_GLOBAL_UBO - and then writes the result back into
|
||||
// the type's qualifier (iomapper.cpp, `base->getWritableType().getQualifier().layoutBinding =
|
||||
// at->second.newBinding`). getBinding() therefore answers with the auto-assigned slot and
|
||||
// cannot be distinguished from a declared one. An unqualified block lands on 0 only when
|
||||
// nothing else in the program claimed 0 first, which is why a lone storage block in a
|
||||
// trivial shader looked correct and KHR-GL43.compute_shader.resource-ubo - whose shader also
|
||||
// declares twelve uniform blocks - wrote everything to a binding nothing was bound at.
|
||||
//
|
||||
// THE FLAT SPACE IS LEFT ALONE. It is load-bearing: DirectVulkan indexes bindingKinds[],
|
||||
// uniformBlockIndexByBinding[] and storageBlockIndexByBinding[] by that one number and
|
||||
// asserts when two resources collide on it, so forcing the SPIR-V decoration to 0 would
|
||||
// collide an unqualified block with the global UBO and take working programs down. What is
|
||||
// repaired is the GL-VISIBLE binding, through the record GL already has for exactly this -
|
||||
// the same per-name map glShaderStorageBlockBinding writes, which both backends already
|
||||
// consult (ProgramInterface's GL_BUFFER_BINDING, DirectGLES's SPIRV-Cross binding rewrite,
|
||||
// DirectVulkan's GetShaderStorageBlockBinding). Seeding it here means the default and a
|
||||
// later rebind travel the same path, and basic-noBindingLayout - which rebinds all three of
|
||||
// its unqualified blocks - keeps working because a rebind simply overwrites the seed.
|
||||
//
|
||||
// Seeded INSIDE `artifacts`, so an L1 translation-cache hit that republishes the artifacts
|
||||
// wholesale carries it too; a seed applied outside them would silently vanish on a hit.
|
||||
//
|
||||
// The blocks are named by TMglGlslIoResolver at mapIO's collect callback, which runs over
|
||||
// every declared block of every stage BEFORE the write-back above happens - so "declared no
|
||||
// binding" is a fact read off the AST, not a guess made about the text. The lexical scanner
|
||||
// this replaced could only report positively, dropping any declaration whose grammar it did
|
||||
// not fully recognise, and could not read `binding = SOME_MACRO` at all (it ran on
|
||||
// macro-unexpanded source, and reading "no literal" as "no binding" once aliased eight
|
||||
// Flywheel storage blocks onto 0).
|
||||
//
|
||||
// THE COLLISION IS DELIBERATE, and it is GL's. Several unqualified blocks all default to 0
|
||||
// and alias there until the application rebinds them; a real GL driver does the same, which
|
||||
// is why every program that has more than one either rebinds or uses one of them.
|
||||
// basic-noBindingLayout is that regression test - it rebinds all three of its blocks
|
||||
// immediately after linking, and the DirectGLES transpile is lazy (first use, not link), so
|
||||
// the ESSL it eventually emits already carries the rebound 0/1/2 and never the aliased seed.
|
||||
// What this replaces was not a safer arrangement, only an accidental one: the three blocks
|
||||
// got glslang's 0/1/2 and an application that rebound them to anything else still wrote to
|
||||
// the wrong buffers.
|
||||
void ProgramLinkTask::SeedDefaultStorageBlockBindings() {
|
||||
if (artifacts.storageBlocksWithoutBinding.empty()) return;
|
||||
for (const ProgramObject::BlockReflection& block : artifacts.blockReflection) {
|
||||
if (!block.type.isBuffer) continue;
|
||||
// An instance array reflects as "B[0]", "B[1]", ... and each element is its own GL
|
||||
// resource with its own binding; the scanner keys on the block TYPE name, so the
|
||||
// subscript is stripped before the lookup. GL gives element k of an unqualified
|
||||
// array binding 0 + k, the same base + element rule a declared binding follows.
|
||||
const String base = StripArrayElementSuffix(block.name);
|
||||
if (!artifacts.storageBlocksWithoutBinding.contains(base)) continue;
|
||||
// First writer wins: never overwrite a binding the application has already chosen.
|
||||
artifacts.shaderStorageBlockBinding.emplace(block.name, BlockArrayElement(block.name));
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||
if (!artifacts.program) return false;
|
||||
// The pipe-output list is the output interface of the program's LAST stage. Only a
|
||||
|
||||
@@ -120,10 +120,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||
// A, and not by phase B - two reasons, both structural:
|
||||
// * the key covers the four link-time request maps and the merged opaque
|
||||
// bindings, and one of those (explicitOpaqueUniformBindings) lives in
|
||||
// `artifacts`, which phase B is forbidden to read because the GL-thread join
|
||||
// moves it out from under phase B;
|
||||
// * the key covers the three link-time request maps, which live in `in` - and
|
||||
// it has to be built before the link, because a hit is what makes the link
|
||||
// unnecessary;
|
||||
// * built once, it serves both the lookup and the insert, so the program's
|
||||
// sources are copied into the blob exactly once per link.
|
||||
// Invalid (null blob) when the cache is disabled, or when a stage arrived
|
||||
@@ -168,10 +167,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// with any other, and an attached shader that failed to compile. Split out of
|
||||
// ConsumeShaders so they still run - in the same order, with the same diagnostics -
|
||||
// BEFORE the L1 memo is consulted, rather than behind a hit that would skip them.
|
||||
// The two lexical side channels the relaxed parse cannot provide, merged across
|
||||
// stages. Reads the compile snapshots only, so it runs before any parse - the merged
|
||||
// opaque bindings are part of the L1 memo key. Sets artifacts.infoLog and leaves
|
||||
// linkStatus false when two stages disagree on an explicit uniform location.
|
||||
// Merges the per-stage explicit default-block uniform locations glslang recorded at
|
||||
// compile time. Reads the compile snapshots only, so it runs before any parse - and
|
||||
// before the L1 memo, so a hit can never paper over a program that must fail to link.
|
||||
// Sets artifacts.infoLog and leaves linkStatus false when two stages disagree on an
|
||||
// explicit uniform location.
|
||||
void MergeShaderSideChannels();
|
||||
Bool ValidateAttachedShaders();
|
||||
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
|
||||
@@ -189,6 +189,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Copies every reflection record the GL query surface reads out of the glslang
|
||||
// TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection.
|
||||
void SnapshotGlslangReflection();
|
||||
// Gives every storage block whose shader declared no layout(binding = N) the binding
|
||||
// GL 4.3 core 7.8 says it has - zero - because glslang's IO mapper has by then invented
|
||||
// one and overwritten the qualifier. See the definition for why the invented binding is
|
||||
// deliberately left in place for the backends' own use.
|
||||
void SeedDefaultStorageBlockBindings();
|
||||
Bool ValidateFragmentOutputLocations();
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||
|
||||
@@ -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 {
|
||||
@@ -331,6 +350,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.tProgramUniformIndexToGl.clear();
|
||||
artifacts.glBlockIndexToTProgram.clear();
|
||||
artifacts.tProgramBlockIndexToGl.clear();
|
||||
artifacts.glUniformBlockIndexToBlock.clear();
|
||||
artifacts.blockIndexToGlUniformBlock.clear();
|
||||
artifacts.linkedExplicitUniformLocations.clear();
|
||||
artifacts.uniformInitialValues.clear();
|
||||
artifacts.uniformIndexInTProgram.clear();
|
||||
@@ -344,6 +365,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.uniformBlockIndexByName.clear();
|
||||
artifacts.uniformBlockBinding.clear();
|
||||
artifacts.shaderStorageBlockBinding.clear();
|
||||
// Cleared with it: the seed above is re-derived from the newly attached shaders on every
|
||||
// link, so a stale set would otherwise default a block the new sources do declare a
|
||||
// binding for.
|
||||
artifacts.storageBlocksWithoutBinding.clear();
|
||||
artifacts.attribs.clear();
|
||||
artifacts.attribTypes.clear();
|
||||
artifacts.activeUniformCount = 0;
|
||||
|
||||
@@ -155,6 +155,46 @@ namespace MobileGL::MG_State::GLState {
|
||||
// The last link's full input set; empty when this program has never linked (or its
|
||||
// last link had no shaders attached). GL-thread-owned, rebuilt in Link()'s prologue.
|
||||
const Vector<LinkedShaderRef>& GetLinkedShaderSnapshot() const { return m_linkedShaderSnapshot; }
|
||||
// "Does this program's EXECUTABLE have this stage" - the only form of the question a
|
||||
// draw may ask. GetShaderIndexByStage answers it of the live attach list, which by the
|
||||
// rule above is a different set: glAttachShader adds to that list immediately while
|
||||
// leaving the executable (and LINK_STATUS) alone, and glDetachShader defers the removal
|
||||
// to the next Link(), so between an attach and the relink the two disagree in both
|
||||
// directions. A draw-time stage test that reads the live list therefore starts rejecting
|
||||
// draws GL requires to execute, against an executable that does not carry the stage at
|
||||
// all - and stays wrong until the application happens to relink.
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
return std::any_of(m_linkedShaderSnapshot.begin(), m_linkedShaderSnapshot.end(),
|
||||
[stage](const LinkedShaderRef& ref) {
|
||||
return ref.shader && ref.shader->GetShaderStage() == stage;
|
||||
});
|
||||
}
|
||||
// The stage of each module of GetGeneratedSpirv(), at the SAME index and with the same
|
||||
// size: phase B emits exactly one module per entry of the snapshot above, in that order
|
||||
// (Link() fills ProgramLinkTask::in.shaders from the snapshot loop, phase A copies the
|
||||
// stages straight across into SpirvHandoff::shaderTypes, and GetSpirvBinaryFromProgram
|
||||
// walks that list). This - never GetAttachedShaders() - is what a consumer of the
|
||||
// generated SPIR-V must size its loop by and index alongside.
|
||||
//
|
||||
// The two lists are NOT interchangeable and cannot be made so: the attach list is live
|
||||
// and the SPIR-V is a link artifact, so a glAttachShader after a link grows one and not
|
||||
// the other, with no link in between at which they could be reconciled. A loop that runs
|
||||
// over the attach list and indexes the SPIR-V therefore reads off the end of it - which
|
||||
// is a plain out-of-bounds Vector read, not a wrong answer.
|
||||
//
|
||||
// Deliberately a Vector<ShaderStage> and not the shader objects: every consumer wants
|
||||
// only the stage, and a distinct type is what makes handing it the attach list by
|
||||
// mistake a compile error rather than a segfault. Built on demand because these callers
|
||||
// are program-BUILD paths (a backend rebuild, a pipeline cache miss), each of which then
|
||||
// spends milliseconds compiling the very modules this indexes.
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
Vector<ShaderStage> stages;
|
||||
stages.reserve(m_linkedShaderSnapshot.size());
|
||||
for (const LinkedShaderRef& ref : m_linkedShaderSnapshot) {
|
||||
stages.push_back(ref.shader ? ref.shader->GetShaderStage() : ShaderStage::Unknown);
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
// Pipeline-composite attach: AttachShader plus a pin that makes THIS program's
|
||||
// Link() consume ref's (source, node) instead of the shader's current ones, so a
|
||||
// post-link recompile of the stage program's shader cannot leak into the composite.
|
||||
@@ -239,12 +279,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
|
||||
return Artifacts().tProgramUniformIndexToGl[tIndex];
|
||||
}
|
||||
// GL uniform-block index -> glslang TProgram block index (the inverse of
|
||||
// Block index -> glslang TProgram block index (the inverse of
|
||||
// GlBlockIndexFromTProgram). The interface-query layer needs it to reach block
|
||||
// properties glslang exposes but no typed getter here does.
|
||||
Int TProgramBlockIndex(Uint glBlockIndex) const {
|
||||
return glBlockIndex < Artifacts().glBlockIndexToTProgram.size()
|
||||
? Artifacts().glBlockIndexToTProgram[glBlockIndex]
|
||||
Int TProgramBlockIndex(Uint blockIndex) const {
|
||||
return blockIndex < Artifacts().glBlockIndexToTProgram.size()
|
||||
? Artifacts().glBlockIndexToTProgram[blockIndex]
|
||||
: -1;
|
||||
}
|
||||
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
|
||||
@@ -252,6 +292,41 @@ namespace MobileGL::MG_State::GLState {
|
||||
return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
|
||||
}
|
||||
|
||||
// ---- GL_UNIFORM_BLOCK index <-> block index translation ----
|
||||
// The block index space above carries the storage blocks and the synthesized atomic
|
||||
// counter blocks as well; GL_ACTIVE_UNIFORM_BLOCKS counts only actual uniform blocks
|
||||
// (GL 4.6 core 7.6). Every glGetActiveUniformBlock* / glGetUniformBlockIndex /
|
||||
// glUniformBlockBinding entry point speaks THIS space and translates into the block
|
||||
// space before touching any of the block-keyed tables; the backends keep speaking the
|
||||
// block space directly. See LinkArtifacts::glUniformBlockIndexToBlock.
|
||||
Int GetGlUniformBlockCount() const {
|
||||
return static_cast<Int>(Artifacts().glUniformBlockIndexToBlock.size());
|
||||
}
|
||||
Bool IsActiveGlUniformBlock(Uint glUniformBlockIndex) const {
|
||||
return glUniformBlockIndex < Artifacts().glUniformBlockIndexToBlock.size();
|
||||
}
|
||||
Int BlockIndexFromGlUniformBlock(Uint glUniformBlockIndex) const {
|
||||
return glUniformBlockIndex < Artifacts().glUniformBlockIndexToBlock.size()
|
||||
? Artifacts().glUniformBlockIndexToBlock[glUniformBlockIndex]
|
||||
: -1;
|
||||
}
|
||||
Int GlUniformBlockIndexFromBlock(Int blockIndex) const {
|
||||
if (blockIndex < 0 || blockIndex >= static_cast<Int>(Artifacts().blockIndexToGlUniformBlock.size())) {
|
||||
return -1;
|
||||
}
|
||||
return Artifacts().blockIndexToGlUniformBlock[blockIndex];
|
||||
}
|
||||
// glGetUniformBlockIndex: GL_INVALID_INDEX for a name that is not an active UNIFORM
|
||||
// block, which includes every storage block and every atomic counter block even though
|
||||
// GetUniformBlockIndex() below resolves them (it answers in the block space, which the
|
||||
// backends need to keep reaching them by name).
|
||||
Uint GetGlUniformBlockIndex(const char* name) const {
|
||||
const Uint blockIndex = GetUniformBlockIndex(name);
|
||||
if (blockIndex == 0xFFFFFFFFu) return 0xFFFFFFFFu;
|
||||
const Int glIndex = GlUniformBlockIndexFromBlock(static_cast<Int>(blockIndex));
|
||||
return glIndex < 0 ? 0xFFFFFFFFu : static_cast<Uint>(glIndex);
|
||||
}
|
||||
|
||||
Int GetActiveUniformIndex(const String& name) const {
|
||||
// uniformIndexByName is keyed by the REFLECTED name, so a lookup that hits is
|
||||
// already the exact-match the old code re-verified with a string compare after
|
||||
@@ -300,7 +375,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
return GetUniformArraySizeByTIndex(TProgramUniformIndex(index));
|
||||
}
|
||||
|
||||
Int GetActiveUniformBlockIndex(Uint index) const {
|
||||
// The BLOCK index of the block owning this active uniform, or -1 when it owns none as
|
||||
// far as GL is concerned. Internal: pair it with another block-space index, never with
|
||||
// a GL_UNIFORM_BLOCK one (GetActiveUniformBlockIndex below is that one).
|
||||
Int GetActiveUniformOwnerBlockIndex(Uint index) const {
|
||||
// An atomic counter is a DEFAULT-BLOCK uniform to GL, whatever block the
|
||||
// transpiler lowered it onto (GL 4.6 core 7.6, table 7.6): -1.
|
||||
if (IsActiveUniformAtomicCounter(index)) return -1;
|
||||
@@ -308,6 +386,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
return GlBlockIndexFromTProgram(UniformAt(TProgramUniformIndex(index)).index);
|
||||
}
|
||||
|
||||
// GL_UNIFORM_BLOCK_INDEX: an index into the GL_ACTIVE_UNIFORM_BLOCKS list, or -1. A
|
||||
// buffer variable owns a storage block, which is not in that list, so it answers -1 too
|
||||
// (and after the enumeration filter it is not an active uniform in the first place).
|
||||
Int GetActiveUniformBlockIndex(Uint index) const {
|
||||
return GlUniformBlockIndexFromBlock(GetActiveUniformOwnerBlockIndex(index));
|
||||
}
|
||||
|
||||
// The transpiler lowers every atomic_uint onto a synthesized gl_AtomicCounterBlock_N
|
||||
// block, but GL keeps seeing an atomic counter as a default-block uniform of type
|
||||
// GL_UNSIGNED_INT_ATOMIC_COUNTER that points at an atomic-counter BUFFER. These two
|
||||
@@ -373,6 +458,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||
if (!uniform.type.isArray) return 0;
|
||||
// An atomic counter reaches the std140 branch below only because the transpiler
|
||||
// lowered it onto a synthesized block; the buffer it actually addresses is an
|
||||
// ATOMIC COUNTER buffer, whose elements are tightly packed uints (GL 4.6 core 7.6:
|
||||
// "each counter is a single 4-byte value"). Its array stride is therefore 4, not the
|
||||
// vec4 round-up std140 would apply
|
||||
// (KHR-GL43.shader_atomic_counters.basic-program-query wants 4 for ac_counter67[0]).
|
||||
if (IsActiveUniformAtomicCounter(index)) return 4;
|
||||
if (uniform.type.isMatrix) {
|
||||
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
||||
const int vectors = rowMajor ? uniform.type.matrixRows : uniform.type.matrixCols;
|
||||
@@ -473,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 ----
|
||||
@@ -828,14 +941,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
Int GetActiveAttributesCount() const {
|
||||
return static_cast<Int>(Artifacts().pipeInputReflection.size());
|
||||
}
|
||||
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
||||
// materializes for default-block uniforms is filtered out by DoReflection.
|
||||
// Size of the BLOCK index space - every block the relaxed parse produced except the
|
||||
// synthesized MGL_GLOBAL_UBO, which DoReflection filters out. NOT the answer to
|
||||
// glGetProgramiv(GL_ACTIVE_UNIFORM_BLOCKS): storage blocks and atomic counter blocks
|
||||
// live in here too, and GetGlUniformBlockCount() is the one that excludes them.
|
||||
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
||||
GLuint GetComputeLocalSize(Uint dim) const {
|
||||
return dim < 3u ? Artifacts().computeLocalSize[dim] : 0u;
|
||||
}
|
||||
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
||||
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
||||
// Answers in the BLOCK space, so it resolves storage and atomic counter blocks too -
|
||||
// the backends reach those by name. glGetUniformBlockIndex must NOT: use
|
||||
// GetGlUniformBlockIndex() for the GL entry point.
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
auto it = Artifacts().uniformBlockIndexByName.find(name);
|
||||
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
|
||||
@@ -846,12 +964,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
|
||||
return 0xFFFFFFFFu; // GL_INVALID_INDEX
|
||||
}
|
||||
Bool IsActiveUniformBlock(Uint index) const {
|
||||
if (index >= GetActiveUniformBlocksCount()) return false;
|
||||
return true;
|
||||
}
|
||||
// Takes a BLOCK index. The GL entry points validate their argument against the
|
||||
// GL_UNIFORM_BLOCK space with IsActiveGlUniformBlock() first and translate; the bound
|
||||
// test here is only the range of the space this index actually lives in.
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsActiveUniformBlock(index)) return 0;
|
||||
if (index >= Artifacts().glBlockIndexToTProgram.size()) return 0;
|
||||
// glslang reports the unpadded end offset of the last member, but a std140 block
|
||||
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
||||
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
||||
@@ -881,11 +998,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
|
||||
// (glslang's numMembers counts declared members, which diverges from the reflected
|
||||
// entry list for struct arrays and arrayed block instances).
|
||||
// Takes a BLOCK index, and scans in the block space: GetUniformBlockMemberOwnerIndex
|
||||
// answers there, so pairing it with the GL_UNIFORM_BLOCK-space
|
||||
// GetActiveUniformBlockIndex would compare two different numberings.
|
||||
Int GetUniformBlockActiveUniformCount(Uint index) const {
|
||||
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
|
||||
Int count = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) {
|
||||
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
|
||||
if (GetActiveUniformOwnerBlockIndex(uniformIndex) == ownerIndex) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -1084,9 +1204,32 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Int> tProgramUniformIndexToGl;
|
||||
Vector<Int> glBlockIndexToTProgram;
|
||||
Vector<Int> tProgramBlockIndexToGl;
|
||||
// Per-link merged snapshot of the attached shaders' lexically extracted
|
||||
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
|
||||
// them from reflection; the DoReflection assigner restores them from here).
|
||||
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
|
||||
// glBlockIndexToTProgram above.
|
||||
//
|
||||
// That list is the BLOCK space - everything the relaxed parse produced except
|
||||
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
|
||||
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
|
||||
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
|
||||
// through indexToUniformBlock too, and the list therefore also carries every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
|
||||
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
|
||||
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
|
||||
//
|
||||
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
|
||||
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
|
||||
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
|
||||
// DirectGLES.cpp), so compacting that list would renumber every backend binding
|
||||
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
|
||||
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||
// DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
@@ -1111,6 +1254,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Int> uniformIndexInTProgram;
|
||||
// ditto. Will be set at glUniform1i
|
||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||
// Sampler/image layout(binding = N) initial texture/image units, captured by
|
||||
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
|
||||
// qualifier still says what the shader declared. An OUTPUT of the link, not an
|
||||
// input to it: nothing supplies this map, the resolver fills it.
|
||||
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||
|
||||
// Ordered by uniform block index
|
||||
@@ -1125,7 +1272,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
Uint maxUniformLocation = 0;
|
||||
@@ -1183,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
|
||||
|
||||
@@ -154,10 +154,16 @@ namespace {
|
||||
}
|
||||
|
||||
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
|
||||
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
||||
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
||||
// Deliberately free of any per-object state so the memo is sound.
|
||||
// environment snapshot: preprocessing and the three lexical rejections. Split out so P0b
|
||||
// layer 2 can memoize exactly this and nothing else - the glslang parse stays per-object
|
||||
// because its TShader is consume-once. Deliberately free of any per-object state so the
|
||||
// memo is sound.
|
||||
//
|
||||
// The side-channel EXTRACTIONS that used to live here are gone: what the relaxed parse
|
||||
// destroys is now recovered from glslang itself, at the two points where it is destroyed
|
||||
// (see ShaderCompileArtifacts::explicitUniformLocations and
|
||||
// TMglGlslIoResolver::reserverResourceSlot). They could not stay here anyway - none of
|
||||
// them is a function of the unexpanded source text, which is all this half can see.
|
||||
//
|
||||
// The compute local-size verdict reads `env` rather than the live backend, and
|
||||
// env.fingerprint is part of the P0b cache key, so a memo can never be returned against
|
||||
@@ -195,22 +201,21 @@ namespace {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (const std::optional<String> counterOffsetError =
|
||||
FindAtomicCounterOffsetViolation(result.preprocessedSource)) {
|
||||
result.outcome = ShaderPreprocessOutcome::AtomicCounterOffsetRejected;
|
||||
result.infoLog = *counterOffsetError;
|
||||
return result;
|
||||
}
|
||||
// NO ATOMIC-COUNTER OFFSET SCAN HERE ANY MORE: glslang raises both rules itself now, at
|
||||
// the site where its relaxed remap folds the counter into a synthesized block
|
||||
// (ParseHelper.cpp atomicCounterOffsetCheck, called from vkRelaxedRemapUniformVariable).
|
||||
// A violation is an ordinary parse failure, so it reaches GL through the same path every
|
||||
// other compile error does - and, unlike a scan of unexpanded text, it sees an offset
|
||||
// spelled as a macro or a const expression.
|
||||
|
||||
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
||||
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
||||
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
||||
// parse. The GL frontend semantics the relaxed parse cannot provide are restored
|
||||
// on top: explicit default-block uniform locations through the lexical
|
||||
// side-channels below, dead-uniform/global-UBO filtering in
|
||||
// on top, all of them out of glslang: explicit default-block uniform locations from
|
||||
// the snapshot the parse takes, opaque bindings and unqualified storage blocks from
|
||||
// the IO mapper's collect callback, dead-uniform/global-UBO filtering in
|
||||
// ProgramObject::DoReflection.
|
||||
result.explicitUniformLocations = ExtractExplicitUniformLocations(result.preprocessedSource);
|
||||
result.explicitOpaqueBindings = ExtractExplicitOpaqueBindings(result.preprocessedSource);
|
||||
result.outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||
return result;
|
||||
}
|
||||
@@ -313,10 +318,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool parsedOk = false;
|
||||
String parseLog;
|
||||
SharedPtr<glslang::TShader> parsedShader;
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
|
||||
if (verdict) {
|
||||
parsedOk = verdict->parsed;
|
||||
parseLog = verdict->infoLog;
|
||||
// From the verdict, not from a parse - see ShaderParseVerdict for why they had to
|
||||
// move into the payload when their origin moved into glslang.
|
||||
explicitUniformLocations = verdict->explicitUniformLocations;
|
||||
MGLOG_D("ShaderCompileTask: shader %u (stage %d) L1c hit - the glslang parse was skipped; "
|
||||
"compileStatus = %d",
|
||||
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
|
||||
@@ -329,6 +338,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
parsedOk = result.has_value();
|
||||
if (parsedOk) {
|
||||
parsedShader = result.value();
|
||||
explicitUniformLocations = CollectExplicitUniformLocations(*parsedShader);
|
||||
} else {
|
||||
parseLog = result.error().log;
|
||||
}
|
||||
@@ -338,6 +348,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Empty on success by construction, matching what the publish below does with
|
||||
// the artifacts' own log; the diagnostic the application reads on failure.
|
||||
freshVerdict->infoLog = parseLog;
|
||||
freshVerdict->explicitUniformLocations = explicitUniformLocations;
|
||||
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
|
||||
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
|
||||
verdictBytes);
|
||||
@@ -353,8 +364,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// `fresh` is about to be handed to the cache. Populated on the hit path too - it
|
||||
// is what ClaimParsedShader's deferred parse consumes.
|
||||
artifacts.preprocessedSource = shared.preprocessedSource;
|
||||
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
||||
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
||||
artifacts.explicitUniformLocations = Move(explicitUniformLocations);
|
||||
artifacts.infoLog.clear();
|
||||
if (shouldPopulateCache) {
|
||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||
@@ -379,8 +389,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (shouldPopulateCache) {
|
||||
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
||||
fresh->infoLog = artifacts.infoLog;
|
||||
fresh->explicitUniformLocations.clear();
|
||||
fresh->explicitOpaqueBindings.clear();
|
||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,16 +60,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
|
||||
// being deterministic across backend-state changes.
|
||||
String preprocessedSource;
|
||||
// The explicit layout(location = N) qualifiers this stage's default-block uniforms
|
||||
// declared, as glslang recorded them at the point its Vulkan-relaxed remap dropped
|
||||
// them (CollectExplicitUniformLocations).
|
||||
//
|
||||
// Populated on the L1c HIT path too, out of the cached verdict rather than out of a
|
||||
// parse - which is why the verdict carries them. Everything else the relaxed parse
|
||||
// destroys is recovered at LINK instead, from the IO mapper's collect callback, and so
|
||||
// has no field here at all.
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||
String infoLog;
|
||||
Bool compileStatus = false;
|
||||
};
|
||||
|
||||
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
|
||||
// work - preprocess, the two lexical rejections, the two lexical extractions, and (unless
|
||||
// the translation memo's compile half already knows the answer) the glslang parse - with
|
||||
// every input it needs owned by the node itself.
|
||||
// work - preprocess, the lexical rejections, and (unless the translation memo's compile
|
||||
// half already knows the answer) the glslang parse plus the explicit-uniform-location
|
||||
// snapshot it yields - with every input it needs owned by the node itself.
|
||||
//
|
||||
// That ownership is the whole point. The node reads no GL-thread state (the source is a
|
||||
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
//
|
||||
// The failure case is covered too: the info log stays queryable because nothing is
|
||||
// cleared. And if the stored TShader already fed a link, the no-op leaves
|
||||
// preprocessedSource and both side-channel maps intact, which is precisely what
|
||||
// preprocessedSource and the explicit-location snapshot intact, which is precisely what
|
||||
// ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed
|
||||
// the next link a fresh parse, the no-op hands it a fresh re-parse of the identical
|
||||
// source instead. Same result, one parse either way.
|
||||
|
||||
@@ -101,17 +101,11 @@ namespace MobileGL {
|
||||
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
|
||||
const String& GetInfoLog() const { return Compiled().infoLog; }
|
||||
// Explicit layout(location = N) qualifiers on this shader's default-block
|
||||
// uniforms, captured lexically at Compile() because the relaxed parse drops
|
||||
// them from reflection (see ExtractExplicitUniformLocations).
|
||||
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
|
||||
// discarded them (see CollectExplicitUniformLocations).
|
||||
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
|
||||
return Compiled().explicitUniformLocations;
|
||||
}
|
||||
// Explicit layout(binding = N) on sampler/image uniforms - their initial
|
||||
// texture/image units - captured lexically for the same reason (see
|
||||
// ExtractExplicitOpaqueBindings).
|
||||
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
|
||||
return Compiled().explicitOpaqueBindings;
|
||||
}
|
||||
Bool GetCompileStatus() const { return Compiled().compileStatus; }
|
||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
|
||||
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
||||
ResourceBindingRejected,
|
||||
// FindAtomicCounterOffsetViolation rejected it: an atomic counter declared a
|
||||
// layout(offset =) that is misaligned or reaches past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.
|
||||
AtomicCounterOffsetRejected,
|
||||
// The source-only half was clean but glslang rejected the preprocessed source.
|
||||
// Memoizing this saves the parse itself on every later object with that source.
|
||||
ParseFailed,
|
||||
@@ -39,13 +36,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
// Everything ShaderObject::Compile() derives from the source text alone, i.e.
|
||||
// everything that is identical for two shader objects holding byte-identical source.
|
||||
//
|
||||
// "The source text alone" is now literally true: the preprocessed text, an accept/reject
|
||||
// verdict, and the log that explains a rejection. Anything that needs to know what the
|
||||
// shader MEANS is derived from the parse instead - see the note on the missing fields.
|
||||
struct ShaderPreprocessResult {
|
||||
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||
// Valid unless the preprocessor itself never ran; kept even for the rejection
|
||||
// outcomes because that is the text the diagnostics refer to.
|
||||
String preprocessedSource;
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||
// NO EXTRACTED SIDE CHANNELS ANY MORE, and their absence is the point. Explicit
|
||||
// uniform locations, explicit opaque bindings and unqualified storage blocks used to be
|
||||
// lexed out of the text here, which meant reading MACRO-UNEXPANDED source: MobileGL's
|
||||
// preprocessor rewrites the text, it does not run the C preprocessor, so
|
||||
// `binding = SOME_MACRO` reached the scanners verbatim. All three now come from
|
||||
// glslang - the first from a snapshot taken inside the parse, the other two from the
|
||||
// IO mapper's collect callback - and none of them is a function of the source text
|
||||
// ALONE any more, which is the only thing this struct is allowed to hold.
|
||||
// The compile info log to publish; empty when outcome == Preprocessed.
|
||||
String infoLog;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,8 +17,16 @@
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_ARRAY_ELEMENT_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_READONLY_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_SPLIT_READ_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
||||
@@ -37,7 +45,18 @@ namespace {
|
||||
return count;
|
||||
}
|
||||
|
||||
// The pass tags the name of every declaration it rewrites with the REPAIR it applied, so the
|
||||
// expectations have to spell the tag that matches how the fixture uses the image.
|
||||
String RoAlias(const String& name) { return String(IMAGE_READONLY_ALIAS_PREFIX) + name; }
|
||||
String WoAlias(const String& name) { return String(IMAGE_WRITEONLY_ALIAS_PREFIX) + name; }
|
||||
String RwAlias(const String& name) { return String(IMAGE_SPLIT_READ_ALIAS_PREFIX) + name; }
|
||||
// The writeonly half is minted from the ALREADY access-tagged name, so it carries both.
|
||||
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
||||
String SplitWriteAlias(const String& name) { return WriteAlias(RwAlias(name)); }
|
||||
// The scalar RemapImageArrayElementUnits declares for one element of a split image array.
|
||||
String Elem(const String& name, Int element) {
|
||||
return String(IMAGE_ARRAY_ELEMENT_PREFIX) + name + "_" + std::to_string(element);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
||||
@@ -61,14 +80,19 @@ void main()
|
||||
// Both halves: same binding, same format, same type - which is what makes two image
|
||||
// variables on one image unit legal - and both `coherent`, which is what makes the store
|
||||
// through one of them visible to the load through the other.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
|
||||
// The load keeps the original name, the store moves to the writeonly half.
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ","));
|
||||
// The load goes to the readonly half, the store to the writeonly one, and neither is called
|
||||
// what the application called it any more.
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ","));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ","));
|
||||
EXPECT_FALSE(Contains(out, "imageStore(goku,"));
|
||||
EXPECT_FALSE(Contains(out, "imageLoad(goku,"));
|
||||
}
|
||||
|
||||
// The split has to survive RemoveLayoutBinding, which runs straight after it: an ES image
|
||||
@@ -98,7 +122,10 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray trunks;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray " +
|
||||
RoAlias("trunks") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("trunks") + ","));
|
||||
EXPECT_FALSE(Contains(out, "writeonly"));
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
EXPECT_EQ(CountOf(out, "image2DArray"), 1u);
|
||||
@@ -113,7 +140,10 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D gohan;"));
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D " + WoAlias("gohan") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("gohan") + ","));
|
||||
EXPECT_FALSE(Contains(out, "readonly"));
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
}
|
||||
@@ -130,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;
|
||||
@@ -139,6 +173,33 @@ void main()
|
||||
{
|
||||
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
|
||||
}
|
||||
)";
|
||||
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);
|
||||
}
|
||||
@@ -154,11 +215,14 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("gohan") + "[3];"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("gohan") + "[3];"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("gohan") + "[1],"));
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("gohan") + "[2],"));
|
||||
}
|
||||
|
||||
// The rewrite is by identifier, not by substring: "goku" must not reach into "goku_hd", and
|
||||
@@ -177,14 +241,19 @@ void main()
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
|
||||
// place, not split, and left non-coherent because nothing aliases it.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(
|
||||
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
||||
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
||||
EXPECT_FALSE(Contains(out, WriteAlias("goku_hd")));
|
||||
// place, not split, and left non-coherent because nothing aliases it. Both are renamed.
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D " +
|
||||
RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " +
|
||||
SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " +
|
||||
WoAlias("goku_hd") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("goku_hd") + ","));
|
||||
EXPECT_FALSE(Contains(out, SplitWriteAlias("goku") + "_hd"));
|
||||
EXPECT_FALSE(Contains(out, SplitWriteAlias("goku_hd")));
|
||||
}
|
||||
|
||||
// Other qualifiers belong to both halves, and the memory qualifier goes where SPIRV-Cross
|
||||
@@ -198,9 +267,11 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D " + RwAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
|
||||
// compile error in ESSL, so the source's own has to be recognized.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
@@ -225,12 +296,13 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
|
||||
// place, has no alias to stay visible to, and must not pay for uncached access.
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D " + WoAlias("storeOnly") + ";")) << out;
|
||||
}
|
||||
|
||||
// The ORDERING half of the split, which `coherent` alone does not buy. Coherent makes the store
|
||||
@@ -253,9 +325,11 @@ void main()
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "imageStore(" + SplitWriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
|
||||
EXPECT_TRUE(
|
||||
Contains(out, "imageStore(" + SplitWriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
|
||||
<< out;
|
||||
// One per store, not one per shader and not one per load.
|
||||
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 2u) << out;
|
||||
@@ -273,7 +347,7 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D " + WoAlias("storeOnly") + ";")) << out;
|
||||
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
|
||||
}
|
||||
|
||||
@@ -289,7 +363,9 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "max(imageLoad(gohan[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();")) << out;
|
||||
EXPECT_TRUE(Contains(out, "max(imageLoad(" + RwAlias("gohan") +
|
||||
"[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();"))
|
||||
<< out;
|
||||
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
|
||||
}
|
||||
|
||||
@@ -340,25 +416,38 @@ void main()
|
||||
}
|
||||
)";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D sizeOnly;"));
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D " +
|
||||
RoAlias("sizeOnly") + ";"))
|
||||
<< out;
|
||||
// The rename has to reach imageSize too, or the declaration and its only use stop agreeing.
|
||||
EXPECT_TRUE(Contains(out, "imageSize(" + RoAlias("sizeOnly") + ")")) << out;
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||
}
|
||||
|
||||
// The alias must not land on an identifier the shader already uses.
|
||||
TEST(SplitReadWriteImageUniformsTest, AliasNameAvoidsAnExistingIdentifier) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 6, rgba8) uniform highp image2D taken;
|
||||
highp vec4 mg_imageWrite_taken;
|
||||
void main()
|
||||
{
|
||||
imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + mg_imageWrite_taken);
|
||||
}
|
||||
)";
|
||||
// Neither minted name may land on an identifier the shader already uses - and there are two of
|
||||
// them now, the access-tagged name of the repaired declaration and the writeonly half built on
|
||||
// top of it. Both collisions are exercised at once.
|
||||
TEST(SplitReadWriteImageUniformsTest, AliasNamesAvoidExistingIdentifiers) {
|
||||
const String stageCollision = RwAlias("taken");
|
||||
const String writeCollision = SplitWriteAlias("taken");
|
||||
const String source = "#version 320 es\n"
|
||||
"layout(binding = 6, rgba8) uniform highp image2D taken;\n"
|
||||
"highp vec4 " +
|
||||
stageCollision + ";\nhighp vec4 " + writeCollision +
|
||||
";\nvoid main()\n{\n"
|
||||
" imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + " +
|
||||
stageCollision + " + " + writeCollision + ");\n}\n";
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_FALSE(Contains(out, "image2D " + WriteAlias("taken") + ";"));
|
||||
EXPECT_TRUE(Contains(out, "image2D " + WriteAlias("taken") + "X;"));
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("taken") + "X,"));
|
||||
EXPECT_TRUE(Contains(out, "+ mg_imageWrite_taken)"));
|
||||
|
||||
EXPECT_FALSE(Contains(out, "image2D " + stageCollision + ";")) << out;
|
||||
EXPECT_FALSE(Contains(out, "image2D " + writeCollision + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + stageCollision + "X;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + writeCollision + "X;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + writeCollision + "X,")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + stageCollision + "X,")) << out;
|
||||
// ...and the globals that forced the suffix are still themselves.
|
||||
EXPECT_TRUE(Contains(out, "highp vec4 " + stageCollision + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "highp vec4 " + writeCollision + ";")) << out;
|
||||
}
|
||||
|
||||
// A use the pass cannot account for (here: the image handed to a user function) means it
|
||||
@@ -372,6 +461,9 @@ void main()
|
||||
imageStore(passed, ivec2(0), helper(passed));
|
||||
}
|
||||
)";
|
||||
// Declining means declining EVERYTHING: no qualifier, and no rename either. A rename that
|
||||
// moved the declaration but not the use inside helper() would be a compile error rather than
|
||||
// the wrong-but-compiling shader this pass refuses to guess at.
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
@@ -387,6 +479,468 @@ void main()
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
// The defect the rename exists for. The pass sees ONE stage at a time and picks the memory
|
||||
// qualifier from the accesses in THAT stage, so a vertex shader that only stores and a fragment
|
||||
// shader that only loads the same image came out `writeonly g_image` and `readonly g_image` -
|
||||
// two declarations of one uniform name that GLSL requires to be identical. Adreno merges them
|
||||
// and silently discards the vertex-stage stores (advanced-memory-dependentInvocation reads back
|
||||
// the untouched zeros, with LINK_STATUS = 1 and an empty driver log). Tagging by the repair
|
||||
// leaves nothing to merge.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageDifferentlyGetDifferentNames) {
|
||||
const String vertexSource = R"(#version 320 es
|
||||
layout(binding = 0, rgba32f) uniform coherent highp image2D g_image;
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image, ivec2(0), vec4(1.0));
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String fragmentSource = R"(#version 320 es
|
||||
layout(binding = 0, rgba32f) uniform coherent 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 writeonly coherent highp image2D " + vsName + ";")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "uniform readonly coherent highp image2D " + fsName + ";")) << fsOut;
|
||||
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
|
||||
// The whole point: after the rewrite the two stages no longer declare a common name, so
|
||||
// there is nothing for a linker to merge and mis-qualify.
|
||||
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
|
||||
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
|
||||
// Both bindings are untouched - the image unit is still the same one.
|
||||
EXPECT_TRUE(Contains(vsOut, "binding = 0"));
|
||||
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
|
||||
// multiplied a program's image-uniform count by the number of stages that mention it - which is
|
||||
// how the five stages of KHR-GL43.shading_language_420pack.binding_images_texture_type_* went
|
||||
// from 6 image uniforms to 30 and drew "Error: Image Image location or component exceeds max
|
||||
// allowed." out of the Adreno 830 linker, with LINK_STATUS = TRUE already published by the
|
||||
// frontend and every draw silently doing nothing.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesThatUseAnImageAlikeShareOneName) {
|
||||
const String vertexSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba8) uniform highp image2D goku;
|
||||
void main()
|
||||
{
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String fragmentSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba8) uniform highp image2D goku;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
mg_FragColor = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
||||
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
||||
|
||||
// One name, arrived at independently by two different stages, so the linker merges them
|
||||
// back into the single image uniform the application declared.
|
||||
for (const String& out : {vsOut, fsOut}) {
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + SplitWriteAlias("goku") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RwAlias("goku") + ",")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + SplitWriteAlias("goku") + ",")) << out;
|
||||
}
|
||||
}
|
||||
|
||||
// One tag per repair, all three distinct, and each a legal identifier stem.
|
||||
TEST(SplitReadWriteImageUniformsTest, EveryAccessTagIsDistinct) {
|
||||
const String prefixes[] = {String(IMAGE_READONLY_ALIAS_PREFIX), String(IMAGE_WRITEONLY_ALIAS_PREFIX),
|
||||
String(IMAGE_SPLIT_READ_ALIAS_PREFIX), String(IMAGE_WRITE_ALIAS_PREFIX)};
|
||||
Vector<String> seenPrefixes;
|
||||
for (const String& prefix : prefixes) {
|
||||
// A GLSL identifier may not contain "__" (GLSL ES 3.20 3.7), and the prefix is glued
|
||||
// straight onto a name that may itself start with '_'.
|
||||
EXPECT_EQ(prefix.find("__"), String::npos) << prefix;
|
||||
for (const String& seen : seenPrefixes) {
|
||||
EXPECT_NE(seen, prefix) << prefix;
|
||||
// Nor may one be a prefix of another: the write half is minted on top of an
|
||||
// already-tagged name, so a shared stem would let two repairs collide.
|
||||
EXPECT_NE(prefix.rfind(seen, 0), 0u) << prefix << " vs " << seen;
|
||||
}
|
||||
seenPrefixes.push_back(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// RemapImageArrayElementUnits
|
||||
//
|
||||
// ES takes an image unit only from layout(binding=N), and one declaration carries one of them,
|
||||
// so an image array's elements land on N, N+1, N+2, ... Desktop GL lets an application point
|
||||
// each element wherever it likes with glUniform1i, which ES makes an INVALID_OPERATION on an
|
||||
// image uniform - there is no API side to fix, so the emitted text has to carry it.
|
||||
|
||||
namespace {
|
||||
// The advanced-sso-simple shape: a four-element image array on units 0, 2, 4, 6. The
|
||||
// subscripts are literals because LegalizeResourceArrayIndexingForEssl has already folded
|
||||
// the conformance case's `for (int i = 0; i < g_image.length(); ++i)` - ESSL forbids a
|
||||
// non-constant image-array subscript outright, so a loop counter never reaches this pass.
|
||||
const char* const kSsoImageArrayFS = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[1], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[2], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
ImageArrayUnitPlan Plan(const String& name, const Vector<Int>& units) {
|
||||
ImageArrayUnitPlan plan;
|
||||
plan.name = name;
|
||||
plan.units = units;
|
||||
return plan;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The defect, end to end. Elements 0..3 need units 0, 2, 4, 6, so the array becomes four scalars
|
||||
// carrying those four bindings. Before this, the single stamped binding sent the four elements to
|
||||
// units 0, 1, 2, 3.
|
||||
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsSplitIntoOneScalarPerElement) {
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, &declined);
|
||||
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
const Int units[4] = {0, 2, 4, 6};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", element) + ", ivec2(gl_FragCoord.xy)"))
|
||||
<< out;
|
||||
}
|
||||
// The array is gone entirely; nothing may still address units 0,1,2,3 through it.
|
||||
EXPECT_FALSE(Contains(out, "image2D g_image[4];")) << out;
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
// Exactly the four image uniforms the application declared - what the earlier widening cost
|
||||
// was the whole SPAN, seven here, which is the budget failure mode this shape removes.
|
||||
EXPECT_EQ(CountOf(out, "image2D "), 4u) << out;
|
||||
}
|
||||
|
||||
// The other program of the same conformance case: units 1, 3, 5, 7 in the application's own
|
||||
// element ORDER, which is what carries the assignment, so it must NOT be sorted or rebased.
|
||||
TEST(RemapImageArrayElementUnitsTest, EachElementCarriesTheUnitTheApplicationGaveIt) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 3) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(2.0));
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {3, 1, 7, 5})});
|
||||
const Int units[4] = {3, 1, 7, 5};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
}
|
||||
// Only elements 0 and 3 are ever accessed; elements 1 and 2 are declared and unused, because
|
||||
// the reflection says the array has four of them.
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0)")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 3) + ", ivec2(0)")) << out;
|
||||
}
|
||||
|
||||
// Consecutive-from-element-zero is exactly what ESSL does unaided, so the emitted text of an
|
||||
// ordinary image shader must come out byte-identical. The caller filters these; the pass must
|
||||
// not depend on that.
|
||||
TEST(RemapImageArrayElementUnitsTest, ConsecutiveUnitsAreLeftCompletelyAlone) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 2) uniform writeonly highp image2D g_image[3];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {2, 3, 4})}), source);
|
||||
// ...and so is a plan for an array this stage does not declare at all: the reflection is
|
||||
// program-wide, the pass runs per stage.
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("other_image", {0, 4})}), source);
|
||||
}
|
||||
|
||||
// A subscript that is not a literal names no element, so there is no scalar to rewrite it to.
|
||||
// It should never arrive - LegalizeResourceArrayIndexingForEssl runs first and ESSL rejects the
|
||||
// shape outright - but if one does, guessing an element would only change WHICH unit the access
|
||||
// reaches wrongly. Decline, loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, ANonLiteralSubscriptIsDeclinedAndNamed) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
imageStore(g_image[i], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 2, 4, 6})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
|
||||
// A literal that is out of the reflected range is the same class of mismatch.
|
||||
const String outOfRange = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[5], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> outOfRangeDeclined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(outOfRange, {Plan("g_image", {0, 5})}, &outOfRangeDeclined),
|
||||
outOfRange);
|
||||
ASSERT_EQ(outOfRangeDeclined.size(), 1u);
|
||||
}
|
||||
|
||||
// A uint subscript IS a literal element index. SPIRV-Cross prints an index in the type SPIR-V
|
||||
// gave it and LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
||||
// index it replaced, so an array walked by anything unsigned - a `uint` loop counter, or
|
||||
// anything derived from gl_LocalInvocationIndex, which is uint by definition - reaches this pass
|
||||
// spelled `g_image[0u]`. Refusing the `u` declined the array and left every element on the
|
||||
// consecutive units one binding hands out, silently.
|
||||
TEST(RemapImageArrayElementUnitsTest, AUintSubscriptIsStillALiteralElementIndex) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[3];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0u], ivec2(0), vec4(1.0));
|
||||
imageStore(g_image[2U], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined);
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
|
||||
EXPECT_TRUE(Contains(out, "binding = 0) uniform writeonly highp image2D " + Elem("g_image", 0) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 4) uniform writeonly highp image2D " + Elem("g_image", 1) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 8) uniform writeonly highp image2D " + Elem("g_image", 2) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0), vec4(1.0))")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 2) + ", ivec2(0), vec4(2.0))")) << out;
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
}
|
||||
|
||||
// ...and the suffix is not a licence to accept anything else that ends in one: `iu` is not a
|
||||
// literal, and neither is a bare `u`.
|
||||
TEST(RemapImageArrayElementUnitsTest, ASuffixAloneDoesNotMakeAnExpressionALiteral) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
highp int iu = 1;
|
||||
imageStore(g_image[iu], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// A use the pass cannot see a subscript on has no element index to rewrite, so splitting the
|
||||
// array out from under it would leave it naming a declaration that no longer exists. Decline,
|
||||
// loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, AUseWithoutASubscriptIsDeclined) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void helper();
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
||||
helper(g_image);
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 5})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
}
|
||||
|
||||
// The reflection and the emitted text have to be talking about the same array. If they are not,
|
||||
// the pass has misidentified something and must not rewrite on a guess.
|
||||
TEST(RemapImageArrayElementUnitsTest, AnExtentThatDisagreesWithTheReflectionIsDeclined) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[0], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// The two passes that run after it have to see the split declarations and keep their bindings: an
|
||||
// ES image unit cannot be assigned through the API, so the qualifier is the only mechanism there
|
||||
// is, and an element that is both read and written is split again into a pair that must BOTH
|
||||
// carry that element's own unit.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheSplitElementsSurviveTheLaterImagePasses) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 6})});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Element 0 is only ever loaded and element 1 only ever stored, so neither is split into a
|
||||
// pair - but each keeps the unit the application gave it, which the array could not express.
|
||||
EXPECT_TRUE(Contains(out, "binding = 4")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 6")) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RoAlias(Elem("g_image", 0)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WoAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias(Elem("g_image", 1)) + ", ivec2(0), imageLoad(" +
|
||||
RoAlias(Elem("g_image", 0)) + ", ivec2(0)))"))
|
||||
<< out;
|
||||
// Nothing is left addressing the array.
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
}
|
||||
|
||||
// The same element both read and written IS split into a coherent pair, and both halves have to
|
||||
// inherit that element's binding - the shape the widening used to have to carry on an array.
|
||||
TEST(RemapImageArrayElementUnitsTest, AnElementThatIsBothReadAndWrittenIsSplitWithItsOwnBinding) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[1], ivec2(0)));
|
||||
imageStore(g_image[0], ivec2(0), vec4(0.0));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Element 1 sits on unit 9, and both halves of its split pair say so.
|
||||
EXPECT_EQ(CountOf(out, "binding = 9"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WriteAlias(RwAlias(Elem("g_image", 1))) + ";"))
|
||||
<< out;
|
||||
EXPECT_EQ(CountOf(out, "binding = 4"), 1u) << out;
|
||||
}
|
||||
|
||||
// The gap that let a per-STAGE image rename reach production: every fixture above declares an
|
||||
// image ARRAY, and the regression it caused was in the SCALAR images sitting next to one. A
|
||||
// scalar with an explicit binding has to come out of the whole chain still on ITS OWN unit,
|
||||
// still spelled once, and named the same thing every stage would name it - it is the array that
|
||||
// needs repairing, not its neighbour.
|
||||
TEST(RemapImageArrayElementUnitsTest, AScalarImageWithItsOwnBindingIsUntouchedByTheArrayRepair) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba8, binding = 7) uniform highp image2D goku;
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
||||
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})}, &declined);
|
||||
EXPECT_TRUE(declined.empty());
|
||||
// The array pass may only ever touch the arrays it was handed a plan for.
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba8, binding = 7) uniform highp image2D goku;")) << out;
|
||||
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Unit 7 exactly twice - the two halves of the scalar's own split pair - and nothing has
|
||||
// moved it onto one of the array's units.
|
||||
EXPECT_EQ(CountOf(out, "binding = 7"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + RwAlias("goku") + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + SplitWriteAlias("goku") + ";")) << out;
|
||||
// ...and no per-stage tag anywhere: the name a scalar gets is a function of how this text
|
||||
// uses it, so every stage that uses it the same way keeps ONE shared uniform (Adreno spends
|
||||
// an image location per distinct one).
|
||||
EXPECT_FALSE(Contains(out, "mg_imageVs_")) << out;
|
||||
EXPECT_FALSE(Contains(out, "mg_imageFs_")) << out;
|
||||
EXPECT_FALSE(Contains(out, "mg_imageCs_")) << out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// RetargetTextureBufferExtension
|
||||
//
|
||||
@@ -732,3 +1286,84 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
||||
}
|
||||
|
||||
// --- pass-through tessellation control stage --------------------------------------------------
|
||||
//
|
||||
// Desktop GL makes the tessellation control stage optional and takes the levels from
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL; ES 3.2 has neither, and rejects a
|
||||
// program that has an evaluation stage without a control stage - with an EMPTY info log. The
|
||||
// synthesized stage is what stands in, and it has to MIRROR its two neighbours' gl_PerVertex
|
||||
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
|
||||
// ES link error against a program that has nothing else wrong with it.
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "");
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
|
||||
EXPECT_TRUE(Contains(out,
|
||||
"gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;"))
|
||||
<< out;
|
||||
// All six, unconditionally: writing a level the evaluation stage's domain does not use is
|
||||
// legal and ignored, and it saves the generator from having to know the domain.
|
||||
for (const char* level : {"gl_TessLevelOuter[0]", "gl_TessLevelOuter[1]", "gl_TessLevelOuter[2]",
|
||||
"gl_TessLevelOuter[3]", "gl_TessLevelInner[0]", "gl_TessLevelInner[1]"}) {
|
||||
EXPECT_TRUE(Contains(out, String(level) + " = 1.0;")) << level << "\n" << out;
|
||||
}
|
||||
// Nothing redeclared when the neighbours redeclared nothing - the driver's own built-in
|
||||
// gl_in/gl_out is then what both sides agree on, and redeclaring is what would break it.
|
||||
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
|
||||
}
|
||||
|
||||
// ES 3.1 reaches tessellation only through the extension; the caller has already established
|
||||
// that the driver runs the evaluation stage at all, so the only question is the spelling.
|
||||
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
||||
const String out = BuildPassthroughTessControlEssl(310, 3, "", "");
|
||||
EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
|
||||
}
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
|
||||
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
|
||||
const String outMembers = " highp vec4 gl_Position; ";
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers);
|
||||
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
|
||||
}
|
||||
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReadsEitherDirectionAndOnlyThatDirection) {
|
||||
const String essl = R"(#version 320 es
|
||||
in gl_PerVertex { highp vec4 gl_Position; } gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex { highp vec4 gl_Position; highp float gl_PointSize; } gl_out[];
|
||||
void main() {}
|
||||
)";
|
||||
const auto inMembers = ExtractPerVertexBlockMembers(essl, true);
|
||||
ASSERT_TRUE(inMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*inMembers, "gl_Position")) << *inMembers;
|
||||
EXPECT_FALSE(Contains(*inMembers, "gl_PointSize"))
|
||||
<< "the `in` block must not pick up the `out` block's members: " << *inMembers;
|
||||
|
||||
const auto outMembers = ExtractPerVertexBlockMembers(essl, false);
|
||||
ASSERT_TRUE(outMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*outMembers, "gl_PointSize")) << *outMembers;
|
||||
}
|
||||
|
||||
// A shader that does not redeclare the block must report nothing, so the generator leaves the
|
||||
// driver's built-in declaration alone rather than inventing one.
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReportsNothingWhenTheBlockIsNotRedeclared) {
|
||||
const String essl = R"(#version 320 es
|
||||
layout(quads) in;
|
||||
void main() { gl_Position = gl_in[0].gl_Position; }
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, false).has_value()) << essl;
|
||||
}
|
||||
|
||||
// "min" ends in "in" and "layout" ends in "out": the direction keyword has to be a whole token
|
||||
// immediately before the block name, or an unrelated identifier would be read as a redeclaration.
|
||||
TEST(ExtractPerVertexBlockMembersTest, DoesNotMatchAnIdentifierEndingInTheKeyword) {
|
||||
const String essl = R"(#version 320 es
|
||||
struct fin gl_PerVertex { highp vec4 gl_Position; };
|
||||
void main() {}
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -877,7 +877,7 @@ TEST_F(AsyncSpirvPhaseTest, AsyncOffAndAsyncOnProduceIdenticalSpirvAndShadow) {
|
||||
// The ASYNC arm runs FIRST, deliberately. Both arms must compile the same source text for
|
||||
// their SPIR-V to be comparable, and the first arm to run is the one that pays for the
|
||||
// cold path: it misses the per-context ShaderPreprocessCache and therefore executes
|
||||
// PreprocessShaderSource, the reserved-identifier scan and both lexical extractions. Run
|
||||
// PreprocessShaderSource and the lexical rejection scans. Run
|
||||
// the sync arm first and the async arm becomes a cache hit that never runs any of that on
|
||||
// a worker - which is exactly the half this case exists to compare.
|
||||
const SingleWorkerScope oneWorker;
|
||||
|
||||
@@ -2413,6 +2413,235 @@ void main() {
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One program carrying all four block/uniform kinds at once: a real uniform block, a
|
||||
// shader storage block, an atomic counter (which the transpiler lowers onto a synthesized
|
||||
// gl_AtomicCounterBlock_N buffer block) and plain default-block uniforms.
|
||||
//
|
||||
// MobileGL does not pass EShReflectionSeparateBuffers to glslang's buildReflection, so
|
||||
// glslang routes BUFFER blocks through indexToUniformBlock alongside the uniform blocks -
|
||||
// which is why every one of these has to be classified explicitly rather than taken at
|
||||
// face value from the reflection list.
|
||||
// The storage block and the counter are declared FIRST on purpose: that pushes both
|
||||
// uniform blocks off the front of the block list, so the GL uniform-block index and the
|
||||
// internal block index of every one of them differ. A translation that quietly reused one
|
||||
// space for the other would answer with the storage block's name, size and binding here.
|
||||
const char* kMixedBlockKindsFs = R"(#version 430
|
||||
layout(std430, binding = 0) buffer AVeryLongStorageBlockName {
|
||||
vec4 storageVec;
|
||||
};
|
||||
layout(binding = 1, offset = 0) uniform atomic_uint counter;
|
||||
layout(std140) uniform Blk {
|
||||
vec4 uboVec;
|
||||
};
|
||||
layout(std140) uniform Blk2 {
|
||||
vec4 uboVec2[3];
|
||||
};
|
||||
uniform float uScale;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = uboVec * uScale + uboVec2[1] + storageVec + vec4(float(atomicCounterIncrement(counter)));
|
||||
})";
|
||||
|
||||
const char* kMixedBlockKindsVs = R"(#version 430
|
||||
void main() { gl_Position = vec4(0.0); })";
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core 7.6: GL_ACTIVE_UNIFORM_BLOCKS and the glGetActiveUniformBlock* /
|
||||
// glGetUniformBlockIndex family enumerate ACTUAL uniform blocks. An atomic counter buffer is
|
||||
// enumerated by GL_ACTIVE_ATOMIC_COUNTER_BUFFERS and a shader storage block by the
|
||||
// GL_SHADER_STORAGE_BLOCK program interface; neither may appear in the uniform-block list.
|
||||
TEST_F(ProgramTest, UniformBlockListExcludesStorageAndAtomicCounterBlocks) {
|
||||
GLuint program = LinkVsFsProgram(kMixedBlockKindsVs, kMixedBlockKindsFs);
|
||||
|
||||
GLint activeBlocks = -1;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &activeBlocks);
|
||||
ASSERT_EQ(activeBlocks, 2) << "only 'Blk' and 'Blk2' are GL uniform blocks";
|
||||
|
||||
// GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH is measured over that same list, so the far
|
||||
// longer storage-block name must not raise it.
|
||||
GLint maxBlockNameLength = -1;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH, &maxBlockNameLength);
|
||||
EXPECT_EQ(maxBlockNameLength, static_cast<GLint>(std::strlen("Blk2") + 1));
|
||||
|
||||
const GLuint blk = GetUniformBlockIndex(program, "Blk");
|
||||
const GLuint blk2 = GetUniformBlockIndex(program, "Blk2");
|
||||
ASSERT_NE(blk, GL_INVALID_INDEX);
|
||||
ASSERT_NE(blk2, GL_INVALID_INDEX);
|
||||
EXPECT_LT(blk, 2u);
|
||||
EXPECT_LT(blk2, 2u);
|
||||
EXPECT_NE(blk, blk2);
|
||||
EXPECT_EQ(GetUniformBlockIndex(program, "AVeryLongStorageBlockName"), GL_INVALID_INDEX);
|
||||
EXPECT_EQ(GetUniformBlockIndex(program, "gl_AtomicCounterBlock_1"), GL_INVALID_INDEX);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// Every index in the list names one of the two, and each index answers with ITS OWN
|
||||
// block's properties - the storage block sits ahead of both in the internal block space,
|
||||
// so a query answered in the wrong space reports "AVeryLongStorageBlockName" here.
|
||||
char nameBuf[128] = "";
|
||||
GLsizei nameLen = 0;
|
||||
GetActiveUniformBlockName(program, blk, sizeof(nameBuf), &nameLen, nameBuf);
|
||||
EXPECT_STREQ(nameBuf, "Blk");
|
||||
GetActiveUniformBlockName(program, blk2, sizeof(nameBuf), &nameLen, nameBuf);
|
||||
EXPECT_STREQ(nameBuf, "Blk2");
|
||||
|
||||
GLint dataSize = -1;
|
||||
GetActiveUniformBlockiv(program, blk, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
|
||||
EXPECT_EQ(dataSize, 16) << "Blk is one vec4";
|
||||
GetActiveUniformBlockiv(program, blk2, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
|
||||
EXPECT_EQ(dataSize, 48) << "Blk2 is a vec4[3]";
|
||||
|
||||
GLint nameLengthProp = -1;
|
||||
GetActiveUniformBlockiv(program, blk2, GL_UNIFORM_BLOCK_NAME_LENGTH, &nameLengthProp);
|
||||
EXPECT_EQ(nameLengthProp, static_cast<GLint>(std::strlen("Blk2") + 1));
|
||||
|
||||
// glUniformBlockBinding lands on the block the GL index names, and reads back through the
|
||||
// same index.
|
||||
UniformBlockBinding(program, blk2, 7);
|
||||
GLint binding = -1;
|
||||
GetActiveUniformBlockiv(program, blk2, GL_UNIFORM_BLOCK_BINDING, &binding);
|
||||
EXPECT_EQ(binding, 7);
|
||||
GetActiveUniformBlockiv(program, blk, GL_UNIFORM_BLOCK_BINDING, &binding);
|
||||
EXPECT_NE(binding, 7) << "the rebind must not have leaked onto the neighbouring block";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// An index past the end of the (now shorter) list is GL_INVALID_VALUE, not a silently
|
||||
// answered query about a storage block.
|
||||
GLint sink = -12345;
|
||||
GetActiveUniformBlockiv(program, static_cast<GLuint>(activeBlocks), GL_UNIFORM_BLOCK_BINDING, &sink);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
|
||||
EXPECT_EQ(sink, -12345);
|
||||
UniformBlockBinding(program, static_cast<GLuint>(activeBlocks), 1);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
|
||||
GetActiveUniformBlockName(program, static_cast<GLuint>(activeBlocks), sizeof(nameBuf), &nameLen, nameBuf);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
|
||||
|
||||
// Each block's own member resolves against the block index this list hands out.
|
||||
const GLuint uboVec = UniformIndexByName(program, "uboVec");
|
||||
const GLuint uboVec2 = UniformIndexByName(program, "uboVec2[0]");
|
||||
ASSERT_NE(uboVec, GL_INVALID_INDEX);
|
||||
ASSERT_NE(uboVec2, GL_INVALID_INDEX);
|
||||
EXPECT_EQ(QueryUniformiv(program, uboVec, GL_UNIFORM_BLOCK_INDEX), static_cast<GLint>(blk));
|
||||
EXPECT_EQ(QueryUniformiv(program, uboVec2, GL_UNIFORM_BLOCK_INDEX), static_cast<GLint>(blk2));
|
||||
|
||||
GLint blockMemberCount = -1;
|
||||
GetActiveUniformBlockiv(program, blk, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &blockMemberCount);
|
||||
EXPECT_EQ(blockMemberCount, 1);
|
||||
GLint blockMemberIndex = -1;
|
||||
GetActiveUniformBlockiv(program, blk, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &blockMemberIndex);
|
||||
EXPECT_EQ(static_cast<GLuint>(blockMemberIndex), uboVec);
|
||||
GetActiveUniformBlockiv(program, blk2, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &blockMemberIndex);
|
||||
EXPECT_EQ(static_cast<GLuint>(blockMemberIndex), uboVec2);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The GL_UNIFORM_BLOCK program interface hands out indices that are usable with
|
||||
// glUniformBlockBinding / glGetActiveUniformBlockiv (ARB_program_interface_query), so it has
|
||||
// to enumerate exactly the same list - not the internal block space that also carries the
|
||||
// storage and atomic counter blocks.
|
||||
TEST_F(ProgramTest, UniformBlockProgramInterfaceMatchesTheUniformBlockList) {
|
||||
GLuint program = LinkVsFsProgram(kMixedBlockKindsVs, kMixedBlockKindsFs);
|
||||
|
||||
GLint interfaceBlocks = -1;
|
||||
GetProgramInterfaceiv(program, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES, &interfaceBlocks);
|
||||
GLint activeBlocks = -1;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &activeBlocks);
|
||||
EXPECT_EQ(interfaceBlocks, activeBlocks);
|
||||
ASSERT_EQ(interfaceBlocks, 2);
|
||||
|
||||
// The storage block is enumerated by its OWN interface instead.
|
||||
GLint storageBlocks = -1;
|
||||
GetProgramInterfaceiv(program, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &storageBlocks);
|
||||
EXPECT_EQ(storageBlocks, 1);
|
||||
EXPECT_EQ(GetProgramResourceIndex(program, GL_UNIFORM_BLOCK, "AVeryLongStorageBlockName"), GL_INVALID_INDEX);
|
||||
EXPECT_NE(GetProgramResourceIndex(program, GL_SHADER_STORAGE_BLOCK, "AVeryLongStorageBlockName"),
|
||||
GL_INVALID_INDEX);
|
||||
// ... and the buffer variable by GL_BUFFER_VARIABLE, not GL_UNIFORM.
|
||||
EXPECT_NE(GetProgramResourceIndex(program, GL_BUFFER_VARIABLE, "storageVec"), GL_INVALID_INDEX);
|
||||
EXPECT_EQ(GetProgramResourceIndex(program, GL_UNIFORM, "storageVec"), GL_INVALID_INDEX);
|
||||
|
||||
for (const char* blockName : {"Blk", "Blk2"}) {
|
||||
const GLuint interfaceIndex = GetProgramResourceIndex(program, GL_UNIFORM_BLOCK, blockName);
|
||||
ASSERT_NE(interfaceIndex, GL_INVALID_INDEX) << blockName;
|
||||
EXPECT_EQ(interfaceIndex, GetUniformBlockIndex(program, blockName)) << blockName;
|
||||
|
||||
// GL_NUM_ACTIVE_VARIABLES / GL_ACTIVE_VARIABLES must reach the same member the
|
||||
// glGetActiveUniformBlockiv spelling does.
|
||||
const GLenum numActive = GL_NUM_ACTIVE_VARIABLES;
|
||||
GLint memberCount = -1;
|
||||
GetProgramResourceiv(program, GL_UNIFORM_BLOCK, interfaceIndex, 1, &numActive, 1, nullptr, &memberCount);
|
||||
ASSERT_EQ(memberCount, 1) << blockName;
|
||||
const GLenum activeVariables = GL_ACTIVE_VARIABLES;
|
||||
GLint memberIndex = -1;
|
||||
GetProgramResourceiv(program, GL_UNIFORM_BLOCK, interfaceIndex, 1, &activeVariables, 1, nullptr,
|
||||
&memberIndex);
|
||||
GLint viaBlockiv = -1;
|
||||
GetActiveUniformBlockiv(program, interfaceIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &viaBlockiv);
|
||||
EXPECT_EQ(memberIndex, viaBlockiv) << blockName;
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.3.1 / 7.6: a buffer variable is not a uniform - it lives in the
|
||||
// GL_BUFFER_VARIABLE interface - so it must not appear in GL_ACTIVE_UNIFORMS,
|
||||
// glGetActiveUniform, glGetUniformIndices or glGetActiveUniformsiv. An ATOMIC COUNTER, by
|
||||
// contrast, IS a uniform (of type GL_UNSIGNED_INT_ATOMIC_COUNTER) and must stay enumerated.
|
||||
TEST_F(ProgramTest, ActiveUniformsExcludeBufferVariablesButKeepAtomicCounters) {
|
||||
GLuint program = LinkVsFsProgram(kMixedBlockKindsVs, kMixedBlockKindsFs);
|
||||
|
||||
GLint activeUniforms = -1;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_EQ(activeUniforms, 4)
|
||||
<< "uboVec, uboVec2[0], uScale and counter - storageVec is a buffer variable";
|
||||
|
||||
// Neither spelling of the buffer variable is a uniform index.
|
||||
EXPECT_EQ(UniformIndexByName(program, "storageVec"), GL_INVALID_INDEX);
|
||||
EXPECT_EQ(UniformIndexByName(program, "AVeryLongStorageBlockName.storageVec"), GL_INVALID_INDEX);
|
||||
// The location half of the same rule (already landed) must stay consistent with it.
|
||||
EXPECT_EQ(GetUniformLocation(program, "storageVec"), -1);
|
||||
|
||||
char nameBuf[128] = "";
|
||||
for (GLint i = 0; i < activeUniforms; ++i) {
|
||||
GLsizei nameLen = 0;
|
||||
GLint size = 0;
|
||||
GLenum type = 0;
|
||||
GetActiveUniform(program, static_cast<GLuint>(i), sizeof(nameBuf), &nameLen, &size, &type, nameBuf);
|
||||
EXPECT_EQ(std::string(nameBuf).find("storageVec"), std::string::npos)
|
||||
<< "buffer variable enumerated as active uniform " << i << ": " << nameBuf;
|
||||
}
|
||||
|
||||
// The counter is still a uniform, still reports the atomic-counter type, has no owning
|
||||
// uniform block, and still points at its atomic counter BUFFER.
|
||||
const GLuint counter = UniformIndexByName(program, "counter");
|
||||
ASSERT_NE(counter, GL_INVALID_INDEX);
|
||||
EXPECT_EQ(QueryUniformiv(program, counter, GL_UNIFORM_TYPE),
|
||||
static_cast<GLint>(GL_UNSIGNED_INT_ATOMIC_COUNTER));
|
||||
EXPECT_EQ(QueryUniformiv(program, counter, GL_UNIFORM_BLOCK_INDEX), -1);
|
||||
EXPECT_EQ(QueryUniformiv(program, counter, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX), 0);
|
||||
EXPECT_EQ(QueryUniformiv(program, counter, GL_UNIFORM_OFFSET), 0);
|
||||
EXPECT_EQ(GetUniformLocation(program, "counter"), -1);
|
||||
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS indexes into the GL uniform index space, so the
|
||||
// counter index it reports has to be the one glGetUniformIndices just handed out.
|
||||
GLint counterBuffers = -1;
|
||||
GetProgramiv(program, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &counterBuffers);
|
||||
ASSERT_EQ(counterBuffers, 1);
|
||||
GLint counterCount = -1;
|
||||
GetActiveAtomicCounterBufferiv(program, 0, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS, &counterCount);
|
||||
ASSERT_EQ(counterCount, 1);
|
||||
GLint counterIndex = -1;
|
||||
GetActiveAtomicCounterBufferiv(program, 0, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
|
||||
&counterIndex);
|
||||
EXPECT_EQ(static_cast<GLuint>(counterIndex), counter);
|
||||
GLint counterBinding = -1;
|
||||
GetActiveAtomicCounterBufferiv(program, 0, GL_ATOMIC_COUNTER_BUFFER_BINDING, &counterBinding);
|
||||
EXPECT_EQ(counterBinding, 1);
|
||||
|
||||
// The default-block uniform is untouched by either filter.
|
||||
EXPECT_NE(GetUniformLocation(program, "uScale"), -1);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) {
|
||||
// GL CTS compiles through exactly this sequence (create, attach, DELETE, source,
|
||||
// compile): glDeleteShader on an attached shader only flags it, and the name must
|
||||
@@ -2940,9 +3169,9 @@ void main() { fragColor = vec4(pow(uBase, 2.2), 1.0); }
|
||||
// Layer 1 - the same shader object re-sourced with byte-identical text keeps its
|
||||
// compiled state, and glCompileShader on it is a no-op.
|
||||
// Layer 2 - two DIFFERENT shader objects holding byte-identical text share the
|
||||
// source-only half of the pipeline (preprocess + lexical checks +
|
||||
// side-channel extraction) through the context's ShaderPreprocessCache,
|
||||
// while each still gets its own glslang parse.
|
||||
// source-only half of the pipeline (preprocess + the lexical rejection
|
||||
// checks) through the context's ShaderPreprocessCache, while each still
|
||||
// gets its own glslang parse.
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
const char* kP0bVs = R"(#version 330 core
|
||||
@@ -3401,3 +3630,182 @@ void main() { fragColor = vec4(1.0); }
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6: an atomic counter is a default-block uniform that addresses an ATOMIC COUNTER
|
||||
// buffer, where every counter is a tightly packed 4-byte value. MobileGL lowers each atomic_uint
|
||||
// onto a synthesized block, which used to drag the whole array-stride query onto the std140 rule
|
||||
// that rounds an element stride up to a vec4 - so an atomic counter array reported 16
|
||||
// (KHR-GL43.shader_atomic_counters.basic-program-query: "GL_UNIFORM_ARRAY_STRIDE is 16 should be
|
||||
// 4"). The offsets, matrix stride and row-major flag are pinned alongside it because the same
|
||||
// synthesized block feeds all four queries.
|
||||
TEST_F(ProgramTest, AtomicCounterArrayReportsThePackedFourByteStride) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
void main() { gl_Position = vec4(1.0); }
|
||||
)";
|
||||
const char* fsSource = R"(#version 430 core
|
||||
layout(location = 0) out vec4 o_color;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint ac_counter0;
|
||||
layout(binding = 0, offset = 4) uniform atomic_uint ac_counter1;
|
||||
layout(binding = 0) uniform atomic_uint ac_counter2;
|
||||
layout(binding = 0) uniform atomic_uint ac_counter67[2];
|
||||
layout(binding = 0) uniform atomic_uint ac_counter3;
|
||||
void main() {
|
||||
uint c = 0u;
|
||||
c += atomicCounterIncrement(ac_counter0);
|
||||
c += atomicCounterIncrement(ac_counter1);
|
||||
c += atomicCounterIncrement(ac_counter2);
|
||||
c += atomicCounterIncrement(ac_counter3);
|
||||
c += atomicCounterIncrement(ac_counter67[0]);
|
||||
c += atomicCounterIncrement(ac_counter67[1]);
|
||||
o_color = vec4(float(c));
|
||||
}
|
||||
)";
|
||||
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||
|
||||
GLint activeUniforms = 0;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_EQ(activeUniforms, 5);
|
||||
|
||||
// Declared offset -> expected {array size, array stride}. layout(offset=) pins the first two;
|
||||
// the rest are packed after them in declaration order, the array taking two 4-byte slots.
|
||||
struct Expectation {
|
||||
const char* name;
|
||||
GLint size;
|
||||
GLint offset;
|
||||
GLint arrayStride;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{"ac_counter0", 1, 0, 0}, {"ac_counter1", 1, 4, 0}, {"ac_counter2", 1, 8, 0},
|
||||
{"ac_counter67[0]", 2, 12, 4}, {"ac_counter3", 1, 20, 0},
|
||||
};
|
||||
|
||||
for (const auto& expected : expectations) {
|
||||
const char* queryName = expected.name;
|
||||
GLuint index = GL_INVALID_INDEX;
|
||||
GetUniformIndices(program, 1, &queryName, &index);
|
||||
ASSERT_NE(index, GL_INVALID_INDEX) << expected.name << " is not an active uniform";
|
||||
|
||||
GLint value = -2;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_TYPE, &value);
|
||||
EXPECT_EQ(value, static_cast<GLint>(GL_UNSIGNED_INT_ATOMIC_COUNTER)) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_SIZE, &value);
|
||||
EXPECT_EQ(value, expected.size) << expected.name;
|
||||
// An atomic counter is a default-block uniform however it was lowered.
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_BLOCK_INDEX, &value);
|
||||
EXPECT_EQ(value, -1) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_OFFSET, &value);
|
||||
EXPECT_EQ(value, expected.offset) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_ARRAY_STRIDE, &value);
|
||||
EXPECT_EQ(value, expected.arrayStride) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_MATRIX_STRIDE, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_IS_ROW_MAJOR, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
GetActiveUniformsiv(program, 1, &index, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, &value);
|
||||
EXPECT_EQ(value, 0) << expected.name;
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6.1: a uniform LOCATION is a property of the default uniform block. A member of
|
||||
// a named uniform block or a buffer block has none, and glGetUniformLocation must answer -1 for
|
||||
// it - which is what glGetProgramResourceLocation(GL_UNIFORM, ...) already did, so the two used
|
||||
// to disagree. The location such a member was handed was not merely reported, it was CONSUMED:
|
||||
// it came out of the same first-fit table the default-block uniforms draw from.
|
||||
TEST_F(ProgramTest, BlockMembersConsumeNoUniformLocation) {
|
||||
const char* csSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 1) buffer ResultBuffer { vec4 bufferMember; };
|
||||
layout(std140, binding = 2) uniform SettingsBlock { vec4 blockMember; };
|
||||
layout(location = 0) uniform float uDead[3];
|
||||
uniform float uImplicit;
|
||||
void main() { bufferMember = blockMember * uImplicit; }
|
||||
)";
|
||||
const GLuint cs = CompileShaderChecked(GL_COMPUTE_SHADER, csSource);
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, cs);
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
char infoLog[1024] = "";
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
|
||||
|
||||
for (const char* member : {"bufferMember", "blockMember"}) {
|
||||
EXPECT_EQ(GetUniformLocation(program, member), -1) << member << " is a block member, not a GL uniform";
|
||||
EXPECT_EQ(GetProgramResourceLocation(program, GL_UNIFORM, member), -1)
|
||||
<< member << ": the two location queries must agree";
|
||||
}
|
||||
|
||||
// uDead[3] reserves 0..2 without becoming visible, so the first location left for the one
|
||||
// default-block uniform is 3. It used to be 4, because a block member took 3 first.
|
||||
EXPECT_EQ(GetUniformLocation(program, "uImplicit"), 3)
|
||||
<< "a block member consumed a location the default-block uniform was entitled to";
|
||||
EXPECT_EQ(GetUniformLocation(program, "uDead"), -1);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same defect at the boundary, which is where the conformance suite catches it. The location
|
||||
// table's ceiling is raised to hold every uniform it must place; counting block members into that
|
||||
// raise pushed the ceiling to GL_MAX_UNIFORM_LOCATIONS itself, and the first-fit pass then handed
|
||||
// out the one location past the legal 0..MAX-1 range
|
||||
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit-max, whose compute program
|
||||
// carries an SSBO: "Uniform u2 returned location (4095) is greater than implementation dependent
|
||||
// limit (4095)"). Its -array sibling shares the root cause and failed one step further along, with
|
||||
// the pool reported exhausted and no link at all.
|
||||
TEST_F(ProgramTest, ImplicitLocationStaysInRangeWhenABufferBlockSharesTheProgram) {
|
||||
GLint maxLocations = 0;
|
||||
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
|
||||
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
|
||||
|
||||
// The CTS shape: explicit unused arrays fill the pool except for a hole of `implicitCount`
|
||||
// locations at `holeBase`, and the one implicit uniform must land exactly in that hole.
|
||||
const auto runCase = [&](int holeBase, int implicitCount) {
|
||||
String decls;
|
||||
int nextName = 0;
|
||||
if (holeBase > 0) {
|
||||
decls += "layout(location = 0) uniform float u" + std::to_string(nextName++) + "[" +
|
||||
std::to_string(holeBase) + "];\n";
|
||||
}
|
||||
const int tailBase = holeBase + implicitCount;
|
||||
if (tailBase < maxLocations) {
|
||||
decls += "layout(location = " + std::to_string(tailBase) + ") uniform float u" +
|
||||
std::to_string(nextName++) + "[" + std::to_string(maxLocations - tailBase) + "];\n";
|
||||
}
|
||||
const String implicitName = "u" + std::to_string(nextName);
|
||||
decls += "uniform float " + implicitName + "[" + std::to_string(implicitCount) + "];\n";
|
||||
|
||||
// The buffer block is the whole point: it is one more uniform the table has to seat, and
|
||||
// seating it inside the location space is what used to push the implicit uniform out.
|
||||
const String csSource = "#version 430 core\n"
|
||||
"layout(local_size_x = 1) in;\n"
|
||||
"layout(std430, binding = 1) buffer ResultBuffer { vec4 cs_result; };\n" +
|
||||
decls + "void main() { cs_result = vec4(" + implicitName + "[0]); }\n";
|
||||
const GLuint cs = CompileShaderChecked(GL_COMPUTE_SHADER, csSource.c_str());
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, cs);
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
char infoLog[1024] = "";
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
ASSERT_EQ(linkStatus, GL_TRUE) << "hole at " << holeBase << " x" << implicitCount << ": " << infoLog;
|
||||
|
||||
const GLint location = GetUniformLocation(program, implicitName.c_str());
|
||||
EXPECT_EQ(location, holeBase) << "the implicit uniform must take the one free span left";
|
||||
EXPECT_LT(location + implicitCount, maxLocations + 1)
|
||||
<< "locations " << location << ".." << (location + implicitCount - 1)
|
||||
<< " must stay inside 0.." << (maxLocations - 1);
|
||||
EXPECT_EQ(GetUniformLocation(program, "cs_result"), -1);
|
||||
};
|
||||
|
||||
// The three holes the CTS walks, for its single-uniform and its 3-element-array subcase.
|
||||
for (const int implicitCount : {1, 3}) {
|
||||
runCase(0, implicitCount);
|
||||
runCase(3, implicitCount);
|
||||
runCase(maxLocations - implicitCount, implicitCount);
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
|
||||
@@ -2130,9 +2131,11 @@ void main() {
|
||||
}
|
||||
|
||||
// The case the old masker actually broke: an apostrophe in real (non-comment) text. Everything after
|
||||
// it looked like string interior, so ExtractExplicitUniformLocations tokenized a blank source and
|
||||
// handed the GL location assigner an empty map - the uniform silently lost its explicit location.
|
||||
TEST_F(ProgramUtilTest, PreprocessApostropheInDirectiveKeepsLaterCodeVisibleToExtractors) {
|
||||
// it looked like string interior, so the rewriter's own scans went blind past it - which is still
|
||||
// what this pins, now that the explicit location itself is recovered from the parse rather than
|
||||
// from a scan. The two halves have to agree end to end: the preprocessed text must still declare
|
||||
// the uniform, AND the parse must still hand its location back.
|
||||
TEST_F(ProgramUtilTest, PreprocessApostropheInDirectiveKeepsLaterCodeVisibleToTheParse) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 460 core
|
||||
@@ -2147,15 +2150,15 @@ void main() {
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||
ASSERT_EQ(locations.count("tint"), 1u) << "extractor went blind past the apostrophe:\n" << source;
|
||||
EXPECT_EQ(locations.at("tint"), 7);
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*res.value());
|
||||
ASSERT_EQ(locations.count("tint"), 1u) << "the rewriter went blind past the apostrophe:\n" << source;
|
||||
EXPECT_EQ(locations.at("tint"), 7);
|
||||
}
|
||||
|
||||
// PreprocessShaderSource used to rediscover "where does the #version directive end?" once per
|
||||
@@ -2339,8 +2342,7 @@ namespace {
|
||||
auto result = MakeShared<ShaderPreprocessResult>();
|
||||
result->outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||
result->preprocessedSource = preprocessed;
|
||||
result->explicitUniformLocations["uMarker"] = 7;
|
||||
result->explicitOpaqueBindings["sMarker"] = 3;
|
||||
result->infoLog = "marker:" + preprocessed;
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
@@ -2357,12 +2359,9 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) {
|
||||
ASSERT_NE(hit, nullptr);
|
||||
EXPECT_TRUE(hit->Preprocessed());
|
||||
EXPECT_EQ(hit->preprocessedSource, "vertex-preprocessed");
|
||||
const auto uniformIt = hit->explicitUniformLocations.find("uMarker");
|
||||
ASSERT_NE(uniformIt, hit->explicitUniformLocations.end());
|
||||
EXPECT_EQ(uniformIt->second, 7);
|
||||
const auto bindingIt = hit->explicitOpaqueBindings.find("sMarker");
|
||||
ASSERT_NE(bindingIt, hit->explicitOpaqueBindings.end());
|
||||
EXPECT_EQ(bindingIt->second, 3u);
|
||||
// The whole payload round-trips, not just the text: every field the entry carries has to
|
||||
// come back, or a hit would publish a half-populated result.
|
||||
EXPECT_EQ(hit->infoLog, "marker:vertex-preprocessed");
|
||||
|
||||
// Byte-identical source, different stage: a different key, so still a miss. Two
|
||||
// stages sharing one entry would hand a fragment shader a vertex preprocess.
|
||||
@@ -3041,6 +3040,47 @@ void main() {
|
||||
<< "the generated ESSL still indexes a fragment output with a non-constant:\n" << essl;
|
||||
}
|
||||
|
||||
// Marking a loop for unrolling means marking every loop enclosing it - SPIRV-Tools only unrolls
|
||||
// innermost loops - and the copies those levels produce MULTIPLY, so bounding each loop on its
|
||||
// own bounds nothing. This nest is the OIT shape wrapped in a tile walk: 64 x 64 x 2, every level
|
||||
// individually inside kMaxUnrolledIterations, and its product is not. Spending the budget as the
|
||||
// walk climbs stops at the innermost level; the switch lowering, whose cost is the output array's
|
||||
// length rather than the trip counts, legalizes whatever the unroll no longer reaches. The same
|
||||
// defect was measured first on LegalizeResourceArrayIndexPass, which the image half of that pass
|
||||
// made reachable; this walk is its twin and is fixed the same way.
|
||||
TEST_F(ProgramUtilTest, ALoopNestAroundAFragmentOutputIndexIsBoundedAsAWhole) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = CompileFragmentToRawSpirv(R"(#version 330 core
|
||||
out vec4 coeff[2];
|
||||
in vec4 vColor;
|
||||
void main() {
|
||||
for (int y = 0; y < 64; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
for (int attachmentIndex = 0; attachmentIndex < 2; ++attachmentIndex) {
|
||||
coeff[attachmentIndex] = vColor * float(x + y + attachmentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
// Still legalized - that is not what is being traded away.
|
||||
EXPECT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(legalized));
|
||||
// ...and the module the driver has to compile is still a module, not the nest's product.
|
||||
// Measured on this fixture: 318 words with the nest budget, 5112 without - so the bound is
|
||||
// loose enough not to pin spirv-opt's exact output (3x the real figure) and tight enough
|
||||
// that a nest-wide unroll cannot slip under it (5x below the unbounded one).
|
||||
EXPECT_LT(legalized.size(), 1024u) << "legalized module is " << legalized.size() << " words";
|
||||
}
|
||||
|
||||
// The fallback half: an index computed from a uniform cannot be folded by any amount of
|
||||
// unrolling, so the write becomes a switch over the array's range and the read becomes
|
||||
// constant-indexed loads combined with selects.
|
||||
@@ -3651,6 +3691,290 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
|
||||
<< "declining means the 1D-array type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- 1D SAMPLED images (Lower1DSampledImagesPass) ----------------------------------------------
|
||||
//
|
||||
// The other half of the 1D story. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - the
|
||||
// test above pins that - but it prints the OFFSET and the two GRADIENT operands with the arity the
|
||||
// desktop shader spelled, against a sampler it has just declared 2D. The result has no ESSL
|
||||
// overload, the driver says "no matching overloaded function found", and the stage is lost.
|
||||
|
||||
namespace {
|
||||
// Same word walk as the storage-image counters, for Sampled == 1.
|
||||
SizeT Count1DSampledImageTypes(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
|
||||
SizeT count = 0;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D &&
|
||||
spirv[i + 7] == 1u) {
|
||||
++count;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// KHR-GL43.compute_shader.resource-texture's own sampler1DArray lookup, minus the other eight
|
||||
// samplers: a textureLodOffset whose offset is the scalar GL gives a 1D array.
|
||||
const char* k1DArraySamplerOffsetCompute = R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() { ssb.data = textureLodOffset(g_sampler4, vec2(0.5, 1.0), 0.0, 0); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// The negative control, and the whole reason the pass exists: SPIRV-Cross emits the sampler as 2D
|
||||
// and widens the coordinate, then hands the scalar offset straight through. Pinning the upstream
|
||||
// behaviour here means that if a future SPIRV-Cross bump fixes it, this test fails and says so,
|
||||
// rather than the pass quietly becoming dead weight.
|
||||
TEST_F(ProgramUtilTest, SpirvCrossEmitsAScalarOffsetFor1DSamplers) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(k1DArraySamplerOffsetCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 1u)
|
||||
<< "glslang no longer emits a Dim1D/Sampled=1 image for sampler1DArray";
|
||||
|
||||
const String essl = DecompileToEssl(spirv);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("sampler2DArray"), String::npos)
|
||||
<< "SPIRV-Cross declares the 1D array sampler as 2D on ES; that half it does do:\n" << essl;
|
||||
EXPECT_EQ(essl.find("ivec2"), String::npos)
|
||||
<< "SPIRV-Cross is expected to pass the SCALAR offset straight through, so nothing in this "
|
||||
"fixture builds an ivec2 - its absence IS the defect, because ESSL has no "
|
||||
"textureLodOffset(sampler2DArray, vec3, float, int). If this no longer happens, "
|
||||
"Lower1DSampledImagesForEssl may no longer be needed:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The fix: the type becomes a 2D array and the offset becomes two components, so the call
|
||||
// type-checks against the declaration SPIRV-Cross was already emitting.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesWidensTheOffsetOfA1DArrayLookup) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(k1DArraySamplerOffsetCompute, GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
// Through the shared chain first, exactly as the DirectGLES transpile path does - the same
|
||||
// reason the storage-image tests above do it: the pass runs on sanitized bytes, and validating
|
||||
// raw glslang output would latch pre-existing properties against this pass.
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u)
|
||||
<< "no 1D sampled image type may survive the pass:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
// The point of moving the TYPE rather than only the operand: an ivec2 offset against a type
|
||||
// still declared Dim1D is an invalid module, and the validator would say so.
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("sampler2DArray"), String::npos)
|
||||
<< "the sampler must still be declared as the 2D array the texture is stored as:\n" << essl;
|
||||
EXPECT_NE(essl.find("ivec2"), String::npos)
|
||||
<< "the offset must now be the two-component one ESSL's sampler2DArray overload takes:\n"
|
||||
<< essl;
|
||||
}
|
||||
|
||||
// The gradients take the identical repair, and through a different SPIRV-Cross branch - the offset
|
||||
// is emitted at `if (args.offset)` and the gradients at `if (args.grad_x || args.grad_y)`, so one
|
||||
// fixture cannot cover both.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesWidensTheGradientsOfA1DLookup) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() { ssb.data = textureGrad(g_sampler0, 0.5, 0.25, 0.125); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("textureGrad"), String::npos) << essl;
|
||||
// Both derivatives have to be widened, not just the first: ESSL's overload takes two vec2s.
|
||||
EXPECT_NE(essl.find("vec2(0.25, 0.0)"), String::npos)
|
||||
<< "dPdx must be widened to two components:\n" << essl;
|
||||
EXPECT_NE(essl.find("vec2(0.125, 0.0)"), String::npos)
|
||||
<< "dPdy must be widened too:\n" << essl;
|
||||
}
|
||||
|
||||
// Scope: a 1D sampler that is only SAMPLED or FETCHED is emitted correctly by the very same
|
||||
// SPIRV-Cross code, so the pass must not touch it. Replacing working emission with our own buys
|
||||
// nothing and risks everything - the same rule the storage-image sibling applies to a 1D image
|
||||
// with no atomic on it. resource-texture's own sampler1D is exactly this shape (it only calls
|
||||
// texelFetch), so this is not a hypothetical.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesLeavesPlainLookupsToSpirvCross) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = texelFetch(g_sampler0, 2, 0) + texture(g_sampler4, vec2(0.5, 1.0));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 2u);
|
||||
EXPECT_FALSE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "no offset and no gradient here, so the probe must say there is nothing to do";
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a 1D sampler with no offset or gradient must pass through byte "
|
||||
"for byte";
|
||||
}
|
||||
|
||||
// The gate is per arrayed-ness, matching the two distinct OpTypeImage declarations glslang emits:
|
||||
// the sampler1DArray carries the offset and is rewritten, while the sampler1D in the same module
|
||||
// is left to SPIRV-Cross. This is resource-texture's own shape.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesRewritesOnlyTheArrayednessThatCarriesTheOffset) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler1DArray g_sampler4;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = texelFetch(g_sampler0, 2, 0) +
|
||||
textureLodOffset(g_sampler4, vec2(0.5, 1.0), 0.0, 0);
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 2u);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 1u)
|
||||
<< "the arrayed sampler must be rewritten and the non-arrayed one left alone:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the lowered module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
|
||||
// Both spellings coincide on ES, which is why a partial rewrite is safe here and is NOT safe
|
||||
// for the storage-image sibling: SPIRV-Cross prints Dim1D as "2D" already, so the stage that
|
||||
// was rewritten and the stage that was not declare the same ESSL type.
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("sampler1D"), String::npos)
|
||||
<< "nothing may reach the driver still spelled 1D:\n" << essl;
|
||||
}
|
||||
|
||||
// The shape that would emit INVALID SPIR-V without the deduplication, and the shape the
|
||||
// conformance case actually has: a 1D sampler and a real 2D sampler of the same sampled type in
|
||||
// one module. Rewriting the first one's Dim in place makes the two OpTypeImage declarations
|
||||
// structurally identical, and SPIR-V forbids duplicate non-aggregate types.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesDeduplicatesAgainstAnExisting2DSampler) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
uniform sampler2D g_sampler1;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = textureLodOffset(g_sampler0, 0.5, 0.0, 1) +
|
||||
textureLod(g_sampler1, vec2(0.5), 0.0);
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DSampledImageTypes(spirv), 1u);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the rewritten 1D sampler collided with the module's own 2D sampler and left a "
|
||||
"duplicate type declaration behind:\n"
|
||||
<< DisassembleSpirv(lowered);
|
||||
}
|
||||
|
||||
// The declined shape, for the sibling's reason: textureSize(sampler1D) yields an int and
|
||||
// textureSize(sampler2D) an ivec2, so rewriting the type while leaving the query would hand the
|
||||
// shader a value of the wrong shape. The module is returned untouched rather than half-translated.
|
||||
TEST_F(ProgramUtilTest, Lower1DSampledImagesDeclinesAModuleThatQueriesTheTextureSize) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
|
||||
layout (local_size_x = 1) in;
|
||||
uniform sampler1D g_sampler0;
|
||||
layout (std430, binding = 0) buffer SSB { vec4 data; } ssb;
|
||||
void main() {
|
||||
ssb.data = textureLodOffset(g_sampler0, 0.5, 0.0, 1) + float(textureSize(g_sampler0, 0));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
Vector<Uint32> spirv;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_TRUE(Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(spirv))
|
||||
<< "the fixture must still carry the offset that arms the pass, so that the decline is "
|
||||
"what leaves the module alone rather than the gate:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DSampledImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv)
|
||||
<< "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(Count1DSampledImageTypes(lowered), 1u)
|
||||
<< "declining means the 1D type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
|
||||
@@ -3674,6 +3998,9 @@ namespace {
|
||||
constexpr Uint kGlRgba32ui = 0x8D70;
|
||||
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
|
||||
@@ -3716,14 +4043,34 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
|
||||
|
||||
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
|
||||
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
|
||||
// into the module would trade a missing qualifier for a missing shader. They are left format-less
|
||||
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
|
||||
// which the stencil half of the packed_depth_stencil case binds, is one of them.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
|
||||
// into the module would trade a missing qualifier for a missing shader.
|
||||
//
|
||||
// That still holds for the formats NOTHING can rescue, which are left format-less here and
|
||||
// completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). It stopped
|
||||
// holding for the ones that widen EXACTLY: WidenImageFormatsForEssl runs immediately after this
|
||||
// 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, 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, the text completion can go";
|
||||
<< "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_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*/), "");
|
||||
@@ -3736,11 +4083,29 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
|
||||
// ...and the stage still transpiles, which is the whole point of declining.
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
{ // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles.
|
||||
Vector<Uint32> 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());
|
||||
}
|
||||
{ // Unprintable but carriable: baked narrow here, then widened into the carrier, which is
|
||||
// what finally gives the declaration a qualifier ES accepts.
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_NE(baked, spirv) << "a format the widening carries must reach the module";
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(baked));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(baked, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
const String essl = DecompileToEssl(widened);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("rgba8ui"), String::npos)
|
||||
<< "the baked r8ui must come out as the core carrier:\n" << essl;
|
||||
}
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
|
||||
@@ -4064,164 +4429,143 @@ TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
|
||||
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter whose layout(offset = N)
|
||||
// puts its last byte past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE is a COMPILE-time error, and the CTS
|
||||
// never links the shader at all. MobileGL only had the rule at link, because the Vulkan-relaxed
|
||||
// parse never reaches glslang's fixOffset().
|
||||
TEST_F(ProgramUtilTest, AtomicCounterOffsetCeilingIsCheckedAtCompile) {
|
||||
// KHR-GL43.shader_image_size.advanced-nonMS-* is nothing but its passing twin basic-nonMS-* plus a
|
||||
// GLSL subroutine, and glslang refuses the keyword outright when the target is SPIR-V ("subroutine
|
||||
// : not allowed when generating SPIR-V"), so every stage of those shaders failed to compile. The
|
||||
// lowering turns a subroutine uniform with exactly ONE compatible subroutine - the case where GL
|
||||
// 4.3 core 7.9 makes a direct call indistinguishable from a dispatch, because every legal value of
|
||||
// the uniform selects that one function - into a forwarding call.
|
||||
TEST_F(ProgramUtilTest, PreprocessLowersSingleImplementationSubroutineToAForwardingCall) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const auto violation = [](const String& body) {
|
||||
return FindAtomicCounterOffsetViolation("#version 430 core\n" + body + "void main() {}\n");
|
||||
};
|
||||
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
|
||||
String source = R"(#version 430 core
|
||||
layout(binding = 0, rgba32i) writeonly uniform iimage2D g_result;
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
void main() {
|
||||
int coord = gl_VertexID;
|
||||
g_func(coord);
|
||||
}
|
||||
subroutine(FuncType) void Func0(int coord) {
|
||||
imageStore(g_result, ivec2(coord, 0), ivec4(imageSize(g_result), 0, 0));
|
||||
}
|
||||
)";
|
||||
const SizeT mainLine = std::count(source.begin(), source.begin() + source.find("void main"), '\n');
|
||||
|
||||
// The boundary itself: the last counter that still fits, and the first that does not.
|
||||
EXPECT_FALSE(violation("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n").has_value());
|
||||
EXPECT_TRUE(violation("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n").has_value());
|
||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||
|
||||
// An array occupies one word per element, so what has to fit is the LAST one.
|
||||
EXPECT_FALSE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
|
||||
") uniform atomic_uint c[4];\n")
|
||||
.has_value());
|
||||
EXPECT_TRUE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
||||
") uniform atomic_uint c[4];\n")
|
||||
.has_value());
|
||||
EXPECT_EQ(source.find("subroutine"), String::npos) << "the keyword glslang refuses must be gone";
|
||||
EXPECT_NE(source.find("void g_func(int mgl_sr_arg0);"), String::npos)
|
||||
<< "the subroutine uniform becomes a prototype under its own name, so call sites stand";
|
||||
EXPECT_NE(source.find("g_func(coord);"), String::npos) << "the call site is untouched";
|
||||
EXPECT_NE(source.find("void Func0(int coord)"), String::npos)
|
||||
<< "the compatible subroutine keeps its body and only sheds the qualifier";
|
||||
EXPECT_NE(source.find("Func0(mgl_sr_arg0);"), String::npos) << "the forwarding body";
|
||||
// The forwarding body has to come after every definition it names: the CTS shaders define
|
||||
// their subroutine BELOW the function that calls through the uniform.
|
||||
EXPECT_LT(source.find("void Func0(int coord)"), source.find("Func0(mgl_sr_arg0);"));
|
||||
// Blanking preserves newlines, and the prototype is single-line, so glslang's diagnostics still
|
||||
// point at the line the application wrote.
|
||||
EXPECT_EQ(std::count(source.begin(), source.begin() + source.find("void main"), '\n'), mainLine)
|
||||
<< "the rewrite must not move a single line";
|
||||
|
||||
// An offset that is not a multiple of 4 (GL 4.6 core 7.7), and one that is.
|
||||
EXPECT_TRUE(violation("layout(offset = 2) uniform atomic_uint c;\n").has_value());
|
||||
EXPECT_FALSE(violation("layout(offset = 8) uniform atomic_uint c;\n").has_value());
|
||||
|
||||
// Things the scanner must NOT judge: a counter with no explicit offset, an `offset` that is
|
||||
// an ordinary identifier rather than a layout qualifier, an array sized by an expression,
|
||||
// and an offset qualifier that belongs to a different declaration.
|
||||
EXPECT_FALSE(violation("uniform atomic_uint c;\nconst int offset = 99999;\n").has_value());
|
||||
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(offset = " + maxSize +
|
||||
") uniform atomic_uint c[kCount];\n")
|
||||
.has_value());
|
||||
EXPECT_FALSE(violation("layout(offset = " + maxSize + ") uniform Block { int x; };\n"
|
||||
"uniform atomic_uint c;\n")
|
||||
.has_value());
|
||||
// A source with no counter at all never pays for the scan and never reports one.
|
||||
EXPECT_FALSE(FindAtomicCounterOffsetViolation("#version 430 core\nvoid main() {}\n").has_value());
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
|
||||
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
|
||||
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
|
||||
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
|
||||
// The identical defect sat on every array dimension and on layout(binding = N).
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
|
||||
// The forwarding function is rebuilt from the subroutine TYPE declaration, so it has to carry the
|
||||
// parameter qualifiers and array shapes across (an parameter that arrives by value writes
|
||||
// nothing back) and has to return the forwarded value for a non-void subroutine.
|
||||
TEST_F(ProgramUtilTest, PreprocessSubroutineForwardingKeepsParameterQualifiersAndReturnsValues) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 0xA) uniform vec4 hexLower;
|
||||
layout(location = 0X1f) uniform vec4 hexUpper;
|
||||
layout(location = 010) uniform vec4 octal;
|
||||
layout(location = 3u) uniform vec4 unsignedSuffix;
|
||||
layout(location = 0x2) uniform float hexArray[0x3];
|
||||
layout(location = 1.0) uniform vec4 notAnInteger;
|
||||
layout(location = 7f) uniform vec4 unknownSuffix;
|
||||
void main() {}
|
||||
String source = R"(#version 430 core
|
||||
subroutine float Blend(const int k, out vec4 rgba, float weights[2]);
|
||||
subroutine uniform Blend g_blend;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 rgba;
|
||||
float w[2] = float[2](0.25, 0.75);
|
||||
fragColor = rgba * g_blend(1, rgba, w);
|
||||
}
|
||||
subroutine(Blend) float Mix(const int k, out vec4 rgba, float weights[2]) {
|
||||
rgba = vec4(weights[0], weights[1], float(k), 1.0);
|
||||
return weights[0];
|
||||
}
|
||||
)";
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||
ASSERT_EQ(locations.count("hexLower"), 1u);
|
||||
EXPECT_EQ(locations.at("hexLower"), 10);
|
||||
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
||||
EXPECT_EQ(locations.at("hexUpper"), 31);
|
||||
ASSERT_EQ(locations.count("octal"), 1u);
|
||||
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
||||
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
||||
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
||||
ASSERT_EQ(locations.count("hexArray"), 1u);
|
||||
EXPECT_EQ(locations.at("hexArray"), 2);
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
|
||||
EXPECT_EQ(locations.count("notAnInteger"), 0u);
|
||||
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
|
||||
EXPECT_NE(source.find("float g_blend(const int mgl_sr_arg0, out vec4 mgl_sr_arg1, float mgl_sr_arg2 [ 2 ]);"),
|
||||
String::npos)
|
||||
<< "qualifiers and the array declarator have to survive, under generated names";
|
||||
EXPECT_NE(source.find("return Mix(mgl_sr_arg0, mgl_sr_arg1, mgl_sr_arg2);"), String::npos)
|
||||
<< "a non-void subroutine has to have its value forwarded back";
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
|
||||
// in the same statement starts at the wrong location.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
|
||||
// Two compatible subroutines is genuine dynamic selection, which MobileGL does not implement:
|
||||
// glUniformSubroutinesuiv is still a stub and nothing reflects the subroutine interfaces. Pinning
|
||||
// such a shader to one of the alternatives would render silently wrong, so the whole rewrite is
|
||||
// abandoned and the source is left exactly as it arrived.
|
||||
TEST_F(ProgramUtilTest, PreprocessLeavesMultiImplementationSubroutinesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
|
||||
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
|
||||
ASSERT_EQ(locations.count("first"), 1u);
|
||||
EXPECT_EQ(locations.at("first"), 50);
|
||||
ASSERT_EQ(locations.count("second"), 1u);
|
||||
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
|
||||
String source = R"(#version 430 core
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
g_func(1);
|
||||
fragColor = vec4(1.0);
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
||||
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
|
||||
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
|
||||
// keys both records missed the map entirely and were first-fitted from location 0.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 2) uniform float two_d[2][3];
|
||||
layout(location = 20) uniform float three_d[2][2][4];
|
||||
layout(location = 40) uniform float one_d[3];
|
||||
void main() {}
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
subroutine(FuncType) void Func1(int coord) { fragColor = vec4(float(coord) * 2.0); }
|
||||
)";
|
||||
const String before = source;
|
||||
|
||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
||||
ASSERT_EQ(locations.count("two_d"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d"), 2);
|
||||
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
||||
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
||||
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
||||
|
||||
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
||||
ASSERT_EQ(locations.count("three_d"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
||||
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
||||
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
||||
|
||||
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
||||
ASSERT_EQ(locations.count("one_d"), 1u);
|
||||
EXPECT_EQ(locations.at("one_d"), 40);
|
||||
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
||||
|
||||
// The declarator after an array-of-arrays still advances by the WHOLE element count.
|
||||
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
|
||||
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
|
||||
ASSERT_EQ(pair.count("b"), 1u);
|
||||
EXPECT_EQ(pair.at("b"), 6);
|
||||
EXPECT_EQ(source, before) << "an unimplementable dispatch must not be quietly pinned to one arm";
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
|
||||
// as the location one, and losing it costs the sampler its initial texture unit.
|
||||
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
|
||||
// An ARRAY of subroutine uniforms indexes the dispatch at the call site ("g_func[i](x)"), which is
|
||||
// the same dynamic selection - and a subroutine declared inside a #if arm cannot be reasoned about
|
||||
// at all, because the forwarding bodies this appends are unconditional.
|
||||
TEST_F(ProgramUtilTest, PreprocessLeavesArrayAndConditionalSubroutinesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String source = R"(#version 430 core
|
||||
layout(binding = 0x2) uniform sampler2D hexUnit;
|
||||
layout(binding = 012) uniform sampler2D octalUnit;
|
||||
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
||||
void main() {}
|
||||
String arrayed = R"(#version 430 core
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func[2];
|
||||
out vec4 fragColor;
|
||||
void main() { g_func[0](1); fragColor = vec4(1.0); }
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
)";
|
||||
const String arrayedBefore = arrayed;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, arrayed);
|
||||
EXPECT_EQ(arrayed, arrayedBefore) << "an arrayed subroutine uniform is a dispatch, not a call";
|
||||
|
||||
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
|
||||
ASSERT_EQ(bindings.count("hexUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("hexUnit"), 2u);
|
||||
ASSERT_EQ(bindings.count("octalUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
||||
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
|
||||
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
|
||||
String conditional = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
#ifdef USE_SUBROUTINE
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
#endif
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
)";
|
||||
const String conditionalBefore = conditional;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, conditional);
|
||||
EXPECT_EQ(conditional, conditionalBefore)
|
||||
<< "an inactive #if arm must not have an unconditional forwarding body appended for it";
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1374,7 +1431,10 @@ TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
|
||||
{SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32},
|
||||
{SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT},
|
||||
{SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM},
|
||||
{SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32},
|
||||
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2:
|
||||
// the view format and the image format have to name the same bit layout, and
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV is A2B10G10R10. A2R10G10B10 transposes R and B.
|
||||
{SpvImageFormatRgb10A2, VK_FORMAT_A2B10G10R10_UNORM_PACK32},
|
||||
{SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM},
|
||||
{SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM},
|
||||
{SpvImageFormatR16, VK_FORMAT_R16_UNORM},
|
||||
@@ -1397,7 +1457,7 @@ TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
|
||||
{SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT},
|
||||
{SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT},
|
||||
{SpvImageFormatR32ui, VK_FORMAT_R32_UINT},
|
||||
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32},
|
||||
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2B10G10R10_UINT_PACK32},
|
||||
{SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT},
|
||||
{SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT},
|
||||
{SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT},
|
||||
|
||||
@@ -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,12 +8,15 @@ add_executable(
|
||||
FixIterationRPSubgroupScratchTest.cpp
|
||||
EmulateSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenFloat64StorageBlockTest.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeStorageBlockArrayIndexTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
WidenImageFormatsTest.cpp
|
||||
GlslangCaptureTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/GlslangCaptureTest.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
|
||||
|
||||
// WHAT SURVIVES MOBILEGL'S PARSE, ASKED OF GLSLANG ITSELF.
|
||||
//
|
||||
// Every shader is parsed as an EShClientVulkan client under
|
||||
// setEnvInputVulkanRulesRelaxed(), which destroys some of the GL declarations MobileGL
|
||||
// still has to answer for. Which ones it destroys - and WHERE - decides whether a piece of
|
||||
// information can be captured from glslang at all or has to be reconstructed. That question
|
||||
// used to be answered by comments; these cases answer it by running the real pipeline and
|
||||
// reading the real qualifiers back.
|
||||
//
|
||||
// The probe drives ShaderCompiler::CompileShader (the production parse configuration, byte
|
||||
// for byte) and then the production mapIO, with a resolver that snapshots every entity's
|
||||
// qualifier AT THE COLLECT CALLBACK - which is before glslang's IO mapper writes its
|
||||
// auto-assigned bindings back into the types (iomapper.cpp:240). That callback is the last
|
||||
// moment at which "the shader declared this" and "glslang invented this" are still
|
||||
// different statements.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
|
||||
namespace {
|
||||
// One entity as the collect callback sees it.
|
||||
struct ProbedEntity {
|
||||
Bool hasBinding = false;
|
||||
Uint binding = 0;
|
||||
Bool hasLocation = false;
|
||||
Int location = 0;
|
||||
Bool isBlock = false;
|
||||
Bool isBufferBlock = false;
|
||||
Bool isSampler = false;
|
||||
};
|
||||
|
||||
// A pass-through resolver that records instead of deciding. It derives from the SAME
|
||||
// base MobileGL ships (TDefaultGlslIoResolver) so the callbacks fire in the same order
|
||||
// and with the same arguments the production resolver sees.
|
||||
class ProbeResolver : public glslang::TDefaultGlslIoResolver {
|
||||
public:
|
||||
explicit ProbeResolver(const glslang::TProgram& program, const EShLanguage stage)
|
||||
: TDefaultGlslIoResolver(*program.getIntermediate(stage)) {}
|
||||
|
||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override {
|
||||
Record(ent);
|
||||
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||
}
|
||||
|
||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override {
|
||||
Record(ent);
|
||||
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
|
||||
}
|
||||
|
||||
std::map<String, ProbedEntity> probed;
|
||||
|
||||
private:
|
||||
void Record(const glslang::TVarEntryInfo& ent) {
|
||||
const glslang::TType& type = ent.symbol->getType();
|
||||
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||
ProbedEntity& record = probed[ent.symbol->getAccessName().c_str()];
|
||||
record.hasBinding = qualifier.hasBinding();
|
||||
record.binding = qualifier.hasBinding() ? qualifier.layoutBinding : 0u;
|
||||
record.hasLocation = qualifier.hasLocation();
|
||||
record.location = qualifier.hasLocation() ? static_cast<Int>(qualifier.layoutLocation) : -1;
|
||||
record.isBlock = type.getBasicType() == glslang::EbtBlock;
|
||||
record.isBufferBlock = record.isBlock && qualifier.storage == glslang::EvqBuffer;
|
||||
record.isSampler = type.getBasicType() == glslang::EbtSampler;
|
||||
}
|
||||
};
|
||||
|
||||
// Parses `source` exactly as production does, links it, and returns what the collect
|
||||
// callback saw. Fails the calling test (through the ASSERT_* the caller applies to the
|
||||
// optional) rather than throwing.
|
||||
std::optional<std::map<String, ProbedEntity>> ProbeShader(const GLenum stage, const String& source,
|
||||
String& outLog) {
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!shaderResult) {
|
||||
outLog = shaderResult.error().log;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto program = MakeShared<glslang::TProgram>();
|
||||
program->addShader(shaderResult.value().get());
|
||||
if (!program->link(EShMsgDefault)) {
|
||||
outLog = program->getInfoLog();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const EShLanguage lang = MG_Util::ConvertGLEnumToEShLanguage(stage);
|
||||
ProbeResolver resolver(*program, lang);
|
||||
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||
if (!program->mapIO(&resolver, ioMapper.get())) {
|
||||
outLog = program->getInfoLog();
|
||||
return std::nullopt;
|
||||
}
|
||||
return resolver.probed;
|
||||
}
|
||||
|
||||
// What ONE production link captures, taken through the real entry points rather than
|
||||
// through a probe: ShaderCompiler::CompileShader and ShaderCompiler::LinkProgram with the
|
||||
// same ProgramAttrib ProgramLinkTask builds. `captureEnabled` false leaves both OUT
|
||||
// pointers null, which is the negative control every capture case below pairs itself with.
|
||||
struct LinkCapture {
|
||||
Bool linked = false;
|
||||
String log;
|
||||
UnorderedMap<String, Uint> opaqueBindings;
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
UnorderedMap<String, Int> uniformLocations;
|
||||
};
|
||||
|
||||
LinkCapture CaptureFromLink(const Vector<Pair<GLenum, String>>& stages, const Bool captureEnabled = true) {
|
||||
LinkCapture capture;
|
||||
ProgramAttrib programAttrib;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!shaderResult) {
|
||||
capture.log = shaderResult.error().log;
|
||||
return capture;
|
||||
}
|
||||
for (const auto& [name, location] : CollectExplicitUniformLocations(*shaderResult.value())) {
|
||||
capture.uniformLocations.emplace(name, location);
|
||||
}
|
||||
programAttrib.shaders.push_back(shaderResult.value());
|
||||
}
|
||||
|
||||
if (captureEnabled) {
|
||||
programAttrib.explicitOpaqueUniformBindings = &capture.opaqueBindings;
|
||||
programAttrib.storageBlocksWithoutBinding = &capture.storageBlocksWithoutBinding;
|
||||
}
|
||||
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!programResult) {
|
||||
capture.log = programResult.error().log;
|
||||
return capture;
|
||||
}
|
||||
capture.linked = true;
|
||||
return capture;
|
||||
}
|
||||
|
||||
LinkCapture CaptureFromCompute(const String& source, const Bool captureEnabled = true) {
|
||||
return CaptureFromLink({{GL_COMPUTE_SHADER, source}}, captureEnabled);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class GlslangCaptureProbeTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
// THE HEADLINE ANSWER, and it contradicts what ExtractExplicitOpaqueBindings' header claimed
|
||||
// for years ("the Vulkan-client relaxed parse strips these before mapIO can capture them").
|
||||
//
|
||||
// A PLAIN sampler/image uniform never enters vkRelaxedRemapUniformVariable's body at all: the
|
||||
// guard at ParseHelper.cpp:8255-8259 admits only types that containsNonOpaque(), atomic_uint,
|
||||
// or a sampler inside a STRUCT. So the binding is still on the qualifier when the IO mapper
|
||||
// collects it, and it is glslang - not a lexer - that knows the answer.
|
||||
//
|
||||
// The default-block uniform LOCATION is the opposite verdict, and this case pins both halves
|
||||
// side by side so neither can be assumed from the other: it is stripped inside that same
|
||||
// function (ParseHelper.cpp:8261-8263, `layoutLocation = layoutLocationEnd`), which is why
|
||||
// recovering it needs a snapshot taken INSIDE glslang rather than a resolver callback.
|
||||
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsSurviveTheRelaxedParseButPlainUniformLocationsDoNot) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 3) uniform sampler2D probeSampler;
|
||||
layout(binding = 5, rgba32f) uniform image2D probeImage;
|
||||
layout(location = 7) uniform vec4 probeUniform;
|
||||
layout(std430, binding = 2) buffer BoundBlock { uint bound; } boundInstance;
|
||||
layout(std430) buffer UnboundBlock { uint unbound; } unboundInstance;
|
||||
void main() {
|
||||
unboundInstance.unbound = boundInstance.bound + uint(texture(probeSampler, vec2(0)).x) +
|
||||
uint(imageLoad(probeImage, ivec2(0)).x) + uint(probeUniform.x);
|
||||
}
|
||||
)";
|
||||
|
||||
String log;
|
||||
const auto probed = ProbeShader(GL_COMPUTE_SHADER, source, log);
|
||||
ASSERT_TRUE(probed.has_value()) << log;
|
||||
|
||||
ASSERT_TRUE(probed->contains("probeSampler"));
|
||||
EXPECT_TRUE(probed->at("probeSampler").hasBinding)
|
||||
<< "a plain sampler's layout(binding=) is NOT stripped by the relaxed parse";
|
||||
EXPECT_EQ(probed->at("probeSampler").binding, 3u);
|
||||
|
||||
ASSERT_TRUE(probed->contains("probeImage"));
|
||||
EXPECT_TRUE(probed->at("probeImage").hasBinding)
|
||||
<< "images take the same path as samplers (both are EbtSampler)";
|
||||
EXPECT_EQ(probed->at("probeImage").binding, 5u);
|
||||
|
||||
// The default-block uniform is gone from the entity list entirely - it was swept into
|
||||
// MGL_GLOBAL_UBO - and even if it were here it would carry layoutLocationEnd. That is
|
||||
// exactly why the location capture has to happen inside glslang.
|
||||
if (probed->contains("probeUniform")) {
|
||||
EXPECT_FALSE(probed->at("probeUniform").hasLocation)
|
||||
<< "vkRelaxedRemapUniformVariable strips a default-block uniform's location";
|
||||
}
|
||||
}
|
||||
|
||||
// The other half of the storage-block question: at the collect callback, "declared no
|
||||
// binding" is still distinguishable from "glslang picked one", which is what makes
|
||||
// TMglGlslIoResolver the right place to recover GL's binding-0 default. Ten lines later
|
||||
// (iomapper.cpp:240) both blocks carry a number and nothing can tell them apart.
|
||||
TEST_F(GlslangCaptureProbeTest, StorageBlockBindingPresenceIsStillTruthfulAtTheCollectCallback) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 2) buffer BoundBlock { uint bound; } boundInstance;
|
||||
layout(std430) buffer UnboundBlock { uint unbound; } unboundInstance;
|
||||
layout(std140) uniform UniformBlock { uint u; } uniformInstance;
|
||||
void main() { unboundInstance.unbound = boundInstance.bound + uniformInstance.u; }
|
||||
)";
|
||||
|
||||
String log;
|
||||
const auto probed = ProbeShader(GL_COMPUTE_SHADER, source, log);
|
||||
ASSERT_TRUE(probed.has_value()) << log;
|
||||
|
||||
ASSERT_TRUE(probed->contains("BoundBlock"));
|
||||
EXPECT_TRUE(probed->at("BoundBlock").isBufferBlock);
|
||||
EXPECT_TRUE(probed->at("BoundBlock").hasBinding);
|
||||
EXPECT_EQ(probed->at("BoundBlock").binding, 2u);
|
||||
|
||||
ASSERT_TRUE(probed->contains("UnboundBlock"));
|
||||
EXPECT_TRUE(probed->at("UnboundBlock").isBufferBlock);
|
||||
EXPECT_FALSE(probed->at("UnboundBlock").hasBinding)
|
||||
<< "an unqualified storage block must still read as unqualified here";
|
||||
|
||||
// A uniform block is a different binding space with its own default path; the capture
|
||||
// must be able to tell the two apart, which storage == EvqBuffer does.
|
||||
ASSERT_TRUE(probed->contains("UniformBlock"));
|
||||
EXPECT_FALSE(probed->at("UniformBlock").isBufferBlock);
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
// THE CAPTURES THEMSELVES.
|
||||
//
|
||||
// Every case below is the SCENARIO of a scan these captures replaced, re-pointed at the new
|
||||
// mechanism. Keeping the scenarios is the point: the interesting inputs were found the
|
||||
// expensive way (a production regression, a CTS failure), and they are still the inputs that
|
||||
// decide whether the recovery is right - what changed is only who answers.
|
||||
//
|
||||
// Every capture also has a NEGATIVE CONTROL: the same shader with the capture switched off,
|
||||
// asserting the answer disappears. Without one, a case that passes proves only that SOMETHING
|
||||
// produced the number.
|
||||
// ===========================================================================================
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style,
|
||||
// so layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The lexical extractor
|
||||
// this replaces had to implement that rule itself, got it wrong for both spellings, and was
|
||||
// then fixed - twice. glslang has always had it, because it is the GLSL lexer.
|
||||
TEST_F(GlslangCaptureProbeTest, UniformLocationsCarryNonDecimalIntegerLiterals) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 0xA) uniform vec4 hexLower;
|
||||
layout(location = 0X1f) uniform vec4 hexUpper;
|
||||
layout(location = 010) uniform vec4 octal;
|
||||
layout(location = 3u) uniform vec4 unsignedSuffix;
|
||||
layout(location = 0x2) uniform float hexArray[0x3];
|
||||
void main() {
|
||||
gl_Position = hexLower + hexUpper + octal + unsignedSuffix + vec4(hexArray[2]);
|
||||
}
|
||||
)";
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||
ASSERT_EQ(locations.count("hexLower"), 1u);
|
||||
EXPECT_EQ(locations.at("hexLower"), 10);
|
||||
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
||||
EXPECT_EQ(locations.at("hexUpper"), 31);
|
||||
ASSERT_EQ(locations.count("octal"), 1u);
|
||||
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
||||
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
||||
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
||||
ASSERT_EQ(locations.count("hexArray"), 1u);
|
||||
EXPECT_EQ(locations.at("hexArray"), 2);
|
||||
}
|
||||
|
||||
// The counterweight the lexical version needed a rule for: a location that is not an integer
|
||||
// literal at all. glslang REJECTS those outright rather than skipping them, which is what GLSL
|
||||
// says should happen - the scanner could only decline to record them and let the declaration
|
||||
// compile with no location.
|
||||
TEST_F(GlslangCaptureProbeTest, ANonIntegralUniformLocationIsRejectedRatherThanIgnored) {
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER,
|
||||
.sourceStr = "#version 430 core\nlayout(location = 1.0) uniform vec4 notAnInteger;\n"
|
||||
"void main() { gl_Position = notAnInteger; }\n"};
|
||||
EXPECT_FALSE(ShaderCompiler::CompileShader(attrib).has_value())
|
||||
<< "a float location is a compile-time error, not a declaration without a location";
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
||||
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the location assigner resolves such a name by
|
||||
// stripping the single trailing "[0]" - so the map has to answer "u[1]", not just "u". The
|
||||
// synthesized keys are the one piece of the old extractor that survived the migration, because
|
||||
// they are a REFLECTION-NAME mapping rather than a reading of the source.
|
||||
TEST_F(GlslangCaptureProbeTest, UniformLocationsExpandArrayOfArraysElements) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 2) uniform float two_d[2][3];
|
||||
layout(location = 20) uniform float three_d[2][2][4];
|
||||
layout(location = 40) uniform float one_d[3];
|
||||
void main() { gl_Position = vec4(two_d[1][2] + three_d[1][1][3] + one_d[2]); }
|
||||
)";
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||
|
||||
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
||||
ASSERT_EQ(locations.count("two_d"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d"), 2);
|
||||
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
||||
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
||||
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
||||
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
||||
|
||||
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
||||
ASSERT_EQ(locations.count("three_d"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
||||
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
||||
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
||||
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
||||
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
||||
|
||||
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
||||
ASSERT_EQ(locations.count("one_d"), 1u);
|
||||
EXPECT_EQ(locations.at("one_d"), 40);
|
||||
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
||||
}
|
||||
|
||||
// A DELIBERATE BEHAVIOUR CHANGE, recorded here because it is the one place the migration does
|
||||
// not reproduce the old answer.
|
||||
//
|
||||
// The lexical extractor advanced the location across the declarators of one statement, so
|
||||
// `layout(location = 50) uniform float first[3], second;` gave second = 53. GLSL has no such
|
||||
// rule: 4.60 4.4 says a layout qualifier applies to THE DECLARATION, i.e. identically to every
|
||||
// declarator in it, and 4.4.3 then makes two uniforms sharing a location an error. glslang - the
|
||||
// reference front end - assigns 50 to both, and the CTS never exercises the form at all (its
|
||||
// generator emits one uniform per declaration, es31cExplicitUniformLocationTest.cpp
|
||||
// streamDefinition). The advance was an invention of the scanner; this is what the parser says.
|
||||
TEST_F(GlslangCaptureProbeTest, EveryDeclaratorOfOneStatementCarriesTheQualifiersLocation) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 50) uniform float first[0x3], second;
|
||||
void main() { gl_Position = vec4(first[2] + second); }
|
||||
)";
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||
|
||||
ASSERT_EQ(locations.count("first"), 1u);
|
||||
EXPECT_EQ(locations.at("first"), 50) << "0x3 is three elements, not zero and not three hundred";
|
||||
ASSERT_EQ(locations.count("second"), 1u);
|
||||
EXPECT_EQ(locations.at("second"), 50);
|
||||
}
|
||||
|
||||
// THE NEGATIVE CONTROL for the uniform-location capture: nothing else in the parsed module
|
||||
// knows the number. If the snapshot inside vkRelaxedRemapUniformVariable were removed, this is
|
||||
// the state the location assigner would be left with - no qualifier, no reflection entry, and
|
||||
// therefore a first-fit location that has nothing to do with what the shader declared.
|
||||
TEST_F(GlslangCaptureProbeTest, WithoutTheSnapshotAPlainUniformsLocationIsNowhereInTheModule) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(location = 7) uniform vec4 tint;
|
||||
void main() { gl_Position = tint; }
|
||||
)";
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||
|
||||
// The capture, on.
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||
ASSERT_EQ(locations.count("tint"), 1u);
|
||||
EXPECT_EQ(locations.at("tint"), 7);
|
||||
|
||||
// The capture, off - i.e. everything the module itself can still say. The uniform is a
|
||||
// member of MGL_GLOBAL_UBO by now, and no symbol in the module carries location 7.
|
||||
auto program = MakeShared<glslang::TProgram>();
|
||||
program->addShader(shaderResult.value().get());
|
||||
ASSERT_TRUE(program->link(EShMsgDefault)) << program->getInfoLog();
|
||||
ASSERT_TRUE(program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
|
||||
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO));
|
||||
for (Int i = 0; i < program->getNumUniformVariables(); ++i) {
|
||||
const auto& uniform = program->getUniform(i);
|
||||
if (uniform.name != "tint") continue;
|
||||
// Copied out: layoutLocationEnd is a static const with no out-of-line definition, so
|
||||
// binding it to EXPECT_EQ's const reference would ODR-use it and fail to link.
|
||||
const Uint noLocation = glslang::TQualifier::layoutLocationEnd;
|
||||
EXPECT_EQ(uniform.layoutLocation(), noLocation)
|
||||
<< "if reflection could answer this, the glslang patch would be unnecessary";
|
||||
}
|
||||
}
|
||||
|
||||
// AN OPAQUE uniform's explicit location must land in the same map as a plain one's, even
|
||||
// though it is the one kind the relaxed remap never touches and reflection could therefore
|
||||
// answer for. Found by running the retired scanner beside this capture over the whole corpus:
|
||||
// the scanner recorded these (it did not read types at all) and the first cut of the capture
|
||||
// did not, which would have demoted a declared location to an implementation-chosen one.
|
||||
//
|
||||
// The difference is not cosmetic. DoReflection marks everything in this map SOURCE-EXPLICIT,
|
||||
// which is what makes a collision a LINK ERROR under ARB_explicit_uniform_location; a location
|
||||
// arriving only through glslang's own layoutLocation() is treated as glslang's choice and
|
||||
// quietly moved out of the way instead.
|
||||
TEST_F(GlslangCaptureProbeTest, AnOpaqueUniformsExplicitLocationIsCapturedAlongsideAPlainOnes) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(location = 7) uniform sampler2D uTex;
|
||||
layout(location = 11) uniform mat4 uMvp;
|
||||
layout(location = 20) uniform sampler2D uTexArray[3];
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = texture(uTex, uMvp[0].xy) + texture(uTexArray[1], vec2(0)); }
|
||||
)";
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||
|
||||
ASSERT_EQ(locations.count("uTex"), 1u) << "the opaque half of the capture is missing";
|
||||
EXPECT_EQ(locations.at("uTex"), 7);
|
||||
ASSERT_EQ(locations.count("uMvp"), 1u) << "the snapshot half of the capture is missing";
|
||||
EXPECT_EQ(locations.at("uMvp"), 11);
|
||||
// Keyed by DECLARED name, which is what the reflection lookup reaches by stripping "[0]".
|
||||
ASSERT_EQ(locations.count("uTexArray"), 1u);
|
||||
EXPECT_EQ(locations.at("uTexArray"), 20);
|
||||
}
|
||||
|
||||
// GLSLANG'S SYNTHESIZED ATOMIC-COUNTER BLOCKS ARE NOT UNQUALIFIED STORAGE BLOCKS, however much
|
||||
// they look like one at the collect callback: relaxed parsing folds every atomic_uint into a
|
||||
// "gl_AtomicCounterBlock_<GL binding>" buffer block and leaves it unbound, because MobileGL
|
||||
// asks for auto-mapped bindings. Seeding one to GL binding 0 would overwrite the counter
|
||||
// buffer's real binding - which is the trailing number in that very name.
|
||||
//
|
||||
// Also found by the side-by-side corpus run: the retired scanner could not see these blocks at
|
||||
// all (they do not exist in the source), so the capture inherited a whole class of entries its
|
||||
// consumer was never written for.
|
||||
TEST_F(GlslangCaptureProbeTest, TheSynthesizedAtomicCounterBlocksAreNotCapturedAsStorageBlocks) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0) uniform atomic_uint counterA;
|
||||
layout(binding = 2) uniform atomic_uint counterB;
|
||||
layout(std430) buffer RealBlock { uint u; } realBlock;
|
||||
void main() { realBlock.u = atomicCounterIncrement(counterA) + atomicCounterIncrement(counterB); }
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("RealBlock"), 1u)
|
||||
<< "the application's own unqualified block is still recognised";
|
||||
for (const String& name : capture.storageBlocksWithoutBinding) {
|
||||
EXPECT_FALSE(name.starts_with(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX))
|
||||
<< "a synthesized atomic-counter block reached the storage-block capture: " << name;
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is its initial texture
|
||||
// unit. Same scenario the lexical extractor carried, now answered by the IO resolver - which
|
||||
// gets the C-style literal rules for free, and sees a binding no scanner could have read.
|
||||
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsAreCapturedIncludingNonDecimalAndMacroSpellings) {
|
||||
const String source = R"(#version 430 core
|
||||
#define UNIT_FROM_A_MACRO 5
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0x2) uniform sampler2D hexUnit;
|
||||
layout(binding = 012) uniform sampler2D octalUnit;
|
||||
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
||||
layout(binding = UNIT_FROM_A_MACRO) uniform sampler2D macroUnit;
|
||||
layout(binding = 6) uniform sampler2D arrayUnits[3];
|
||||
uniform sampler2D noUnit;
|
||||
layout(std430, binding = 0) buffer Out { vec4 v; } o;
|
||||
void main() {
|
||||
o.v = texture(hexUnit, vec2(0)) + texture(octalUnit, vec2(0)) + texture(suffixedUnit, vec2(0)) +
|
||||
texture(macroUnit, vec2(0)) + texture(arrayUnits[1], vec2(0)) + texture(noUnit, vec2(0));
|
||||
}
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
ASSERT_EQ(capture.opaqueBindings.count("hexUnit"), 1u);
|
||||
EXPECT_EQ(capture.opaqueBindings.at("hexUnit"), 2u);
|
||||
ASSERT_EQ(capture.opaqueBindings.count("octalUnit"), 1u);
|
||||
EXPECT_EQ(capture.opaqueBindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
||||
ASSERT_EQ(capture.opaqueBindings.count("suffixedUnit"), 1u);
|
||||
EXPECT_EQ(capture.opaqueBindings.at("suffixedUnit"), 1u);
|
||||
// The whole reason the capture moved: the AST sees expanded text.
|
||||
ASSERT_EQ(capture.opaqueBindings.count("macroUnit"), 1u)
|
||||
<< "a unit spelled as a macro is a declared unit like any other";
|
||||
EXPECT_EQ(capture.opaqueBindings.at("macroUnit"), 5u);
|
||||
// An array is keyed by its declared name, which is what the reflection lookup strips "[0]"
|
||||
// to reach.
|
||||
ASSERT_EQ(capture.opaqueBindings.count("arrayUnits"), 1u);
|
||||
EXPECT_EQ(capture.opaqueBindings.at("arrayUnits"), 6u);
|
||||
// Reported POSITIVELY: a sampler that declared no unit must not appear at all, or it would
|
||||
// be given one it never asked for.
|
||||
EXPECT_EQ(capture.opaqueBindings.count("noUnit"), 0u);
|
||||
}
|
||||
|
||||
// THE NEGATIVE CONTROL for the opaque-binding capture.
|
||||
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsDisappearWhenTheResolverCaptureIsOff) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 3) uniform sampler2D unit;
|
||||
layout(std430, binding = 0) buffer Out { vec4 v; } o;
|
||||
void main() { o.v = texture(unit, vec2(0)); }
|
||||
)";
|
||||
|
||||
ASSERT_EQ(CaptureFromCompute(source).opaqueBindings.count("unit"), 1u);
|
||||
const LinkCapture off = CaptureFromCompute(source, /*captureEnabled=*/false);
|
||||
ASSERT_TRUE(off.linked) << off.log;
|
||||
EXPECT_TRUE(off.opaqueBindings.empty())
|
||||
<< "nothing but the resolver fills this map; a non-empty result would mean the capture "
|
||||
"is being shadowed by a leftover path";
|
||||
}
|
||||
|
||||
// KHR-GL43.compute_shader.resource-ubo's own shape: an unqualified storage block alongside the
|
||||
// uniform blocks whose presence is what pushes it off binding 0. GL 4.3 core 7.8 puts such a
|
||||
// block on binding ZERO; by the time reflection is built glslang has invented a number and
|
||||
// written it into the qualifier, so this capture is the only surviving record.
|
||||
TEST_F(GlslangCaptureProbeTest, UnqualifiedStorageBlocksAreNamedAndQualifiedOnesAreNot) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std140) uniform InputBuffer { vec4 data[4]; } g_in_buffer[12];
|
||||
layout(std430) buffer OutputBuffer { vec4 data0[4]; } g_out_buffer;
|
||||
layout(std430, binding = 3) buffer BoundBlock { vec4 data1[4]; } g_bound;
|
||||
layout(binding = 5, std430) buffer BoundFirst { vec4 data2[4]; } g_bound_first;
|
||||
void main() {
|
||||
g_out_buffer.data0[0] = g_in_buffer[0].data[0] + g_bound.data1[0] + g_bound_first.data2[0];
|
||||
}
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("OutputBuffer"), 1u)
|
||||
<< "the block the test binds at 0 with glBindBufferBase must be recognised";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BoundBlock"), 0u)
|
||||
<< "a declared binding must never be defaulted away";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BoundFirst"), 0u)
|
||||
<< "the binding may appear anywhere in the layout list, not only last";
|
||||
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, and
|
||||
// its default is already handled where uniformBlockBinding is seeded. Naming it here would
|
||||
// make the seeder default a resource it does not own.
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("InputBuffer"), 0u)
|
||||
<< "uniform blocks are out of scope";
|
||||
}
|
||||
|
||||
// The capture must not mistake a buffer-typed SAMPLER or a member qualifier for a block, and
|
||||
// memory qualifiers in either order must not cost a block its binding - the dangerous
|
||||
// direction, because a false positive here DEFAULTS AWAY a binding the shader really declared.
|
||||
TEST_F(GlslangCaptureProbeTest, StorageBlockCaptureSurvivesMemoryQualifiersAndIgnoresBufferSamplers) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform samplerBuffer texelSampler;
|
||||
layout(std430, binding = 1) coherent restrict buffer AfterLayout { uint a; } afterLayout;
|
||||
readonly layout(std430, binding = 2) buffer BeforeLayout { uint b; } beforeLayout;
|
||||
writeonly buffer NoBindingAtAll { uint c; } noBinding;
|
||||
void main() { noBinding.c = afterLayout.a + beforeLayout.b + uint(texelFetch(texelSampler, 0).x); }
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("AfterLayout"), 0u)
|
||||
<< "coherent/restrict must not break the qualifier run and lose the binding";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BeforeLayout"), 0u)
|
||||
<< "a qualifier may precede the layout list too";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("NoBindingAtAll"), 1u)
|
||||
<< "a memory-qualified block with no binding is still an unqualified block";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("texelSampler"), 0u)
|
||||
<< "a samplerBuffer is not a buffer block";
|
||||
}
|
||||
|
||||
// THE REGRESSION THIS MIGRATION EXISTS FOR (7de7cfc6,
|
||||
// minecraft-1.21.1-neoforge-create-indirect-in-world, both backends). Flywheel's indirect
|
||||
// engine spells every storage-block binding as a macro, and the scan that used to answer this
|
||||
// question ran on MACRO-UNEXPANDED text: MobileGL's preprocessing rewrites the source, it does
|
||||
// not run the C preprocessor, so `binding = _FLW_MODEL_BUFFER_BINDING` reached the scanner
|
||||
// verbatim and "no integer literal" was read as "no binding". All eight blocks were defaulted
|
||||
// onto binding 0 at once, aliased there, and the engine drew nothing.
|
||||
//
|
||||
// It passes here for a structural reason rather than a grammatical one: the IO mapper sees the
|
||||
// declaration the PARSER built, and the parser ran the preprocessor first. No rule about macro
|
||||
// spellings exists anywhere in this path, and none can be forgotten.
|
||||
TEST_F(GlslangCaptureProbeTest, AMacroSpelledStorageBlockBindingIsADeclaredBinding) {
|
||||
// Flywheel's own shape, verbatim in structure: the binding is a macro, the block carries
|
||||
// memory qualifiers, and the macro's definition is still sitting in the text above it.
|
||||
const String source = R"(#version 460 core
|
||||
#define _FLW_MODEL_BUFFER_BINDING 3
|
||||
#define _FLW_DRAW_BUFFER_BINDING 4
|
||||
#define FLW_BINDING binding = 2
|
||||
#define SSBO_QUALIFIER layout(std430, binding = 6) restrict
|
||||
layout(local_size_x = 32) in;
|
||||
layout(std430, binding = _FLW_MODEL_BUFFER_BINDING) restrict readonly buffer ModelBuffer {
|
||||
uint models[];
|
||||
};
|
||||
layout(std430, binding = _FLW_DRAW_BUFFER_BINDING) restrict buffer DrawBuffer {
|
||||
uint draws[];
|
||||
};
|
||||
layout(std430, FLW_BINDING) buffer EntryMacro { uint a; } entryMacro;
|
||||
SSBO_QUALIFIER buffer RunMacro { uint b; } runMacro;
|
||||
layout(std430, row_major) buffer PlainLayout { uint c; } plainLayout;
|
||||
layout(std430) buffer ReallyUnqualified { uint u; } reallyUnqualified;
|
||||
void main() {
|
||||
draws[0] = models[0] + reallyUnqualified.u + entryMacro.a + runMacro.b + plainLayout.c;
|
||||
}
|
||||
)";
|
||||
|
||||
const LinkCapture capture = CaptureFromCompute(source);
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("ModelBuffer"), 0u)
|
||||
<< "a binding spelled as a macro is still a declared binding, never an absent one";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("DrawBuffer"), 0u)
|
||||
<< "every block of the engine would otherwise be defaulted onto 0 together";
|
||||
// The two shapes the scanner could only treat as DOUBT - a macro standing in for a whole
|
||||
// layout entry, and one standing in for the whole qualifier run - are now ordinary.
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("EntryMacro"), 0u)
|
||||
<< "a macro that expands to `binding = N` declares a binding";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("RunMacro"), 0u)
|
||||
<< "a macro standing in for the whole qualifier run carries its binding too";
|
||||
// The counterweight: doubt must not swallow the layout identifiers a buffer block legally
|
||||
// carries, or nothing would ever be defaulted again.
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("PlainLayout"), 1u)
|
||||
<< "std430/row_major are layout identifiers, not bindings";
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("ReallyUnqualified"), 1u)
|
||||
<< "a block that truly declares no binding is still recognised in the same shader";
|
||||
}
|
||||
|
||||
// THE NEGATIVE CONTROL for the storage-block capture.
|
||||
TEST_F(GlslangCaptureProbeTest, UnqualifiedStorageBlocksDisappearWhenTheResolverCaptureIsOff) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430) buffer Unbound { uint u; } unbound;
|
||||
void main() { unbound.u = 1u; }
|
||||
)";
|
||||
|
||||
ASSERT_EQ(CaptureFromCompute(source).storageBlocksWithoutBinding.count("Unbound"), 1u);
|
||||
const LinkCapture off = CaptureFromCompute(source, /*captureEnabled=*/false);
|
||||
ASSERT_TRUE(off.linked) << off.log;
|
||||
EXPECT_TRUE(off.storageBlocksWithoutBinding.empty())
|
||||
<< "nothing but the resolver fills this set; a non-empty result would mean the capture "
|
||||
"is being shadowed by a leftover path";
|
||||
}
|
||||
|
||||
// A block declared in two stages contributes ONCE, and the capture is a union across them -
|
||||
// which is what one resolver serving the whole program gives for free. GLSL requires every
|
||||
// stage that declares a block to declare it identically, so the stages cannot disagree.
|
||||
TEST_F(GlslangCaptureProbeTest, TheStorageBlockCaptureIsAUnionAcrossStages) {
|
||||
const String vertex = R"(#version 430 core
|
||||
layout(std430) buffer SharedBlock { uint u; } sharedInstance;
|
||||
layout(std430) buffer VertexOnly { uint v; } vertexOnly;
|
||||
void main() { gl_Position = vec4(float(sharedInstance.u + vertexOnly.v)); }
|
||||
)";
|
||||
const String fragment = R"(#version 430 core
|
||||
layout(std430) buffer SharedBlock { uint u; } sharedInstance;
|
||||
layout(std430, binding = 4) buffer FragmentBound { uint f; } fragmentBound;
|
||||
out vec4 colour;
|
||||
void main() { colour = vec4(float(sharedInstance.u + fragmentBound.f)); }
|
||||
)";
|
||||
|
||||
const LinkCapture capture =
|
||||
CaptureFromLink({{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, fragment}});
|
||||
ASSERT_TRUE(capture.linked) << capture.log;
|
||||
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("SharedBlock"), 1u);
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("VertexOnly"), 1u);
|
||||
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("FragmentBound"), 0u);
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter at a misaligned offset,
|
||||
// or one whose last byte passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, is a COMPILE-time error
|
||||
// (GL 4.6 core 7.7). glslang enforces the alignment rule in fixOffset(), which the relaxed
|
||||
// parse never reaches - vkRelaxedRemapUniformVariable folds the counter into a synthesized
|
||||
// block and returns from declareVariable() first - and it never enforced the size ceiling at
|
||||
// all. Both now run at that fold (ParseHelper.cpp atomicCounterOffsetCheck), so a violation is
|
||||
// an ordinary parse failure.
|
||||
TEST_F(GlslangCaptureProbeTest, AtomicCounterOffsetRulesAreRaisedByTheParse) {
|
||||
const auto compiles = [](const String& body) {
|
||||
const String source = "#version 430 core\n" + body + "void main() {}\n";
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
return ShaderCompiler::CompileShader(attrib).has_value();
|
||||
};
|
||||
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
|
||||
|
||||
// The boundary itself: the last counter that still fits, and the first that does not.
|
||||
EXPECT_TRUE(compiles("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n"));
|
||||
EXPECT_FALSE(compiles("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n"));
|
||||
|
||||
// An array occupies one word per element, so what has to fit is the LAST one.
|
||||
EXPECT_TRUE(compiles("layout(binding = 0, offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
|
||||
") uniform atomic_uint c[4];\n"));
|
||||
EXPECT_FALSE(compiles("layout(binding = 0, offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
||||
") uniform atomic_uint c[4];\n"));
|
||||
|
||||
// An offset that is not a multiple of 4, and one that is.
|
||||
EXPECT_FALSE(compiles("layout(binding = 0, offset = 2) uniform atomic_uint c;\n"));
|
||||
EXPECT_TRUE(compiles("layout(binding = 0, offset = 8) uniform atomic_uint c;\n"));
|
||||
|
||||
// A counter with no explicit offset has nothing to judge, and neither has a shader with no
|
||||
// counter at all.
|
||||
EXPECT_TRUE(compiles("layout(binding = 0) uniform atomic_uint c;\n"));
|
||||
EXPECT_TRUE(compiles(""));
|
||||
|
||||
// The gain over the scan this replaces: an array sized by a constant EXPRESSION, and an
|
||||
// offset spelled as a macro, are now both judged. The scanner declined both - it read
|
||||
// unexpanded text and only understood integer literals.
|
||||
EXPECT_FALSE(compiles("const int kCount = 4;\nlayout(binding = 0, offset = " +
|
||||
std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
||||
") uniform atomic_uint c[kCount];\n"));
|
||||
EXPECT_FALSE(compiles("#define BAD_OFFSET 2\nlayout(binding = 0, offset = BAD_OFFSET) uniform atomic_uint c;\n"));
|
||||
|
||||
// The counterweight: `offset` as an ordinary identifier is not a layout qualifier, and an
|
||||
// offset qualifier on an unrelated declaration must not reach the counter.
|
||||
EXPECT_TRUE(compiles("layout(binding = 0) uniform atomic_uint c;\nconst int offset = 99999;\n"));
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeResourceArrayIndexTest.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>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .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 = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// Test-side reference walker, deliberately independent of the production detection so a
|
||||
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
|
||||
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
|
||||
// exactly what the Qualcomm ES compiler refuses.
|
||||
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
|
||||
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpDecorate:
|
||||
if (wordCount >= 3u) {
|
||||
const auto decoration = static_cast<spv::Decoration>(words[2]);
|
||||
if (decoration == spv::Decoration::Block ||
|
||||
decoration == spv::Decoration::BufferBlock) {
|
||||
blockStructs.insert(words[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
// OpTypeArray <result> <element type> <length>
|
||||
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
|
||||
blockArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
// OpTypePointer <result> <storage class> <pointee>
|
||||
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
|
||||
blockArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
// OpVariable <result type> <result> <storage class>
|
||||
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
|
||||
blockArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
// OpAccessChain <result type> <result> <base> <index 0> ...
|
||||
if (wordCount < 5u) return;
|
||||
if (blockArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// The image half of the same reference walker, and equally independent of the production
|
||||
// detection: true when some access chain rooted at an array-of-IMAGES variable carries a
|
||||
// non-constant FIRST index. A UniformConstant array whose element type is an OpTypeImage
|
||||
// with Sampled == 2 is what GLSL spells `image2D g_image[N]`; a sampler array is an
|
||||
// OpTypeSampledImage and is deliberately not matched here, because ESSL allows it a
|
||||
// dynamically-uniform index.
|
||||
bool HasDynamicImageArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> storageImages; // OpTypeImage ids with Sampled == 2
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> imageArrayTypes; // OpTypeArray ids whose element is such an image
|
||||
std::set<Uint32> imageArrayPointers; // OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> imageArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpTypeImage:
|
||||
// OpTypeImage <result> <sampled type> <dim> <depth> <arrayed> <ms> <sampled>
|
||||
if (wordCount >= 8u && words[7] == 2u) storageImages.insert(words[1]);
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
if (wordCount >= 4u && storageImages.count(words[2]) != 0u) {
|
||||
imageArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (wordCount >= 4u && imageArrayTypes.count(words[3]) != 0u) {
|
||||
imageArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
if (wordCount >= 4u && imageArrayPointers.count(words[1]) != 0u) {
|
||||
imageArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
if (wordCount < 5u) return;
|
||||
if (imageArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// The ESSL SPIRV-Cross prints for a module, or the error it refused with. This is where the
|
||||
// rule actually bites: the SPIR-V is legal Vulkan either way, and what a strict ES driver
|
||||
// reads is this text.
|
||||
struct EsslAttempt {
|
||||
Bool succeeded = false;
|
||||
String text;
|
||||
String error;
|
||||
};
|
||||
|
||||
EsslAttempt EmitEssl(const Vector<Uint32>& spirv) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
EsslAttempt attempt;
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) return attempt;
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
if (session.SetOptions(options) != SPVC_SUCCESS) return attempt;
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) {
|
||||
attempt.error = essl.error().log;
|
||||
return attempt;
|
||||
}
|
||||
attempt.succeeded = true;
|
||||
attempt.text = *essl;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
|
||||
// induction variable is a literal after unrolling.
|
||||
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
g_out.data[i] = g_blocks[i].data[0];
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced index - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
|
||||
// can fold it, so the switch/select lowering is what has to carry it.
|
||||
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_blocks[g_index].data[0] = 7u;
|
||||
g_out.value = g_blocks[g_index].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control from the device run: dynamic addressing through an array MEMBER of
|
||||
// ONE block is legal ES and must not be rewritten.
|
||||
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = g_block.data[g_index];
|
||||
}
|
||||
)";
|
||||
|
||||
// A block array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
void main() {
|
||||
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The IMAGE half, and the case that has always been broken independently of any per-element
|
||||
// unit remapping: a plain CONSECUTIVE image array subscripted by a loop variable. This is
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple's own fragment shader shape, and a raw
|
||||
// GLES probe on Mesa 26.1.4 at ES 3.2 refuses the ESSL it produces with "image arrays indexed
|
||||
// with non-constant expressions are forbidden in GLSL ES". Foldable: after unrolling every
|
||||
// subscript is a literal.
|
||||
constexpr const char* kLoopIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
imageStore(g_image[i], ivec2(0), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// The same fold, buried in the loop nest a real image-writing shader has: a tile walk with
|
||||
// the array walk innermost. Every level's trip count is inside the per-loop budget on its
|
||||
// own, so nothing but a NEST budget stops the three from multiplying.
|
||||
constexpr const char* kNestedLoopIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
for (int y = 0; y < 64; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
imageStore(g_image[i], ivec2(x, y), vec4(1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced image index: nothing can fold it, so the switch/select lowering is what
|
||||
// has to carry it. Both directions in one shader, as the block-array fixture does.
|
||||
constexpr const char* kUniformIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform image2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
imageStore(g_image[g_index], ivec2(0), vec4(7.0));
|
||||
g_out.value = imageLoad(g_image[g_index], ivec2(1));
|
||||
}
|
||||
)";
|
||||
|
||||
// An image array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control for the scope decision: ESSL 3.20 4.1.7 allows a SAMPLER array a
|
||||
// dynamically-uniform index, and the same raw GLES probe confirms it - both a loop-variable
|
||||
// subscript and a const-table lookup compile and link. Nothing here may be rewritten.
|
||||
constexpr const char* kUniformIndexedSamplerArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform sampler2D g_tex[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = texture(g_tex[g_index], vec2(0.5));
|
||||
}
|
||||
)";
|
||||
|
||||
// An imageAtomic* reaches the array through OpImageTexelPointer, and running one per element
|
||||
// would perform every other element's atomic as well. The pass has to decline rather than
|
||||
// lower this.
|
||||
// imageSize() on a dynamically indexed image array. The query carries the image in the same
|
||||
// leading operand position as an imageLoad and answers with an int vector, so the select
|
||||
// ladder spells it exactly - and unlike a read it touches no memory at all, so evaluating it
|
||||
// for every element cannot even return undefined data.
|
||||
constexpr const char* kUniformIndexedImageSizeQuery = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform image2D g_image[4];
|
||||
layout(rgba32f, binding = 4) uniform image2DArray g_layered[2];
|
||||
layout(std430, binding = 8) buffer Out { ivec2 size; int layers; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.size = imageSize(g_image[g_index]);
|
||||
g_out.layers = imageSize(g_layered[g_index]).z;
|
||||
}
|
||||
)";
|
||||
constexpr const char* kUniformIndexedImageAtomic = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform uimage2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = imageAtomicAdd(g_image[g_index], ivec2(0), 1u);
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedBlockArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Either half of the legalization is an acceptable outcome here - what the ES driver
|
||||
// cares about is only that no dynamic subscript survives.
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
// One switch for the store, and one select per element past the first for the load.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
// The case no test covered before, and the one that has nothing to do with per-element unit
|
||||
// remapping: an ordinary consecutive image array written from a loop. Every emitted subscript has
|
||||
// to end up a literal, or the ES driver drops the stage and every draw with it.
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedImageArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
// ...and in the text the driver actually reads. Before: `g_image[i]`; after: four literals.
|
||||
const EsslAttempt before = EmitEssl(input);
|
||||
ASSERT_TRUE(before.succeeded) << before.error;
|
||||
EXPECT_NE(before.text.find("g_image[i]"), String::npos) << before.text;
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_EQ(after.text.find("g_image[i]"), String::npos) << after.text;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Marking a loop for unrolling means marking every loop enclosing it - SPIRV-Tools only unrolls
|
||||
// innermost loops, so an outer one is unrollable only once its children are gone - and the copies
|
||||
// those levels produce MULTIPLY. Bounding each loop on its own therefore bounds nothing: with the
|
||||
// per-loop cap alone this nest (64 x 64 x 4, every level inside it) folded to 16384 OpImageWrite,
|
||||
// a 3.68 MB module and 3.6 s of spirv-opt on desktop x86, from twelve lines of GLSL - before
|
||||
// SPIRV-Cross or the device compiler saw any of it. Spending the budget as the walk climbs stops
|
||||
// at the innermost level here, and the switch lowering - whose cost is the ARRAY LENGTH, not the
|
||||
// trip counts - is what legalizes anything the unroll no longer reaches.
|
||||
TEST(LegalizeResourceArrayIndexPass, BoundsTheWholeLoopNestAndNotEachLoopSeparately) {
|
||||
const Vector<Uint32> input = CompileCompute(kNestedLoopIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Still legalized - that is not what is being traded away.
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
// ...and paid for at the budget, not at its cube. 64 is kMaxUnrolledIterations.
|
||||
EXPECT_LE(CountOpcode(output, spv::Op::OpImageWrite), 64u);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedImageWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
// One switch for the imageStore, and one select per element past the first for the imageLoad.
|
||||
// The selection is on the loaded TEXEL, never on the image object - an opaque type cannot be
|
||||
// selected at all - so there is one OpImageRead per element behind those selects.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageRead), 4u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageWrite), 4u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedImageArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// The scope decision, asserted rather than assumed: a sampler array indexed by a uniform is legal
|
||||
// ESSL, so the module must come back untouched - not merely legal, byte for byte the same.
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesADynamicallyIndexedSamplerArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedSamplerArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// imageSize() on a dynamically indexed image array used to lose the whole stage: the consumer
|
||||
// whitelist accepted only OpImageRead/OpImageWrite, so the chain was declined and the illegal
|
||||
// subscript reached the ES compiler intact. It is the same select ladder as a read - the query
|
||||
// takes the image in in-operand 0 and produces an int vector - and it reads no memory, so the
|
||||
// elements the shader did not ask for cost nothing but the instruction.
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedImageSizeQueryToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageSizeQuery);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpImageQuerySize), 2u);
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSelect), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
// Four elements for g_image and two for g_layered, one query apiece, and one select per
|
||||
// element past the first of each ladder.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageQuerySize), 6u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 4u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 0u) << "a query produces a value, so no control flow";
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
// An imageAtomic* is the shape the lowering must refuse: its per-element rebuild would run every
|
||||
// other element's read-modify-write. Declining leaves the illegal subscript in place - which is
|
||||
// what the latched warning in LegalizeResourceArrayIndexingForEssl is for - but a half-transform
|
||||
// would corrupt four images instead of losing one stage.
|
||||
TEST(LegalizeResourceArrayIndexPass, DeclinesAUniformIndexedImageAtomic) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageAtomic);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 0u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotentOnImageArrays) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.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>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .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 = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// Test-side reference walker, deliberately independent of the production detection so a
|
||||
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
|
||||
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
|
||||
// exactly what the Qualcomm ES compiler refuses.
|
||||
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
|
||||
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpDecorate:
|
||||
if (wordCount >= 3u) {
|
||||
const auto decoration = static_cast<spv::Decoration>(words[2]);
|
||||
if (decoration == spv::Decoration::Block ||
|
||||
decoration == spv::Decoration::BufferBlock) {
|
||||
blockStructs.insert(words[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
// OpTypeArray <result> <element type> <length>
|
||||
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
|
||||
blockArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
// OpTypePointer <result> <storage class> <pointee>
|
||||
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
|
||||
blockArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
// OpVariable <result type> <result> <storage class>
|
||||
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
|
||||
blockArrayVars.insert(words[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
bool dynamic = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
|
||||
// OpAccessChain <result type> <result> <base> <index 0> ...
|
||||
if (wordCount < 5u) return;
|
||||
if (blockArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
|
||||
// induction variable is a literal after unrolling.
|
||||
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
g_out.data[i] = g_blocks[i].data[0];
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced index - the shape
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
|
||||
// can fold it, so the switch/select lowering is what has to carry it.
|
||||
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_blocks[g_index].data[0] = 7u;
|
||||
g_out.value = g_blocks[g_index].data[1];
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control from the device run: dynamic addressing through an array MEMBER of
|
||||
// ONE block is legal ES and must not be rewritten.
|
||||
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = g_block.data[g_index];
|
||||
}
|
||||
)";
|
||||
|
||||
// A block array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
void main() {
|
||||
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, FoldsALoopIndexedBlockArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
// Either half of the legalization is an acceptable outcome here - what the ES driver
|
||||
// cares about is only that no dynamic subscript survives.
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
|
||||
// One switch for the store, and one select per element past the first for the load.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeStorageBlockArrayIndexPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -475,11 +475,11 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
|
||||
v.explicitFragmentOutIndices = &fragIndex;
|
||||
variants.emplace_back("explicitFragmentOutIndices", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
{ // the merged layout(binding = N) opaque units
|
||||
SpirvTranslationKeyInputs v = base;
|
||||
v.explicitOpaqueUniformBindings = &opaque;
|
||||
variants.emplace_back("explicitOpaqueUniformBindings", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
// NOT the merged layout(binding = N) opaque units, which used to be a variant here: that
|
||||
// map is an OUTPUT of mapIO (TMglGlslIoResolver writes it and never reads it), so it is a
|
||||
// pure function of the stage sources this key already carries in full. It was dropped from
|
||||
// SpirvTranslationKeyInputs with the glslang-capture migration; kKeyLayoutVersion moved to
|
||||
// 4 so no blob written under the old shape can be honoured.
|
||||
{ // ShaderCompileBits (0 on both production parse paths; keyed so a future value
|
||||
// cannot alias a module parsed without it)
|
||||
SpirvTranslationKeyInputs v = base;
|
||||
@@ -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,14 +902,21 @@ 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;
|
||||
v.supportsNoperspectiveInterpolation = true;
|
||||
variants.emplace_back("supportsNoperspectiveInterpolation", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms WidenImageFormatsForEssl - a driver WITH GL_NV_image_formats keeps the declared
|
||||
// rg32f/r8ui/... image formats, one without has them re-declared in a core carrier and
|
||||
// every access to them masked, so the two get materially different ESSL from one module.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.supportsExtendedImageFormats = true;
|
||||
variants.emplace_back("supportsExtendedImageFormats", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms AND parameterizes ClampMultisampleFetchesForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.maxColorTextureSamples = 1;
|
||||
@@ -1001,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);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5212,3 +5212,127 @@ TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGet
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// The image-format widening's transfer half. GL has forty image formats and GLSL ES core has
|
||||
// thirteen, so an image-bindable GL_R8UI texture is stored as a GL_RGBA8UI and an image-bindable
|
||||
// GL_RG32F as a GL_RGBA32F (TextureImpl::GetImageBindableStorageWidening). The driver is then told
|
||||
// the transfer is four components wide, and one- or two-component client data has to be repacked to
|
||||
// match - with the SAME values GL gives the channels a narrow format does not have, so that a
|
||||
// later sample, imageLoad or glGetTexImage cannot tell the carrier from the real thing.
|
||||
TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingChannelValues) {
|
||||
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PrepareChannelWidenedUpload;
|
||||
|
||||
const IntVec3 texelSize(2, 1, 1);
|
||||
|
||||
// GL_RG32F -> GL_RGBA32F. Blue is 0 and alpha 1.0, which is exactly what GL answers for the
|
||||
// two channels an rg32f image does not have.
|
||||
{
|
||||
const Float source[] = {0.25f, -0.5f, 1.5f, -2.5f};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Float*>(
|
||||
PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_FLOAT, widened, false));
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
ASSERT_EQ(widened.size(), 8 * sizeof(Float));
|
||||
const Float expected[] = {0.25f, -0.5f, 0.0f, 1.0f, 1.5f, -2.5f, 0.0f, 1.0f};
|
||||
for (SizeT i = 0; i < 8; ++i) {
|
||||
EXPECT_FLOAT_EQ(result[i], expected[i]) << "component " << i;
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R8UI -> GL_RGBA8UI. Three added channels, and the one in alpha is the INTEGER one: an
|
||||
// integer format's missing alpha reads back as 1, not as the saturated field a normalized
|
||||
// format's does, and GL_UNSIGNED_BYTE serves both classes so the type alone cannot decide.
|
||||
{
|
||||
const Uint8 source[] = {7, 8};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Uint8*>(PrepareChannelWidenedUpload(
|
||||
1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/true));
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
const Uint8 expected[] = {7, 0, 0, 1, 8, 0, 0, 1};
|
||||
ASSERT_EQ(widened.size(), sizeof(expected));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// GL_R8 -> GL_RGBA8, the normalized twin of the case above: same transfer type, saturated one.
|
||||
{
|
||||
const Uint8 source[] = {7, 8};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Uint8*>(PrepareChannelWidenedUpload(
|
||||
1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/false));
|
||||
const Uint8 expected[] = {7, 0, 0, 0xFF, 8, 0, 0, 0xFF};
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// GL_RG8_SNORM -> GL_RGBA8_SNORM keeps GL_BYTE, whose 1.0 is the positive maximum.
|
||||
{
|
||||
const Int8 source[] = {-1, 2, 3, -4};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Int8*>(
|
||||
PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_BYTE, widened, false));
|
||||
const Int8 expected[] = {-1, 2, 0, 0x7F, 3, -4, 0, 0x7F};
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0);
|
||||
}
|
||||
|
||||
// A four-component source is already the carrier's shape: nothing to repack, and the caller's
|
||||
// sub-rect upload fast path depends on the pointer coming back unchanged when that is so.
|
||||
{
|
||||
const Uint8 source[] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
Vector<Uint8> widened;
|
||||
EXPECT_EQ(PrepareChannelWidenedUpload(4, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened,
|
||||
false),
|
||||
static_cast<const void*>(source));
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,9 +287,17 @@ namespace MobileGL {
|
||||
case TexturePixelDataType::UnsignedInt8888Rev:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TexturePixelDataType::UnsignedInt1010102:
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
// GL_UNSIGNED_INT_10_10_10_2 is R in bits 22-31, G 12-21, B 2-11, A 0-1 - an
|
||||
// R10G10B10A2 packing Vulkan has no format for at all. Reported as UNDEFINED
|
||||
// rather than as the A2*10*10*10 neighbours below, which are a different
|
||||
// packing: naming one of those would hand a caller a format whose components
|
||||
// sit in the wrong bits.
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
case TexturePixelDataType::UnsignedInt2101010Rev:
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
// A2**B**10G10R10, for the reason spelled out on
|
||||
// ConvertTextureInternalFormatToVkFormat's RGB10A2: _REV puts R in bits 0-9,
|
||||
// which is A2B10G10R10. A2R10G10B10 silently swaps R and B.
|
||||
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
|
||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||
return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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{};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -33,7 +34,9 @@
|
||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/Lower1DSampledImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
#include "SpirvPasses/WidenImageFormatsPass.h"
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -41,7 +44,7 @@
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeStorageBlockArrayIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
@@ -356,6 +359,107 @@ namespace MobileGL {
|
||||
glslang::SetThreadPoolAllocator(nullptr);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
|
||||
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
|
||||
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
|
||||
// linker resolves such a name by stripping the single trailing "[0]", so it looks
|
||||
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
|
||||
// silently loses its explicit location.
|
||||
//
|
||||
// Emit those pre-flattened keys next to the root, so the result is
|
||||
// order-independent: each carries the location its own element starts at (element
|
||||
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
|
||||
// contain brackets, so a synthesized key never collides with a real uniform name,
|
||||
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
|
||||
void RecordArrayOfArraysElementLocations(const String& name, const std::vector<int>& dimensions,
|
||||
const long long baseLocation,
|
||||
UnorderedMap<String, Int>& locations) {
|
||||
if (dimensions.size() < 2) return;
|
||||
// A pathological declaration must not be able to blow up the map; past the cap
|
||||
// only the root entry stands, which is what every case used to get.
|
||||
constexpr long long kMaxSynthesizedKeys = 4096;
|
||||
const long long innerSpan = dimensions.back();
|
||||
const SizeT outerDimensions = dimensions.size() - 1;
|
||||
long long elementCount = 1;
|
||||
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||
elementCount *= dimensions[d];
|
||||
if (elementCount > kMaxSynthesizedKeys) return;
|
||||
}
|
||||
for (long long element = 0; element < elementCount; ++element) {
|
||||
String key = name;
|
||||
long long remainder = element;
|
||||
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||
long long stride = 1;
|
||||
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
|
||||
key += "[" + std::to_string(remainder / stride) + "]";
|
||||
remainder %= stride;
|
||||
}
|
||||
locations.emplace(key, static_cast<Int>(std::min(baseLocation + element * innerSpan,
|
||||
static_cast<long long>(INT_MAX / 2))));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UnorderedMap<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader) {
|
||||
UnorderedMap<String, Int> locations;
|
||||
const glslang::TIntermediate* intermediate = shader.getIntermediate();
|
||||
if (intermediate == nullptr) return locations;
|
||||
|
||||
// Half one: the uniforms the relaxed remap swallowed, out of the snapshot it
|
||||
// takes on the way past.
|
||||
for (const glslang::TIntermediate::TUniformLocation& record :
|
||||
intermediate->getUniformLocations()) {
|
||||
if (record.location < 0) continue;
|
||||
// Keep the first sighting. Two records for one name mean the parser saw the
|
||||
// declaration twice, and the first is the one the symbol table kept.
|
||||
locations.emplace(record.name, record.location);
|
||||
RecordArrayOfArraysElementLocations(record.name, record.arraySizes, record.location,
|
||||
locations);
|
||||
}
|
||||
|
||||
// Half two: the OPAQUE uniforms, which the remap never touches (the guard in
|
||||
// vkRelaxedRemapUniformVariable admits only types containing something
|
||||
// non-opaque, atomic_uint, or a sampler inside a struct) and which therefore
|
||||
// still carry their qualifier here.
|
||||
//
|
||||
// They belong in the same map even though reflection could also answer for them,
|
||||
// and the distinction is not cosmetic: this map is what marks a location as
|
||||
// SOURCE-EXPLICIT, i.e. API contract under ARB_explicit_uniform_location. A
|
||||
// location that only reaches DoReflection through glslang's own layoutLocation()
|
||||
// is treated as implementation-chosen and quietly moved on a collision, which is
|
||||
// the wrong answer for one the shader declared.
|
||||
//
|
||||
// Read BEFORE any link: mapIO writes its own choices into these same qualifiers
|
||||
// (iomapper.cpp:240), so this is only truthful while the shader is unlinked -
|
||||
// which is exactly where ShaderCompileTask calls it.
|
||||
const glslang::TIntermAggregate* linkerObjects = intermediate->findLinkerObjects();
|
||||
if (linkerObjects == nullptr) return locations;
|
||||
for (TIntermNode* node : linkerObjects->getSequence()) {
|
||||
const glslang::TIntermSymbol* symbol = node ? node->getAsSymbolNode() : nullptr;
|
||||
if (symbol == nullptr) continue;
|
||||
const glslang::TType& type = symbol->getType();
|
||||
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||
if (qualifier.storage != glslang::EvqUniform || !qualifier.hasLocation()) continue;
|
||||
// A BLOCK has no glGetUniformLocation of its own, and its members are
|
||||
// addressed through the block. Only loose uniforms take locations.
|
||||
if (type.getBasicType() == glslang::EbtBlock || type.isBuiltIn()) continue;
|
||||
|
||||
std::vector<int> arraySizes;
|
||||
if (type.isArray() && type.getArraySizes() != nullptr) {
|
||||
const glslang::TArraySizes& sizes = *type.getArraySizes();
|
||||
for (int dim = 0; dim < sizes.getNumDims(); ++dim) {
|
||||
arraySizes.push_back(sizes.getDimSize(dim));
|
||||
}
|
||||
}
|
||||
const String name = symbol->getAccessName().c_str();
|
||||
const Int location = static_cast<Int>(qualifier.layoutLocation);
|
||||
locations.emplace(name, location);
|
||||
RecordArrayOfArraysElementLocations(name, arraySizes, location, locations);
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
|
||||
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
|
||||
for (auto& s : attrib.shaders) {
|
||||
@@ -381,7 +485,8 @@ namespace MobileGL {
|
||||
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
|
||||
attrib.explicitFragmentOutLocations,
|
||||
attrib.explicitFragmentOutIndices,
|
||||
attrib.explicitOpaqueUniformBindings);
|
||||
attrib.explicitOpaqueUniformBindings,
|
||||
attrib.storageBlocksWithoutBinding);
|
||||
break;
|
||||
}
|
||||
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||
@@ -596,6 +701,94 @@ 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;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleReadsLocatedInput"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
// A LOCATION is exactly the property that separates a user-defined varying (or a
|
||||
// per-patch input) from a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||
// gl_PrimitiveID and the tessellation levels carry none, and every one of them is
|
||||
// either forwarded by the pass-through or generated by the tessellator itself.
|
||||
//
|
||||
// Decided on the OpVariable's own Location decoration rather than on any
|
||||
// built-in classification, for the reason DirectVulkan's
|
||||
// ReflectPassthroughTessControlNeed records at length: gl_in is an ARRAY OF
|
||||
// INTERFACE BLOCKS, and a member walk of one reads back as BuiltIn::Position for
|
||||
// every member, so classifying by built-in would accept anything.
|
||||
for (auto& 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;
|
||||
}
|
||||
Bool located = false;
|
||||
context->get_decoration_mgr()->ForEachDecoration(
|
||||
variable.result_id(), static_cast<uint32_t>(spv::Decoration::Location),
|
||||
[&located](const spvtools::opt::Instruction&) { located = true; });
|
||||
if (located) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
@@ -609,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);
|
||||
|
||||
@@ -646,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);
|
||||
@@ -771,6 +991,49 @@ namespace MobileGL {
|
||||
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
|
||||
}
|
||||
|
||||
bool ShaderCompiler::WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool onlyFormatsSpirvCrossRefusesToPrint,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
WidenImageFormatsPass::CreateWidenImageFormatsPass(onlyFormatsSpirvCrossRefusesToPrint));
|
||||
// Two image types that differed only in a format the widening collapses -
|
||||
// `layout(rg32f)` and `layout(rgba32f)` in one module - are one type afterwards,
|
||||
// and duplicate non-aggregate type declarations are invalid SPIR-V. This joins
|
||||
// them, and cascades to the pointer and array types that named them; the pass
|
||||
// itself deliberately does not carry a join of its own.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("WidenImageFormatsForEssl", optimizer, inputBinary, outputBinary,
|
||||
true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
const bool onlyFormatsSpirvCrossRefusesToPrint) {
|
||||
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary,
|
||||
onlyFormatsSpirvCrossRefusesToPrint);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::WidenedCoreEsslImageFormat(glInternalFormat);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::ImageFormatChannelCount(Uint glInternalFormat) {
|
||||
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,
|
||||
@@ -943,16 +1206,16 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(
|
||||
bool ShaderCompiler::LegalizeResourceArrayIndexingForEssl(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Detection gates everything: a module that declares no array of storage
|
||||
// blocks, or indexes one only with constants - every shader but a handful -
|
||||
// pays one BuildModule and is handed back byte for byte, so the folding chain
|
||||
// can never perturb a shader that did not need it.
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
// Detection gates everything: a module that declares no array of storage blocks
|
||||
// and no array of images, or indexes one only with constants - every shader but
|
||||
// a handful - pays one BuildModule and is handed back byte for byte, so the
|
||||
// folding chain can never perturb a shader that did not need it.
|
||||
if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
@@ -968,7 +1231,7 @@ namespace MobileGL {
|
||||
// induction variable as an OpPhi, and glslang emits it as loads and stores of
|
||||
// a Function variable.
|
||||
folder.RegisterPass(CreateLocalMultiStoreElimPass());
|
||||
folder.RegisterPass(LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass());
|
||||
folder.RegisterPass(LegalizeResourceArrayIndexPass::CreateMarkLoopsForUnrollPass());
|
||||
folder.RegisterPass(CreateLoopUnrollPass(true));
|
||||
// Fold the unrolled induction values into the access chains, then clear out
|
||||
// what constant conditions leave behind.
|
||||
@@ -978,14 +1241,14 @@ namespace MobileGL {
|
||||
folder.RegisterPass(CreateBlockMergePass());
|
||||
|
||||
Vector<uint32_t> folded;
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.fold", folder,
|
||||
if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.fold", folder,
|
||||
inputBinary, folded, true, enableSpirvValidation) ||
|
||||
folded.empty()) {
|
||||
// Fail open onto the fallback rather than onto the illegal module.
|
||||
folded = inputBinary;
|
||||
}
|
||||
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
folded)) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
@@ -994,25 +1257,25 @@ namespace MobileGL {
|
||||
// Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it.
|
||||
Optimizer lowerer(SPV_ENV_VULKAN_1_1);
|
||||
lowerer.RegisterPass(
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass());
|
||||
LegalizeResourceArrayIndexPass::CreateLowerToConstantSwitchPass());
|
||||
// The chains the lowering replaced are dead now; remove_outputs must stay
|
||||
// false here for the same reason it does in SanitizeAndOptimizeBinary.
|
||||
lowerer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.lower", lowerer, folded,
|
||||
if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.lower", lowerer, folded,
|
||||
outputBinary, true, enableSpirvValidation) ||
|
||||
outputBinary.empty()) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
if (LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
outputBinary)) {
|
||||
// MGLOG_W, latched, for the same reason the fragment-output one is: this
|
||||
// runs per shader compile and shader packs compile lazily mid-session.
|
||||
MGLOG_W_ONCE("[spirv] LegalizeStorageBlockArrayIndexingForEssl: an array of storage "
|
||||
"blocks is still indexed dynamically; a strict ES driver will reject "
|
||||
"this shader");
|
||||
MGLOG_W_ONCE("[spirv] LegalizeResourceArrayIndexingForEssl: an array of storage "
|
||||
"blocks or of images is still indexed dynamically; a strict ES "
|
||||
"driver will reject this shader");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1094,6 +1357,39 @@ namespace MobileGL {
|
||||
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::Lower1DSampledImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// The overwhelmingly common answer, and the reason the probe exists: no 1D sampler
|
||||
// is reached by an offset or a gradient, so the module is handed back byte for
|
||||
// byte without an Optimizer ever being built. Every ESSL shader in the process
|
||||
// passes through here, so the cost of the case with nothing to do is the cost of
|
||||
// this pass. Note the probe is deliberately NARROWER than "declares a 1D sampler":
|
||||
// SPIRV-Cross emits the plain sample and fetch forms correctly, and taking those
|
||||
// over would be a regression looking for somewhere to happen.
|
||||
if (!Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(Lower1DSampledImagesPass::CreateLower1DSampledImagesPass());
|
||||
// Mandatory, not tidying - the same collision Lower1DArrayImagesForEssl documents
|
||||
// one screen up. Rewriting a 1D sampled image type to the 2D one makes it
|
||||
// structurally IDENTICAL to any real 2D sampled image of the same sampled type the
|
||||
// module already declared, and SPIR-V forbids duplicate non-aggregate type
|
||||
// declarations. That is not exotic here: it is the exact shape of the headline
|
||||
// case, whose compute shader declares sampler1D and sampler2D side by side. The
|
||||
// same applies to the OpTypeSampledImage and OpTypePointer instructions above
|
||||
// them, and to the Sampled1D capability the rewrite turns into a second Shader.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("Lower1DSampledImagesForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -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).
|
||||
@@ -71,6 +83,11 @@ namespace MobileGL {
|
||||
// GL_OES_viewport_array AND integer multisample squeezed to 1) the separate
|
||||
// probes made compile-heavy workloads measurably slower - ReservedNames-class
|
||||
// CTS cases paid ~10%. Callers with more than one armed gate use this instead.
|
||||
// The image-format widening deliberately does NOT ride this probe, even though it
|
||||
// is a module question of exactly the same shape. It is armed on every driver, so
|
||||
// a gate answered from the module would put a BuildModule on every stage of every
|
||||
// program - and the frontend's uniform reflection can answer it for free
|
||||
// (PrgramImpl::ImageFormatBakeInputs::declaresWidenableImageFormat).
|
||||
struct SpirvGateFeatures {
|
||||
Bool WritesViewportIndexOutput = false;
|
||||
Bool DeclaresMultisampledImage = false;
|
||||
@@ -156,18 +173,22 @@ namespace MobileGL {
|
||||
static bool LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Makes every index into an ARRAY OF SHADER STORAGE BLOCKS a constant integral
|
||||
// expression. GL 4.3 allows any dynamically-uniform index there; the Qualcomm
|
||||
// ES compiler enforces the ES 3.1 constant-expression rule and refuses the whole
|
||||
// stage ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted"), which loses the program while the frontend still reports
|
||||
// GL_LINK_STATUS = TRUE. Same two halves as the fragment-output legalization:
|
||||
// fold the loop-derived indices, then lower whatever is genuinely dynamic to a
|
||||
// switch over the array's range. DirectGLES transpile path only - Vulkan has no
|
||||
// such restriction and must keep seeing one descriptor array. Copies the input
|
||||
// through untouched when no block array is indexed dynamically, which is every
|
||||
// shader but a handful. See LegalizeStorageBlockArrayIndexPass.
|
||||
static bool LegalizeStorageBlockArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
// Makes every index into an ARRAY OF SHADER STORAGE BLOCKS or an ARRAY OF IMAGE
|
||||
// UNIFORMS a constant integral expression. Desktop GL allows any
|
||||
// dynamically-uniform index in either; ES keeps the ES 3.1
|
||||
// constant-expression rule for both and the drivers refuse the whole stage
|
||||
// ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted" on Qualcomm, "image arrays indexed with non-constant expressions
|
||||
// are forbidden in GLSL ES" on Mesa), which loses the program while the
|
||||
// frontend still reports GL_LINK_STATUS = TRUE. Same two halves as the
|
||||
// fragment-output legalization: fold the loop-derived indices, then lower
|
||||
// whatever is genuinely dynamic to a switch over the array's range. SAMPLER
|
||||
// arrays are out of scope - ESSL 3.20 4.1.7 permits them a dynamically-uniform
|
||||
// index. DirectGLES transpile path only - Vulkan has no such restriction and
|
||||
// must keep seeing one descriptor array. Copies the input through untouched
|
||||
// when no such array is indexed dynamically, which is every shader but a
|
||||
// handful. See LegalizeResourceArrayIndexPass.
|
||||
static bool LegalizeResourceArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Collapses each synthesized gl_AtomicCounterBlock_<N> into one uint array at
|
||||
@@ -204,6 +225,19 @@ namespace MobileGL {
|
||||
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// The SAMPLED-image counterpart. SPIRV-Cross widens a 1D sampler's COORDINATE for
|
||||
// ES and prints the OFFSET and GRADIENT operands with their original 1D arity, so
|
||||
// textureOffset / textureLodOffset / texelFetchOffset / textureGrad on a
|
||||
// sampler1D(Array) come out with no ESSL overload ("no matching overloaded
|
||||
// function found") and the stage is lost. Rewrites the type to 2D and widens
|
||||
// coordinate, offset and gradients together. DirectGLES transpile path only -
|
||||
// Vulkan has 1D images natively. Copies the input through untouched unless the
|
||||
// module actually carries such an operand on a 1D sampler, so a shader that only
|
||||
// samples or fetches keeps SPIRV-Cross's own correct emission. See
|
||||
// Lower1DSampledImagesPass for what it declines and why.
|
||||
static bool Lower1DSampledImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Gives each format-less storage image the format bound to its image unit, so
|
||||
// the emitted ESSL can carry the format layout qualifier GLSL ES requires of
|
||||
// every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
|
||||
@@ -231,6 +265,50 @@ namespace MobileGL {
|
||||
// must not ask BakeImageFormatsForEssl for those, and completes them in the
|
||||
// emitted text instead.
|
||||
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
|
||||
// Re-declares every storage image whose DECLARED format GLSL ES cannot spell in
|
||||
// the core format that carries it exactly, and masks each access back to the
|
||||
// channels the original format has. The 26 formats outside the ES core set have no
|
||||
// legal ESSL spelling on any tested driver (none exposes GL_NV_image_formats), and
|
||||
// a format-less declaration is rejected too, so the stage is otherwise lost
|
||||
// whatever this backend emits. DirectGLES transpile path only - Vulkan takes the
|
||||
// declared format natively. See WidenImageFormatsPass for the table, for the nine
|
||||
// formats it deliberately does NOT widen, and for why the texture storage and the
|
||||
// glBindImageTexture argument have to move with it.
|
||||
// `onlyFormatsSpirvCrossRefusesToPrint` narrows it to the formats that have no
|
||||
// ESSL route even WITH GL_NV_image_formats, because SPIRV-Cross throws for them
|
||||
// rather than printing a token - which is the whole set a driver that advertises
|
||||
// the extension still needs. See WidenImageFormatsPass.
|
||||
static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares a storage image WidenImageFormatsForEssl would
|
||||
// widen, under the same mode the run would use. Costs its own module parse, so
|
||||
// the transpile path does NOT gate on this - it answers the question from the
|
||||
// frontend's uniform reflection instead, for the reason on SpirvGateFeatures.
|
||||
// Here for tests and for callers that already hold nothing but the binary.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
|
||||
// when it needs no widening or cannot be widened exactly. The single source of
|
||||
// truth for all three layers of the emulation: this one answers the shader, and
|
||||
// DirectGLES asks it again for the texture storage and the image bind, so the two
|
||||
// sides cannot drift.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
// 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);
|
||||
@@ -376,7 +454,50 @@ namespace MobileGL {
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
// 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.
|
||||
//
|
||||
// Asked of a TESSELLATION EVALUATION stage that has no control stage, to decide
|
||||
// whether the pass-through control stage GL 4.6 core 11.2.2 describes can stand
|
||||
// in for the missing one. That stage forwards gl_Position and nothing else, so a
|
||||
// located input - which the vertex stage feeds today and which would stop
|
||||
// arriving once a control stage sat in between - means the program has to be
|
||||
// declined rather than fed an undefined varying. Same rule, same reasoning, as
|
||||
// DirectVulkan's ReflectPassthroughTessControlNeed, which asks SPIRV-Reflect the
|
||||
// identical question for the identical decision.
|
||||
static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
|
||||
};
|
||||
|
||||
// The explicit layout(location = N) qualifiers this shader's DEFAULT-BLOCK uniforms
|
||||
// declared, keyed the way glslang's own reflection will later spell them.
|
||||
//
|
||||
// They cannot be read back off the parsed module, and that is not an oversight of
|
||||
// this function: MobileGL parses every shader as a Vulkan client under relaxed
|
||||
// rules, which sweeps plain uniforms into MGL_GLOBAL_UBO - where a location
|
||||
// qualifier has no meaning - and DROPS the qualifier on the way past
|
||||
// (ParseHelper.cpp vkRelaxedRemapUniformVariable). What this reads is the snapshot
|
||||
// glslang takes at that exact site, handed over through TIntermediate; the GL
|
||||
// location assigner in ProgramLinkTask::DoReflection is the only party left that
|
||||
// can honour the number.
|
||||
//
|
||||
// Keyed by declared name (no "[0]" suffix), plus one synthesized key per outer
|
||||
// index of an array-of-arrays - see the note in the implementation for why
|
||||
// reflection needs those spelled out. A uniform declared in several stages must
|
||||
// agree, which the caller enforces across stages.
|
||||
UnorderedMap<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -930,6 +930,405 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// Index one past the token that closes the group tokens[open] opens, or the token count when
|
||||
// the group is never closed. Nesting of the SAME bracket pair is counted, everything else is
|
||||
// skipped, so a '(' inside a '[' run cannot confuse a bracket walk and vice versa.
|
||||
SizeT FindGroupEnd(const Vector<CodeToken>& tokens, SizeT open, char opener, char closer) {
|
||||
int depth = 0;
|
||||
for (SizeT i = open; i < tokens.size(); ++i) {
|
||||
if (tokens[i].text.size() != 1) continue;
|
||||
if (tokens[i].text[0] == opener) {
|
||||
++depth;
|
||||
} else if (tokens[i].text[0] == closer && --depth == 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return tokens.size();
|
||||
}
|
||||
|
||||
// Token text joined by single spaces. Token text is comment-free by construction (the
|
||||
// tokenizer reads a masked source), so this is how a rewritten declaration is rebuilt without
|
||||
// dragging a comment - or a newline - into a line the rewrite promises to keep single-line.
|
||||
String JoinTokenText(const Vector<CodeToken>& tokens, SizeT begin, SizeT end) {
|
||||
String text;
|
||||
for (SizeT i = begin; i < end; ++i) {
|
||||
if (!text.empty()) text += ' ';
|
||||
text += tokens[i].text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Erase a span for the compiler while keeping every later offset - and every LINE NUMBER -
|
||||
// exactly where it was, so edits collected against one token scan all stay valid and glslang's
|
||||
// diagnostics still point at the line the application wrote.
|
||||
void BlankSpan(MobileGL::String& source, SizeT begin, SizeT end) {
|
||||
for (SizeT i = begin; i < end && i < source.size(); ++i) {
|
||||
if (source[i] != '\n' && source[i] != '\r') {
|
||||
source[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsParameterQualifierKeyword(const String& text) {
|
||||
static constexpr std::string_view kQualifiers[] = {
|
||||
"const", "in", "out", "inout", "highp", "mediump", "lowp",
|
||||
"precise", "coherent", "volatile", "restrict", "readonly", "writeonly",
|
||||
};
|
||||
return std::find(std::begin(kQualifiers), std::end(kQualifiers), std::string_view(text)) !=
|
||||
std::end(kQualifiers);
|
||||
}
|
||||
|
||||
// The #if/#ifdef/#ifndef nesting in effect at each offset, as (offset, depth) marks. Every
|
||||
// mark takes effect at the END of the directive line that changed the depth.
|
||||
Vector<std::pair<SizeT, int>> BuildConditionalDepthMarks(const MobileGL::String& source,
|
||||
const Vector<std::pair<SizeT, SizeT>>& ranges) {
|
||||
Vector<std::pair<SizeT, int>> marks;
|
||||
marks.emplace_back(static_cast<SizeT>(0), 0);
|
||||
int depth = 0;
|
||||
for (const std::pair<SizeT, SizeT>& range : ranges) {
|
||||
SizeT pos = range.first;
|
||||
SkipDirectiveWhitespace(source, pos, range.second);
|
||||
if (pos >= range.second || source[pos] != '#') continue;
|
||||
++pos;
|
||||
SkipDirectiveWhitespace(source, pos, range.second);
|
||||
const String name = ReadDirectiveIdentifier(source, pos, range.second);
|
||||
if (name == "if" || name == "ifdef" || name == "ifndef") {
|
||||
++depth;
|
||||
} else if (name == "endif") {
|
||||
if (depth > 0) --depth;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
marks.emplace_back(range.second, depth);
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
|
||||
int ConditionalDepthAt(const Vector<std::pair<SizeT, int>>& marks, SizeT offset) {
|
||||
const auto next = std::upper_bound(marks.begin(), marks.end(), offset,
|
||||
[](SizeT value, const std::pair<SizeT, int>& mark) {
|
||||
return value < mark.first;
|
||||
});
|
||||
return next == marks.begin() ? 0 : std::prev(next)->second;
|
||||
}
|
||||
|
||||
struct SubroutineParameters {
|
||||
Vector<String> declarations; // "in highp float mgl_sr_arg0", ready for a parameter list
|
||||
Vector<String> arguments; // "mgl_sr_arg0", ready for a forwarding call
|
||||
};
|
||||
|
||||
// One parameter of a subroutine TYPE declaration, whose name (if it even has one) this rewrite
|
||||
// replaces with a generated one. The shape read is
|
||||
// <qualifier>* <typeName> <arrayOfType>? <name>? <arrayOfName>?
|
||||
// which is the whole of the GLSL parameter grammar; anything that does not fit is refused so
|
||||
// the caller can abandon the rewrite rather than emit a guess.
|
||||
bool AppendSubroutineParameter(const Vector<CodeToken>& tokens, SizeT begin, SizeT end, SizeT index,
|
||||
SubroutineParameters& parameters) {
|
||||
if (begin >= end) return false;
|
||||
|
||||
SizeT cursor = begin;
|
||||
while (cursor < end && IsParameterQualifierKeyword(tokens[cursor].text)) {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= end || !IsIdentifierToken(tokens[cursor])) return false;
|
||||
++cursor;
|
||||
while (cursor < end && tokens[cursor].text == "[") { // "float[4] a"
|
||||
const SizeT close = FindGroupEnd(tokens, cursor, '[', ']');
|
||||
if (close > end) return false;
|
||||
cursor = close;
|
||||
}
|
||||
const String typeText = JoinTokenText(tokens, begin, cursor);
|
||||
|
||||
String arraySuffix;
|
||||
if (cursor < end) { // the declared parameter name, which the generated one replaces
|
||||
if (!IsIdentifierToken(tokens[cursor])) return false;
|
||||
const SizeT afterName = cursor + 1;
|
||||
cursor = afterName;
|
||||
while (cursor < end && tokens[cursor].text == "[") { // "float a[4]"
|
||||
const SizeT close = FindGroupEnd(tokens, cursor, '[', ']');
|
||||
if (close > end) return false;
|
||||
cursor = close;
|
||||
}
|
||||
if (cursor != end) return false;
|
||||
arraySuffix = JoinTokenText(tokens, afterName, end);
|
||||
}
|
||||
|
||||
const String name = "mgl_sr_arg" + std::to_string(index);
|
||||
parameters.declarations.push_back(typeText + " " + name + (arraySuffix.empty() ? "" : " " + arraySuffix));
|
||||
parameters.arguments.push_back(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The parameter list between (but not including) the parentheses of a subroutine type
|
||||
// declaration. "()" and "(void)" are both the empty list.
|
||||
bool ParseSubroutineParameters(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
|
||||
SubroutineParameters& parameters) {
|
||||
if (begin >= end) return true;
|
||||
if (end == begin + 1 && tokens[begin].text == "void") return true;
|
||||
|
||||
SizeT parameterBegin = begin;
|
||||
SizeT index = 0;
|
||||
for (SizeT i = begin; i <= end; ++i) {
|
||||
if (i < end) {
|
||||
if (tokens[i].text == "[") { // a comma inside a subscript is not a separator
|
||||
const SizeT close = FindGroupEnd(tokens, i, '[', ']');
|
||||
if (close > end) return false;
|
||||
i = close - 1;
|
||||
continue;
|
||||
}
|
||||
if (tokens[i].text != ",") continue;
|
||||
}
|
||||
if (!AppendSubroutineParameter(tokens, parameterBegin, i, index, parameters)) return false;
|
||||
++index;
|
||||
parameterBegin = i + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GLSL subroutines (ARB_shader_subroutine, core since 4.00).
|
||||
//
|
||||
// glslang refuses the keyword outright once the target is SPIR-V - "'subroutine' : not allowed
|
||||
// when generating SPIR-V", "feature not yet implemented" - so a shader that declares one never
|
||||
// produces a module at all and the whole program is lost at COMPILE time. That is the entire
|
||||
// failure of KHR-GL43.shader_image_size.advanced-nonMS-*: its subroutine-free twin
|
||||
// basic-nonMS-* drives the identical image battery through the identical imageSize() calls on
|
||||
// the identical targets and passes on every stage.
|
||||
//
|
||||
// The rewrite is confined to the case where it is provably a no-op on semantics: a subroutine
|
||||
// uniform whose type has EXACTLY ONE compatible subroutine. GL 4.3 core 7.9 leaves the value of
|
||||
// a subroutine uniform implementation-dependent until glUniformSubroutinesuiv sets it, so with
|
||||
// a single compatible subroutine every legal value of that uniform selects the same function
|
||||
// and a direct call is indistinguishable from a dispatch under any GL state. A type with two or
|
||||
// more compatible subroutines genuinely needs the dynamic selection MobileGL does not implement
|
||||
// (glUniformSubroutinesuiv is still a stub, and nothing reflects the subroutine interfaces), so
|
||||
// it is left to fail at compile time exactly as it does today rather than silently pinned to
|
||||
// one of the alternatives.
|
||||
//
|
||||
// subroutine void FuncType(int coord); -> (blanked)
|
||||
// subroutine uniform FuncType g_func; -> void g_func(int mgl_sr_arg0);
|
||||
// subroutine(FuncType) void Func0(int c) { } -> void Func0(int c) { }
|
||||
// ...plus, appended at end of source,
|
||||
// void g_func(int mgl_sr_arg0) {
|
||||
// Func0(mgl_sr_arg0);
|
||||
// }
|
||||
//
|
||||
// Naming the forwarding function after the subroutine UNIFORM is what leaves every CALL site
|
||||
// untouched - "g_func(coord)" already reads as a call - and that name is free precisely because
|
||||
// the declaration that held it is gone. The forwarding body has to be appended rather than
|
||||
// written in place because the compatible subroutine is routinely defined AFTER the function
|
||||
// that calls through the uniform (the CTS shaders define theirs below main()); at end of source
|
||||
// every definition it names is already in scope, and a prototype at the old declaration site
|
||||
// keeps the call sites legal.
|
||||
//
|
||||
// All-or-nothing, in the discipline of the scanners below it: an array subroutine uniform, a
|
||||
// subroutine token inside a #if arm or a macro body, an unbalanced file, a type that is never
|
||||
// declared - anything outside the grammar abandons the whole pass with the source untouched,
|
||||
// which is exactly today's behaviour.
|
||||
void LowerShaderSubroutines(MobileGL::String& source) {
|
||||
if (source.find("subroutine") == MobileGL::String::npos) return;
|
||||
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
if (count < 4 || !HasBalancedBraces(tokens)) return;
|
||||
|
||||
const Vector<std::pair<SizeT, SizeT>> directiveRanges = FindDirectiveLineRanges(source);
|
||||
const Vector<std::pair<SizeT, int>> conditionalDepth =
|
||||
BuildConditionalDepthMarks(source, directiveRanges);
|
||||
|
||||
struct SubroutineType {
|
||||
String returnText; // empty until the type declaration itself is seen
|
||||
SubroutineParameters parameters;
|
||||
Vector<String> implementations; // compatible subroutines, in declaration order
|
||||
};
|
||||
struct UniformSite {
|
||||
SizeT begin = 0; // first byte of the declaration, layout(...) qualifier included
|
||||
SizeT end = 0; // one past its ';'
|
||||
String typeName;
|
||||
Vector<String> variables;
|
||||
};
|
||||
struct BlankEdit {
|
||||
SizeT begin;
|
||||
SizeT end;
|
||||
};
|
||||
|
||||
MobileGL::UnorderedMap<String, SubroutineType> types;
|
||||
Vector<UniformSite> uniformSites;
|
||||
Vector<BlankEdit> blanks;
|
||||
|
||||
SizeT braceDepth = 0;
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
const CodeToken& token = tokens[i];
|
||||
if (token.text.size() == 1) {
|
||||
if (token.text[0] == '{') {
|
||||
++braceDepth;
|
||||
continue;
|
||||
}
|
||||
if (token.text[0] == '}') {
|
||||
if (braceDepth > 0) --braceDepth;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (token.text != "subroutine") continue;
|
||||
|
||||
// Nothing here may reason about a subroutine that is not unconditionally at file
|
||||
// scope: the forwarding bodies this appends are unconditional, so a declaration that
|
||||
// only exists in one #if arm (or inside a macro body) would have them naming a
|
||||
// function that is not there.
|
||||
if (braceDepth != 0 || IsInDirectiveLine(directiveRanges, token.begin) ||
|
||||
ConditionalDepthAt(conditionalDepth, token.begin) != 0) {
|
||||
return;
|
||||
}
|
||||
if (i + 1 >= count) return;
|
||||
|
||||
// (a) `[layout(...)] subroutine uniform <TypeName> <var>[, <var>]... ;`
|
||||
if (tokens[i + 1].text == "uniform") {
|
||||
UniformSite site;
|
||||
site.begin = token.begin;
|
||||
if (i >= 2 && tokens[i - 1].text == ")") {
|
||||
SizeT open = i - 1;
|
||||
int depth = 1;
|
||||
while (depth > 0) {
|
||||
if (open == 0) return;
|
||||
--open;
|
||||
if (tokens[open].text == ")") {
|
||||
++depth;
|
||||
} else if (tokens[open].text == "(") {
|
||||
--depth;
|
||||
}
|
||||
}
|
||||
if (open == 0 || tokens[open - 1].text != "layout") return;
|
||||
site.begin = tokens[open - 1].begin;
|
||||
}
|
||||
|
||||
SizeT cursor = i + 2;
|
||||
if (cursor >= count || !IsIdentifierToken(tokens[cursor])) return;
|
||||
site.typeName = tokens[cursor].text;
|
||||
++cursor;
|
||||
while (true) {
|
||||
if (cursor >= count || !IsIdentifierToken(tokens[cursor])) return;
|
||||
site.variables.push_back(tokens[cursor].text);
|
||||
++cursor;
|
||||
if (cursor >= count) return;
|
||||
if (tokens[cursor].text == ",") {
|
||||
++cursor;
|
||||
continue;
|
||||
}
|
||||
// An ARRAY of subroutine uniforms indexes the dispatch itself
|
||||
// ("g_func[i](x)"), which is the dynamic selection this rewrite refuses.
|
||||
if (tokens[cursor].text != ";") return;
|
||||
break;
|
||||
}
|
||||
site.end = tokens[cursor].end;
|
||||
uniformSites.push_back(std::move(site));
|
||||
i = cursor;
|
||||
continue;
|
||||
}
|
||||
|
||||
// (b) `subroutine(<TypeName>, ...) <ret> <name>(<params>) { ... }` - a definition,
|
||||
// which only has to shed the qualifier to become an ordinary function.
|
||||
if (tokens[i + 1].text == "(") {
|
||||
const SizeT listEnd = FindGroupEnd(tokens, i + 1, '(', ')');
|
||||
if (listEnd >= count) return;
|
||||
Vector<String> listed;
|
||||
for (SizeT t = i + 2; t + 1 < listEnd; ++t) {
|
||||
if (tokens[t].text == ",") continue;
|
||||
if (!IsIdentifierToken(tokens[t])) return;
|
||||
listed.push_back(tokens[t].text);
|
||||
}
|
||||
if (listed.empty()) return;
|
||||
|
||||
SizeT paren = listEnd;
|
||||
while (paren < count && tokens[paren].text != "(") {
|
||||
const String& text = tokens[paren].text;
|
||||
if (text == "{" || text == "}" || text == ";" || text == ",") return;
|
||||
++paren;
|
||||
}
|
||||
if (paren >= count || paren == listEnd || !IsIdentifierToken(tokens[paren - 1])) return;
|
||||
|
||||
for (const String& typeName : listed) {
|
||||
types[typeName].implementations.push_back(tokens[paren - 1].text);
|
||||
}
|
||||
blanks.push_back({token.begin, tokens[listEnd - 1].end});
|
||||
i = listEnd - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// (c) `subroutine <ret> <TypeName>(<params>);` - the type declaration.
|
||||
SizeT paren = i + 1;
|
||||
while (paren < count && tokens[paren].text != "(") {
|
||||
const String& text = tokens[paren].text;
|
||||
if (text == "{" || text == "}" || text == ";" || text == ",") return;
|
||||
++paren;
|
||||
}
|
||||
if (paren >= count || paren == i + 1 || !IsIdentifierToken(tokens[paren - 1])) return;
|
||||
const SizeT listEnd = FindGroupEnd(tokens, paren, '(', ')');
|
||||
if (listEnd >= count || tokens[listEnd].text != ";") return;
|
||||
|
||||
SubroutineType& type = types[tokens[paren - 1].text];
|
||||
if (!type.returnText.empty()) return; // declared twice; out of scope
|
||||
type.returnText = JoinTokenText(tokens, i + 1, paren - 1);
|
||||
if (type.returnText.empty()) return;
|
||||
if (!ParseSubroutineParameters(tokens, paren + 1, listEnd - 1, type.parameters)) return;
|
||||
blanks.push_back({token.begin, tokens[listEnd].end});
|
||||
i = listEnd;
|
||||
}
|
||||
|
||||
if (blanks.empty() && uniformSites.empty()) return;
|
||||
|
||||
for (const UniformSite& site : uniformSites) {
|
||||
const auto known = types.find(site.typeName);
|
||||
if (known == types.end() || known->second.returnText.empty()) return;
|
||||
if (known->second.implementations.size() != 1) return;
|
||||
}
|
||||
|
||||
String appended;
|
||||
Vector<std::pair<SizeT, String>> prototypes; // (offset, text), applied back to front
|
||||
for (const UniformSite& site : uniformSites) {
|
||||
const SubroutineType& type = types.at(site.typeName);
|
||||
String parameterList;
|
||||
for (const String& declaration : type.parameters.declarations) {
|
||||
if (!parameterList.empty()) parameterList += ", ";
|
||||
parameterList += declaration;
|
||||
}
|
||||
String arguments;
|
||||
for (const String& argument : type.parameters.arguments) {
|
||||
if (!arguments.empty()) arguments += ", ";
|
||||
arguments += argument;
|
||||
}
|
||||
|
||||
String text;
|
||||
for (const String& variable : site.variables) {
|
||||
const String signature = type.returnText + " " + variable + "(" + parameterList + ")";
|
||||
text += signature + "; ";
|
||||
appended += signature + " {\n " + (type.returnText == "void" ? "" : "return ") +
|
||||
type.implementations.front() + "(" + arguments + ");\n}\n";
|
||||
}
|
||||
prototypes.emplace_back(site.begin, std::move(text));
|
||||
}
|
||||
|
||||
// Blanking first keeps every collected offset valid (it preserves length AND newlines), so
|
||||
// only the prototype insertions - which are single-line, and so cost no line numbers - have
|
||||
// to run back to front.
|
||||
for (const BlankEdit& blank : blanks) {
|
||||
BlankSpan(source, blank.begin, blank.end);
|
||||
}
|
||||
for (const UniformSite& site : uniformSites) {
|
||||
BlankSpan(source, site.begin, site.end);
|
||||
}
|
||||
std::sort(prototypes.begin(), prototypes.end(),
|
||||
[](const std::pair<SizeT, String>& a, const std::pair<SizeT, String>& b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
for (auto it = prototypes.rbegin(); it != prototypes.rend(); ++it) {
|
||||
source.insert(it->first, it->second);
|
||||
}
|
||||
|
||||
if (!appended.empty()) {
|
||||
if (!source.empty() && source.back() != '\n') source += '\n';
|
||||
source += appended;
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
|
||||
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
|
||||
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
|
||||
@@ -1080,6 +1479,10 @@ namespace MobileGL {
|
||||
// declaration identical.
|
||||
SizeNonFinalUnsizedBufferBlockMembers(source);
|
||||
|
||||
// Before the builtin-shadowing rename, so the forwarding functions this synthesizes
|
||||
// are just as visible to it as the ones the application wrote.
|
||||
LowerShaderSubroutines(source);
|
||||
|
||||
RenameBuiltinShadowingFunctions(source);
|
||||
|
||||
ModernizeLegacyGLSL(stage, source, afterVersion);
|
||||
@@ -1250,266 +1653,8 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
|
||||
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
|
||||
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
|
||||
// linker resolves such a name by stripping the single trailing "[0]", so it looks
|
||||
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
|
||||
// silently loses its explicit location.
|
||||
//
|
||||
// Emit those pre-flattened keys here, next to the root, so the result is
|
||||
// order-independent: each carries the location its own element starts at (element
|
||||
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
|
||||
// contain brackets, so a synthesized key never collides with a real uniform name,
|
||||
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
|
||||
void RecordArrayOfArraysElementLocations(const String& name, const Vector<long long>& dimensions,
|
||||
long long baseLocation,
|
||||
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
|
||||
if (dimensions.size() < 2) return;
|
||||
// A pathological declaration must not be able to blow up the map; past the cap
|
||||
// only the root entry stands, which is what every case used to get.
|
||||
constexpr long long kMaxSynthesizedKeys = 4096;
|
||||
const long long innerSpan = dimensions.back();
|
||||
const SizeT outerDimensions = dimensions.size() - 1;
|
||||
long long elementCount = 1;
|
||||
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||
elementCount *= dimensions[d];
|
||||
if (elementCount > kMaxSynthesizedKeys) return;
|
||||
}
|
||||
for (long long element = 0; element < elementCount; ++element) {
|
||||
String key = name;
|
||||
long long remainder = element;
|
||||
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||
long long stride = 1;
|
||||
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
|
||||
key += "[" + std::to_string(remainder / stride) + "]";
|
||||
remainder %= stride;
|
||||
}
|
||||
locations.emplace(key, static_cast<MobileGL::Int>(
|
||||
std::min(baseLocation + element * innerSpan,
|
||||
static_cast<long long>(INT_MAX / 2))));
|
||||
}
|
||||
}
|
||||
|
||||
// Parses one brace-free depth-0 statement [begin, end) and records its
|
||||
// declarators when it is a uniform declaration carrying an integral
|
||||
// layout(location = N). Multi-declarator statements assign consecutive
|
||||
// locations, each declarator advancing by its array element count
|
||||
// (ARB_explicit_uniform_location rules). Anything the narrow grammar does
|
||||
// not recognize is skipped, never guessed at.
|
||||
void RecordUniformDeclarationLocations(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
|
||||
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
|
||||
using MobileGL::Int;
|
||||
long long location = -1;
|
||||
long long literal = 0;
|
||||
bool sawUniform = false;
|
||||
SizeT declaratorBegin = end;
|
||||
|
||||
for (SizeT k = begin; k < end;) {
|
||||
const String& text = tokens[k].text;
|
||||
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
|
||||
SizeT j = k + 2;
|
||||
Int parenDepth = 1;
|
||||
while (j < end && parenDepth > 0) {
|
||||
const String& layoutToken = tokens[j].text;
|
||||
if (layoutToken == "(") {
|
||||
++parenDepth;
|
||||
} else if (layoutToken == ")") {
|
||||
--parenDepth;
|
||||
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
|
||||
tokens[j + 1].text == "=" &&
|
||||
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||
location = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||
j += 2;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
k = j;
|
||||
continue;
|
||||
}
|
||||
if (text == "uniform") {
|
||||
sawUniform = true;
|
||||
++k;
|
||||
continue;
|
||||
}
|
||||
if (sawUniform && location >= 0 && IsIdentifierToken(tokens[k]) &&
|
||||
!IsNonLayoutQualifierKeyword(text)) {
|
||||
declaratorBegin = k + 1; // 'text' is the type; declarators follow
|
||||
break;
|
||||
}
|
||||
++k;
|
||||
}
|
||||
|
||||
if (!sawUniform || location < 0 || declaratorBegin >= end) return;
|
||||
|
||||
long long nextLocation = location;
|
||||
for (SizeT k = declaratorBegin; k < end;) {
|
||||
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
|
||||
const String& name = tokens[k].text;
|
||||
++k;
|
||||
long long span = 1;
|
||||
Vector<long long> dimensions;
|
||||
while (k < end && tokens[k].text == "[") {
|
||||
++k;
|
||||
long long dimension = 1;
|
||||
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
|
||||
dimension = literal;
|
||||
++k;
|
||||
}
|
||||
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
||||
++k;
|
||||
dimensions.push_back(
|
||||
std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2))));
|
||||
span *= dimensions.back();
|
||||
}
|
||||
// Keep the first sighting: a duplicate can only come from alternative
|
||||
// preprocessor branches declaring the same name.
|
||||
locations.emplace(name, static_cast<Int>(std::min(
|
||||
nextLocation, static_cast<long long>(INT_MAX / 2))));
|
||||
RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations);
|
||||
nextLocation += span;
|
||||
if (k >= end) break;
|
||||
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
|
||||
Int nestingDepth = 0;
|
||||
++k;
|
||||
while (k < end) {
|
||||
const String& initializerToken = tokens[k].text;
|
||||
if (initializerToken == "(" || initializerToken == "[") {
|
||||
++nestingDepth;
|
||||
} else if (initializerToken == ")" || initializerToken == "]") {
|
||||
--nestingDepth;
|
||||
} else if (initializerToken == "," && nestingDepth == 0) {
|
||||
break;
|
||||
}
|
||||
++k;
|
||||
}
|
||||
}
|
||||
if (k >= end) break;
|
||||
if (tokens[k].text != ",") return;
|
||||
++k;
|
||||
}
|
||||
}
|
||||
// Parses one brace-free depth-0 statement [begin, end) and records its
|
||||
// declarators when it is a sampler/image uniform declaration carrying an
|
||||
// integral layout(binding = N). Such a binding is a GL texture/image unit,
|
||||
// which the Vulkan-client relaxed parse strips before mapIO can observe it
|
||||
// (it is not a valid descriptor binding there), so it is extracted lexically
|
||||
// and restored as the uniform's initial unit. Every declarator in the
|
||||
// statement shares the qualifier's binding, matching what the GL-client
|
||||
// mapIO used to capture from the shared type qualifier. Anything the narrow
|
||||
// grammar does not recognize is skipped, never guessed at.
|
||||
void RecordOpaqueDeclarationBindings(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
|
||||
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
|
||||
using MobileGL::Int;
|
||||
long long binding = -1;
|
||||
long long literal = 0;
|
||||
bool sawUniform = false;
|
||||
SizeT declaratorBegin = end;
|
||||
|
||||
for (SizeT k = begin; k < end;) {
|
||||
const String& text = tokens[k].text;
|
||||
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
|
||||
SizeT j = k + 2;
|
||||
Int parenDepth = 1;
|
||||
while (j < end && parenDepth > 0) {
|
||||
const String& layoutToken = tokens[j].text;
|
||||
if (layoutToken == "(") {
|
||||
++parenDepth;
|
||||
} else if (layoutToken == ")") {
|
||||
--parenDepth;
|
||||
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
|
||||
tokens[j + 1].text == "=" &&
|
||||
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||
j += 2;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
k = j;
|
||||
continue;
|
||||
}
|
||||
if (text == "uniform") {
|
||||
sawUniform = true;
|
||||
++k;
|
||||
continue;
|
||||
}
|
||||
if (sawUniform && binding >= 0 && IsIdentifierToken(tokens[k]) &&
|
||||
!IsNonLayoutQualifierKeyword(text)) {
|
||||
// 'text' is the type. Only sampler/image opaques carry unit
|
||||
// bindings; on anything else (e.g. atomic_uint, whose binding
|
||||
// is a counter-buffer index) record nothing.
|
||||
if (text.find("sampler") == String::npos && text.find("image") == String::npos) return;
|
||||
declaratorBegin = k + 1;
|
||||
break;
|
||||
}
|
||||
++k;
|
||||
}
|
||||
|
||||
if (!sawUniform || binding < 0 || declaratorBegin >= end) return;
|
||||
|
||||
for (SizeT k = declaratorBegin; k < end;) {
|
||||
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
|
||||
const String& name = tokens[k].text;
|
||||
++k;
|
||||
while (k < end && tokens[k].text == "[") {
|
||||
++k;
|
||||
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) ++k;
|
||||
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
||||
++k;
|
||||
}
|
||||
bindings[name] = static_cast<MobileGL::Uint>(binding);
|
||||
if (k >= end) break;
|
||||
if (tokens[k].text != ",") return; // opaque declarators cannot take initializers
|
||||
++k;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source) {
|
||||
UnorderedMap<String, Uint> bindings;
|
||||
// Fast path: without the qualifier keyword there is nothing to extract.
|
||||
if (source.find("binding") == String::npos) return bindings;
|
||||
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
Int braceDepth = 0;
|
||||
SizeT pos = 0;
|
||||
while (pos < count) {
|
||||
const String& text = tokens[pos].text;
|
||||
if (text == "{") {
|
||||
++braceDepth;
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (text == "}") {
|
||||
if (braceDepth > 0) --braceDepth;
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (braceDepth != 0 || text == ";") {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A depth-0 statement runs to its ';'. One that opens a brace instead is
|
||||
// a function definition or an interface/uniform block: a block's binding
|
||||
// is a buffer binding point, not a texture unit, so skip both alike.
|
||||
SizeT statementEnd = pos;
|
||||
while (statementEnd < count && tokens[statementEnd].text != ";" &&
|
||||
tokens[statementEnd].text != "{") {
|
||||
++statementEnd;
|
||||
}
|
||||
if (statementEnd >= count || tokens[statementEnd].text == "{") {
|
||||
pos = statementEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
RecordOpaqueDeclarationBindings(tokens, pos, statementEnd, bindings);
|
||||
pos = statementEnd + 1;
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Binding points a storage-block declaration starting at `bufferPos` occupies.
|
||||
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
|
||||
@@ -1560,8 +1705,8 @@ namespace MobileGL {
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
// The binding the qualifier run currently being scanned declared, -1 for none.
|
||||
// Several layout(...) lists may precede one declaration and the later one wins,
|
||||
// which is the same accumulate-then-consume shape the extractors above use.
|
||||
// Several layout(...) lists may precede one declaration and the later one wins:
|
||||
// accumulate, then consume at the `buffer` keyword.
|
||||
long long binding = -1;
|
||||
long long literal = 0;
|
||||
for (SizeT pos = 0; pos < count; ++pos) {
|
||||
@@ -1604,137 +1749,6 @@ namespace MobileGL {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<String> FindAtomicCounterOffsetViolation(const String& source) {
|
||||
// Fast path: both keywords are required for a violation to exist, and the pair is
|
||||
// absent from every shader-pack source.
|
||||
if (source.find("atomic_uint") == String::npos || source.find("offset") == String::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
constexpr long long kAtomicCounterSize = 4; // one 32-bit word per counter
|
||||
const long long maxBufferSize = static_cast<long long>(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
// The offset the qualifier run currently being scanned declared, -1 for none.
|
||||
// Same accumulate-then-consume shape as the storage-binding scan above.
|
||||
long long offset = -1;
|
||||
long long literal = 0;
|
||||
for (SizeT pos = 0; pos < count; ++pos) {
|
||||
const String& text = tokens[pos].text;
|
||||
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
|
||||
SizeT j = pos + 2;
|
||||
Int parenDepth = 1;
|
||||
while (j < count && parenDepth > 0) {
|
||||
const String& layoutToken = tokens[j].text;
|
||||
if (layoutToken == "(") {
|
||||
++parenDepth;
|
||||
} else if (layoutToken == ")") {
|
||||
--parenDepth;
|
||||
} else if (parenDepth == 1 && layoutToken == "offset" && j + 2 < count &&
|
||||
tokens[j + 1].text == "=" &&
|
||||
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||
offset = literal;
|
||||
j += 2;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
pos = j - 1;
|
||||
continue;
|
||||
}
|
||||
if (text == "atomic_uint") {
|
||||
// How far the declaration reaches: `atomic_uint c[N]` occupies N words
|
||||
// from the offset. An unparsable or absent declarator (an expression-sized
|
||||
// array, or the "layout(...) uniform atomic_uint;" default-qualifier form,
|
||||
// which declares no counter at all) is left alone rather than guessed at -
|
||||
// over-rejection here would be a compile failure the application cannot
|
||||
// work around.
|
||||
long long elements = 1;
|
||||
SizeT k = pos + 1;
|
||||
if (k < count && IsIdentifierToken(tokens[k])) {
|
||||
++k;
|
||||
if (k < count && tokens[k].text == "[") {
|
||||
elements = (k + 2 < count && tokens[k + 2].text == "]" &&
|
||||
ParseGlslIntegerLiteral(tokens[k + 1].text, literal))
|
||||
? std::max<long long>(1, literal)
|
||||
: -1;
|
||||
}
|
||||
} else {
|
||||
elements = -1;
|
||||
}
|
||||
// Clamped so the byte arithmetic below cannot overflow on an absurd
|
||||
// literal; any element count at or past the ceiling already fails.
|
||||
elements = std::min(elements, maxBufferSize);
|
||||
|
||||
if (offset >= 0 && elements > 0) {
|
||||
if (offset % kAtomicCounterSize != 0) {
|
||||
return "ERROR: invalid value " + std::to_string(offset) +
|
||||
" for layout specifier 'offset': an atomic counter offset must be a "
|
||||
"multiple of 4.";
|
||||
}
|
||||
if (offset > maxBufferSize - elements * kAtomicCounterSize) {
|
||||
return "ERROR: invalid value " + std::to_string(offset) +
|
||||
" for layout specifier 'offset': an atomic counter ending at byte " +
|
||||
std::to_string(offset + elements * kAtomicCounterSize) +
|
||||
" passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE (" +
|
||||
std::to_string(maxBufferSize) + ").";
|
||||
}
|
||||
}
|
||||
offset = -1;
|
||||
continue;
|
||||
}
|
||||
// `uniform` and the precision/auxiliary qualifiers may sit between the layout
|
||||
// list and the type keyword; anything else ends the run, so an offset never
|
||||
// leaks onto an unrelated declaration.
|
||||
if (text != "uniform" && !IsNonLayoutQualifierKeyword(text)) offset = -1;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
||||
UnorderedMap<String, Int> locations;
|
||||
// Fast path: without the qualifier keyword there is nothing to extract.
|
||||
if (source.find("location") == String::npos) return locations;
|
||||
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
Int braceDepth = 0;
|
||||
SizeT pos = 0;
|
||||
while (pos < count) {
|
||||
const String& text = tokens[pos].text;
|
||||
if (text == "{") {
|
||||
++braceDepth;
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (text == "}") {
|
||||
if (braceDepth > 0) --braceDepth;
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (braceDepth != 0 || text == ";") {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A depth-0 statement runs to its ';'. One that opens a brace instead is a
|
||||
// function definition or an interface/uniform block: neither can declare a
|
||||
// default-block uniform location, so hand the '{' back to the depth tracker.
|
||||
SizeT statementEnd = pos;
|
||||
while (statementEnd < count && tokens[statementEnd].text != ";" &&
|
||||
tokens[statementEnd].text != "{") {
|
||||
++statementEnd;
|
||||
}
|
||||
if (statementEnd >= count || tokens[statementEnd].text == "{") {
|
||||
pos = statementEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
RecordUniformDeclarationLocations(tokens, pos, statementEnd, locations);
|
||||
pos = statementEnd + 1;
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <set>
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/ProgramState/ShaderObject.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
@@ -43,50 +45,44 @@ namespace MobileGL {
|
||||
// compile-error text for the first violation, or nullopt for a clean source.
|
||||
std::optional<String> FindReservedIdentifierViolation(const String& source);
|
||||
|
||||
// Explicit layout(location = N) qualifiers on default-block uniform declarations,
|
||||
// keyed by declared name (no "[0]" suffix). Multi-declarator statements assign
|
||||
// consecutive locations, advancing by the array element count.
|
||||
//
|
||||
// Exists because the single link-compatible parse runs under relaxed Vulkan rules,
|
||||
// where glslang's vkRelaxedRemapUniformVariable moves plain uniforms into
|
||||
// MGL_GLOBAL_UBO and DISCARDS their location qualifiers ("ignoring layout qualifier
|
||||
// for uniform location"); opaque uniforms keep theirs. This lexical side-channel
|
||||
// restores the discarded locations to the GL location assigner
|
||||
// (ProgramObject::DoReflection). It scans preprocessor-visible text, so a
|
||||
// declaration inside an inactive #if branch is still recorded - harmless unless a
|
||||
// pack declares the same uniform with different explicit locations in alternative
|
||||
// branches (none observed; explicit uniform locations have zero incidence in the
|
||||
// shader-pack corpus, this is an ARB_explicit_uniform_location conformance surface).
|
||||
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source);
|
||||
|
||||
// Explicit layout(binding = N) on sampler/image uniforms, i.e. their initial
|
||||
// texture/image units. The Vulkan-client relaxed parse strips these before
|
||||
// mapIO can capture them, so they are recovered lexically (same narrow
|
||||
// grammar discipline as ExtractExplicitUniformLocations).
|
||||
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
||||
// NO SIDE-CHANNEL EXTRACTORS LIVE HERE ANY MORE. Three of them did - explicit
|
||||
// default-block uniform locations, explicit sampler/image bindings, and the storage
|
||||
// blocks that declared no binding - each recovering something MobileGL's
|
||||
// Vulkan-client relaxed parse destroys. All three are now taken from glslang at the
|
||||
// point of destruction instead:
|
||||
// * uniform locations: a snapshot inside vkRelaxedRemapUniformVariable, read back
|
||||
// through CollectExplicitUniformLocations (ShaderCompiler.h);
|
||||
// * opaque bindings and unqualified storage blocks:
|
||||
// TMglGlslIoResolver::reserverResourceSlot, which mapIO calls while the
|
||||
// qualifier still says what the shader declared.
|
||||
// The rewrites below stay lexical by construction - they exist to make glslang
|
||||
// ACCEPT input it would otherwise reject, so they cannot be built on its parse.
|
||||
|
||||
// A shader storage block whose layout(binding = N) reaches or passes
|
||||
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
|
||||
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what
|
||||
// has to fit. glslang cannot raise it for MobileGL: every shader is parsed as a Vulkan
|
||||
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
|
||||
// has no storage-buffer binding field to check against in the first place. Returns the
|
||||
// compile-error text for the first violation, or nullopt for a clean source.
|
||||
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
|
||||
// means "nothing to check against" and every declaration passes.
|
||||
// has to fit. Returns the compile-error text for the first violation, or nullopt for
|
||||
// a clean source. `maxBindings` is what glGetIntegerv answers for that pname; a
|
||||
// non-positive value means "nothing to check against" and every declaration passes.
|
||||
//
|
||||
// THE ONE SCAN THAT COULD NOT MOVE TO GLSLANG, and the reason is structural rather
|
||||
// than a matter of where the check is written. glslang has no resource limit for this
|
||||
// ceiling at all - Include/ResourceLimits.h carries maxAtomicCounterBindings,
|
||||
// maxCombinedTextureImageUnits and forty others, but nothing for uniform-block or
|
||||
// storage-block binding points - so there is no number for a parse-time check to
|
||||
// compare against, and the relaxed Vulkan rules MobileGL parses under would exempt it
|
||||
// anyway (ParseHelper.cpp layoutTypeCheck gates its binding ceilings on
|
||||
// `spvVersion.vulkan == 0`). Reading the AST post-parse from MobileGL is possible and
|
||||
// would be strictly better - a macro-spelled binding would finally be checked - but
|
||||
// the limit is a per-device number that CompileEnv deliberately keeps OUT of
|
||||
// frontendFingerprint (see its classification), so the L1c parse-verdict key would
|
||||
// have to grow it before any such verdict could be memoized. That is a cache-key
|
||||
// change in exchange for a new REJECTION surface, which is the one direction that
|
||||
// cannot be validated without device time.
|
||||
//
|
||||
// Consequence, and it is deliberate: a binding this scanner cannot read as a literal
|
||||
// is not judged. Under-rejection, never over-rejection.
|
||||
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
|
||||
|
||||
// GL 4.6 core 7.7 / ARB_shader_atomic_counters makes it a COMPILE-time error to
|
||||
// declare an atomic counter at an offset that is not a multiple of 4, or whose last
|
||||
// byte passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces both in fixOffset(),
|
||||
// which the Vulkan-relaxed parse never reaches (vkRelaxedRemapUniformVariable folds
|
||||
// the atomic_uint into a synthesized storage block and returns from declareVariable()
|
||||
// first), so MobileGL only caught them at LINK - and
|
||||
// KHR-GL43.shader_atomic_counters.negative-offset-1 never links at all. The
|
||||
// cross-stage rule (two counters sharing a binding must not overlap) stays at link:
|
||||
// a single-stage source cannot see it. Returns the compile-error text for the first
|
||||
// violation, or nullopt for a clean source.
|
||||
std::optional<String> FindAtomicCounterOffsetViolation(const String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "BakeImageFormatsPass.h"
|
||||
|
||||
#include "WidenImageFormatsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
@@ -418,7 +420,18 @@ namespace MobileGL {
|
||||
// Those formats are completed in the emitted text instead (see
|
||||
// PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for
|
||||
// them, which is exactly the state that pass looks for.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) continue;
|
||||
//
|
||||
// UNLESS the format widens exactly: WidenImageFormatsPass runs immediately
|
||||
// after this one on the ESSL chain and rewrites it to a core four-channel
|
||||
// carrier SPIRV-Cross does print, masking the accesses back to the channels
|
||||
// the baked format has. So for those the module IS the right place, and
|
||||
// routing them to the text completion instead would spell the narrow format
|
||||
// the driver rejects. The two lists are asked in this order because
|
||||
// printability is the cheaper and more common answer.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format)) &&
|
||||
WidenImageFormatsPass::WidenedCoreEsslImageFormat(formatIt->second) == 0) {
|
||||
continue;
|
||||
}
|
||||
// spirv-val: "Expected Image Format to match Sampled Type". A bind format
|
||||
// whose class disagrees with the declaration is an application error GL
|
||||
// leaves undefined; baking it would turn that into an invalid module, so it
|
||||
|
||||
@@ -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,33 +60,42 @@ 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
|
||||
// KHR-GL43.compute_shader.fp64-case1
|
||||
// KHR-GL43.compute_shader.fp64-case3
|
||||
// ...and the std430 half of the same stdLayout case.
|
||||
//
|
||||
// They 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. 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.
|
||||
// fp64-case3 is NOT this pass's to fix, and is listed only so it stops being counted
|
||||
// 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.
|
||||
//
|
||||
// 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. Four of 16085 cases; deliberately not
|
||||
// attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// operation whose validity depends on the operand really being 64 bits wide:
|
||||
|
||||
@@ -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
|
||||
@@ -52,6 +52,10 @@ namespace MobileGL {
|
||||
// lowering, whose cost is the array length rather than the trip count, takes
|
||||
// it instead. Real shaders of this shape (Minecraft 26.3's OIT coefficient
|
||||
// writer included) iterate a handful of times.
|
||||
//
|
||||
// This is a budget for the whole NEST, not for one loop: marking a loop for
|
||||
// unrolling means marking its ancestors too (see MarkLoopsForUnroll), and the
|
||||
// copies they produce multiply.
|
||||
constexpr size_t kMaxUnrolledIterations = 64;
|
||||
|
||||
struct DynamicIndexUse {
|
||||
@@ -228,12 +232,17 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether fully unrolling |loop| is bounded work. The trip count is read the
|
||||
// same way the stock unroller reads it, so a loop this declines to measure is
|
||||
// one CanPerformUnroll would refuse anyway - the hint would be inert on it,
|
||||
// and the fallback lowering is what handles it. Requires the induction
|
||||
// variable to already be an OpPhi, which is why this runs after ssa-rewrite.
|
||||
bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) {
|
||||
// |loop|'s trip count, when it has a measurable one, in *outIterations. The
|
||||
// count is read the same way the stock unroller reads it, so a loop this
|
||||
// declines to measure is one CanPerformUnroll would refuse anyway - the hint
|
||||
// would be inert on it, and the fallback lowering is what handles it. Requires
|
||||
// the induction variable to already be an OpPhi, which is why this runs after
|
||||
// ssa-rewrite.
|
||||
//
|
||||
// A count of zero is reported as unmeasurable: it means nothing this pass can
|
||||
// multiply a nest's budget by, and a loop that never runs is not one whose
|
||||
// subscript needs folding.
|
||||
bool TryGetUnrollTripCount(spvtools::opt::Loop* loop, size_t* outIterations) {
|
||||
const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock();
|
||||
if (condition == nullptr) {
|
||||
return false;
|
||||
@@ -243,10 +252,12 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
size_t iterations = 0;
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) {
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations) ||
|
||||
iterations == 0) {
|
||||
return false;
|
||||
}
|
||||
return iterations <= kMaxUnrolledIterations;
|
||||
*outIterations = iterations;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -287,25 +298,51 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The offending chain's own loop AND every loop enclosing it, because
|
||||
// SPIRV-Tools only ever unrolls an INNERMOST loop and because the index that
|
||||
// has to become a literal may be an outer loop's induction variable.
|
||||
//
|
||||
// Marking a whole nest means the unrolled body count is the PRODUCT of its
|
||||
// trip counts, so the budget is spent as the walk climbs rather than tested
|
||||
// loop by loop - a nest of three levels each individually inside the cap is
|
||||
// its CUBE, which is neither bounded nor anything the fold chain downstream
|
||||
// can absorb. Same defect, same shape, and the same reasoning as
|
||||
// LegalizeResourceArrayIndexPass::MarkLoopsForUnroll, which is where it was
|
||||
// first measured; the two walks are deliberately identical.
|
||||
//
|
||||
// Every exit is a BREAK rather than a skip-and-keep-climbing: a loop that
|
||||
// cannot be marked is a gap the unroller cannot cross, which makes every
|
||||
// mark above it dead weight. Falling out of the unroll path costs nothing
|
||||
// correctness-wise - LowerToConstantSwitch still legalizes the chain, at a
|
||||
// cost proportional to the output array's length.
|
||||
spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function);
|
||||
size_t nestIterations = 1;
|
||||
for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr;
|
||||
loop = loop->GetParent()) {
|
||||
if (!IsBoundedUnrollCandidate(loop)) {
|
||||
continue;
|
||||
size_t iterations = 0;
|
||||
if (!TryGetUnrollTripCount(loop, &iterations)) {
|
||||
break;
|
||||
}
|
||||
// Division, not multiplication, so the test itself cannot overflow.
|
||||
if (iterations > kMaxUnrolledIterations / nestIterations) {
|
||||
break;
|
||||
}
|
||||
Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst();
|
||||
// Only a bare `None` control is promoted, and only when no extra
|
||||
// literal (PartialCount, PeelCount, ...) follows it: the unroller
|
||||
// tests the control word for equality with Unroll, so ORing the bit
|
||||
// into a control that already carries something - DontUnroll above
|
||||
// all - would neither unroll nor mean what it says.
|
||||
// all - would neither unroll nor mean what it says. An `Unroll` this
|
||||
// pass itself already wrote for another chain in the same nest ends the
|
||||
// walk too: everything above it was considered on that pass through.
|
||||
if (mergeInst == nullptr || mergeInst->NumOperands() != 3 ||
|
||||
mergeInst->GetSingleWordOperand(2) !=
|
||||
static_cast<uint32_t>(spv::LoopControlMask::MaskNone)) {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
mergeInst->SetOperand(
|
||||
2, {static_cast<uint32_t>(spv::LoopControlMask::Unroll)});
|
||||
nestIterations *= iterations;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
+432
-72
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.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
|
||||
@@ -6,7 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "LegalizeStorageBlockArrayIndexPass.h"
|
||||
#include "LegalizeResourceArrayIndexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
@@ -39,25 +39,37 @@ namespace MobileGL {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS is 16 on the devices MobileGL targets, and
|
||||
// each lowered element costs one basic block per write, so a module claiming
|
||||
// more than this is refused rather than exploded. The largest array in the
|
||||
// conformance suite is 8.
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS and GL_MAX_*_IMAGE_UNIFORMS are both 16 or
|
||||
// fewer on the devices MobileGL targets, and each lowered element costs one
|
||||
// basic block per write, so a module claiming more than this is refused rather
|
||||
// than exploded. The largest array in the conformance suite is 8.
|
||||
constexpr uint32_t kMaxLoweredArrayLength = 32;
|
||||
// One CFG-changing rewrite per round (analyses are dropped after each), so
|
||||
// the round budget bounds the work on a pathological module.
|
||||
constexpr int kMaxLoweringRounds = 256;
|
||||
// Full unrolling copies the body once per iteration, and nothing in the stock
|
||||
// unroller bounds that. Past this count the loop is left alone and the switch
|
||||
// lowering, whose cost is the array length rather than the trip count, takes
|
||||
// it instead. A loop over an array of storage blocks iterates at most
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS times in any shader that is not already
|
||||
// broken.
|
||||
// unroller bounds that. Past this many copies the nest is left alone and the
|
||||
// switch lowering, whose cost is the array length rather than the trip count,
|
||||
// takes it instead. A loop over an array of storage blocks or of images iterates
|
||||
// at most GL_MAX_*_SHADER_STORAGE_BLOCKS / GL_MAX_*_IMAGE_UNIFORMS times in any
|
||||
// shader that is not already broken.
|
||||
//
|
||||
// This is a budget for the whole NEST, not for one loop: marking a loop for
|
||||
// unrolling means marking its ancestors too (see MarkLoopsForUnroll), and the
|
||||
// copies they produce multiply.
|
||||
constexpr size_t kMaxUnrolledIterations = 64;
|
||||
|
||||
struct ResourceArray {
|
||||
uint32_t length = 0;
|
||||
// Which lowering the chain's uses need; see the header. Detection and
|
||||
// loop-marking are identical for both.
|
||||
bool isImage = false;
|
||||
};
|
||||
|
||||
struct DynamicIndexUse {
|
||||
Instruction* accessChain = nullptr;
|
||||
uint32_t arrayLength = 0;
|
||||
bool isImageArray = false;
|
||||
};
|
||||
|
||||
bool HasDecoration(IRContext* context, uint32_t id, spv::Decoration kind) {
|
||||
@@ -73,16 +85,24 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every variable that is an ARRAY OF STORAGE BLOCKS, mapped to that array's
|
||||
// length. Two spellings are accepted because both reach here depending on the
|
||||
// SPIR-V version glslang targets: StorageBuffer + Block (1.3, what MobileGL
|
||||
// asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM block
|
||||
// array - Uniform + Block - is deliberately NOT collected; see the header.
|
||||
// Every variable that is an ARRAY OF STORAGE BLOCKS or an ARRAY OF IMAGE
|
||||
// UNIFORMS, mapped to that array's length and kind.
|
||||
//
|
||||
// Storage blocks: two spellings are accepted because both reach here depending
|
||||
// on the SPIR-V version glslang targets: StorageBuffer + Block (1.3, what
|
||||
// MobileGL asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM
|
||||
// block array - Uniform + Block - is deliberately NOT collected; see the header.
|
||||
//
|
||||
// Images: UniformConstant + OpTypeArray of OpTypeImage. Sampled == 2 is what
|
||||
// separates a storage image - what GLSL calls `image2D` and what the ES rule is
|
||||
// about - from the OpTypeImage that sits INSIDE an OpTypeSampledImage, which
|
||||
// never appears as an array element type on its own here and whose array ESSL
|
||||
// 3.20 4.1.7 explicitly permits a dynamically-uniform index.
|
||||
//
|
||||
// A length that is not a plain OpConstant (a spec constant) maps to 0: still
|
||||
// detected as illegal ESSL, never lowered.
|
||||
std::unordered_map<uint32_t, uint32_t> CollectStorageBlockArrays(IRContext* context) {
|
||||
std::unordered_map<uint32_t, uint32_t> blockArrays;
|
||||
std::unordered_map<uint32_t, ResourceArray> CollectResourceArrays(IRContext* context) {
|
||||
std::unordered_map<uint32_t, ResourceArray> resourceArrays;
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
|
||||
@@ -93,7 +113,8 @@ namespace MobileGL {
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::StorageBuffer &&
|
||||
storageClass != spv::StorageClass::Uniform) {
|
||||
storageClass != spv::StorageClass::Uniform &&
|
||||
storageClass != spv::StorageClass::UniformConstant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,17 +127,33 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
Instruction* elementType = defUseMgr->GetDef(pointeeType->GetSingleWordInOperand(0));
|
||||
if (elementType == nullptr || elementType->opcode() != spv::Op::OpTypeStruct) {
|
||||
if (elementType == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isStorageBlock =
|
||||
storageClass == spv::StorageClass::StorageBuffer
|
||||
? HasDecoration(context, elementType->result_id(), spv::Decoration::Block)
|
||||
: HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::BufferBlock);
|
||||
if (!isStorageBlock) {
|
||||
continue;
|
||||
bool isImage = false;
|
||||
if (storageClass == spv::StorageClass::UniformConstant) {
|
||||
// OpTypeImage <result> <sampled type> <dim> <depth> <arrayed> <ms>
|
||||
// <sampled> <format>
|
||||
if (elementType->opcode() != spv::Op::OpTypeImage ||
|
||||
elementType->NumInOperands() < 6 ||
|
||||
elementType->GetSingleWordInOperand(5) != 2u) {
|
||||
continue;
|
||||
}
|
||||
isImage = true;
|
||||
} else {
|
||||
if (elementType->opcode() != spv::Op::OpTypeStruct) {
|
||||
continue;
|
||||
}
|
||||
const bool isStorageBlock =
|
||||
storageClass == spv::StorageClass::StorageBuffer
|
||||
? HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::Block)
|
||||
: HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::BufferBlock);
|
||||
if (!isStorageBlock) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t arrayLength = 0;
|
||||
@@ -125,9 +162,9 @@ namespace MobileGL {
|
||||
if (lengthConstant != nullptr && lengthConstant->AsIntConstant() != nullptr) {
|
||||
arrayLength = lengthConstant->AsIntConstant()->GetU32BitValue();
|
||||
}
|
||||
blockArrays.emplace(inst.result_id(), arrayLength);
|
||||
resourceArrays.emplace(inst.result_id(), ResourceArray{arrayLength, isImage});
|
||||
}
|
||||
return blockArrays;
|
||||
return resourceArrays;
|
||||
}
|
||||
|
||||
// "Constant integral expression" in the ESSL sense: an OpConstant (or the
|
||||
@@ -140,17 +177,17 @@ namespace MobileGL {
|
||||
def->opcode() == spv::Op::OpConstantNull);
|
||||
}
|
||||
|
||||
// Access chains that index an array of storage blocks with a non-constant.
|
||||
// Only the FIRST index is considered: it is the one that selects the block,
|
||||
// and it is the only one ESSL constrains here. Indices inside the block - the
|
||||
// member selector and any array subscript below it - are legal however they
|
||||
// are computed, and chains rooted at another access chain are already inside
|
||||
// one element.
|
||||
// Access chains that index an array of storage blocks or of images with a
|
||||
// non-constant. Only the FIRST index is considered: it is the one that selects
|
||||
// the element, and it is the only one ESSL constrains here. Indices inside the
|
||||
// block - the member selector and any array subscript below it - are legal
|
||||
// however they are computed, and chains rooted at another access chain are
|
||||
// already inside one element.
|
||||
std::vector<DynamicIndexUse> CollectDynamicIndexUses(IRContext* context) {
|
||||
std::vector<DynamicIndexUse> uses;
|
||||
const std::unordered_map<uint32_t, uint32_t> blockArrays =
|
||||
CollectStorageBlockArrays(context);
|
||||
if (blockArrays.empty()) {
|
||||
const std::unordered_map<uint32_t, ResourceArray> resourceArrays =
|
||||
CollectResourceArrays(context);
|
||||
if (resourceArrays.empty()) {
|
||||
return uses;
|
||||
}
|
||||
|
||||
@@ -164,14 +201,15 @@ namespace MobileGL {
|
||||
if (inst.NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
const auto arrayIt = blockArrays.find(inst.GetSingleWordInOperand(0));
|
||||
if (arrayIt == blockArrays.end()) {
|
||||
const auto arrayIt = resourceArrays.find(inst.GetSingleWordInOperand(0));
|
||||
if (arrayIt == resourceArrays.end()) {
|
||||
continue;
|
||||
}
|
||||
if (IsConstantIndex(context, inst.GetSingleWordInOperand(1))) {
|
||||
continue;
|
||||
}
|
||||
uses.push_back({&inst, arrayIt->second});
|
||||
uses.push_back(
|
||||
{&inst, arrayIt->second.length, arrayIt->second.isImage});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,12 +290,17 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether fully unrolling |loop| is bounded work. The trip count is read the
|
||||
// same way the stock unroller reads it, so a loop this declines to measure is
|
||||
// one CanPerformUnroll would refuse anyway - the hint would be inert on it,
|
||||
// and the fallback lowering is what handles it. Requires the induction
|
||||
// variable to already be an OpPhi, which is why this runs after ssa-rewrite.
|
||||
bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) {
|
||||
// |loop|'s trip count, when it has a measurable one, in *outIterations. The
|
||||
// count is read the same way the stock unroller reads it, so a loop this
|
||||
// declines to measure is one CanPerformUnroll would refuse anyway - the hint
|
||||
// would be inert on it, and the fallback lowering is what handles it. Requires
|
||||
// the induction variable to already be an OpPhi, which is why this runs after
|
||||
// ssa-rewrite.
|
||||
//
|
||||
// A count of zero is reported as unmeasurable: it means nothing this pass can
|
||||
// multiply a nest's budget by, and a loop that never runs is not one whose
|
||||
// subscript needs folding.
|
||||
bool TryGetUnrollTripCount(spvtools::opt::Loop* loop, size_t* outIterations) {
|
||||
const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock();
|
||||
if (condition == nullptr) {
|
||||
return false;
|
||||
@@ -267,14 +310,16 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
size_t iterations = 0;
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) {
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations) ||
|
||||
iterations == 0) {
|
||||
return false;
|
||||
}
|
||||
return iterations <= kMaxUnrolledIterations;
|
||||
*outIterations = iterations;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
bool LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing(
|
||||
const std::vector<uint32_t>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
@@ -289,11 +334,11 @@ namespace MobileGL {
|
||||
return !CollectDynamicIndexUses(context.get()).empty();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::Process() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::Process() {
|
||||
return m_mode == Mode::MarkLoopsForUnroll ? MarkLoopsForUnroll() : LowerToConstantSwitch();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::MarkLoopsForUnroll() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::MarkLoopsForUnroll() {
|
||||
auto* irContext = context();
|
||||
const std::vector<DynamicIndexUse> uses = CollectDynamicIndexUses(irContext);
|
||||
if (uses.empty()) {
|
||||
@@ -311,25 +356,58 @@ namespace MobileGL {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The offending chain's own loop AND every loop enclosing it, because
|
||||
// SPIRV-Tools only ever unrolls an INNERMOST loop - an outer one is
|
||||
// unrollable only once its children are gone - and because the index that
|
||||
// has to become a literal may be an outer loop's induction variable.
|
||||
//
|
||||
// Marking a whole nest means the unrolled body count is the PRODUCT of its
|
||||
// trip counts, not the largest of them, so the budget is spent as the walk
|
||||
// climbs rather than tested loop by loop. Measured through
|
||||
// LegalizeResourceArrayIndexingForEssl itself, on a twelve-line shader
|
||||
// writing image2D g_image[4] from a 64/64/4 nest - every level individually
|
||||
// inside the per-loop cap, which is all this used to test: 256 OpImageWrite
|
||||
// with the per-loop cap alone against 4 with the nest budget, and the
|
||||
// per-loop cap bounds nothing at all as the trip counts grow. The image half
|
||||
// of this pass is what made such a nest reachable; a storage-block array
|
||||
// rarely sits inside one. BoundsTheWholeLoopNestAndNotEachLoopSeparately is
|
||||
// that measurement.
|
||||
//
|
||||
// Every exit is a BREAK rather than a skip-and-keep-climbing. A loop that
|
||||
// cannot be marked - unmeasurable, out of budget, or carrying a control this
|
||||
// pass will not overwrite - is a gap the unroller cannot cross, which makes
|
||||
// every mark above it dead weight on a module that will not be unrolled
|
||||
// anyway. Falling out of the unroll path costs nothing correctness-wise:
|
||||
// LowerToConstantSwitch still legalizes the chain, at a cost proportional to
|
||||
// the ARRAY LENGTH rather than to the trip counts.
|
||||
spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function);
|
||||
size_t nestIterations = 1;
|
||||
for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr;
|
||||
loop = loop->GetParent()) {
|
||||
if (!IsBoundedUnrollCandidate(loop)) {
|
||||
continue;
|
||||
size_t iterations = 0;
|
||||
if (!TryGetUnrollTripCount(loop, &iterations)) {
|
||||
break;
|
||||
}
|
||||
// Division, not multiplication, so the test itself cannot overflow.
|
||||
if (iterations > kMaxUnrolledIterations / nestIterations) {
|
||||
break;
|
||||
}
|
||||
Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst();
|
||||
// Only a bare `None` control is promoted, and only when no extra
|
||||
// literal (PartialCount, PeelCount, ...) follows it: the unroller
|
||||
// tests the control word for equality with Unroll, so ORing the bit
|
||||
// into a control that already carries something - DontUnroll above
|
||||
// all - would neither unroll nor mean what it says.
|
||||
// all - would neither unroll nor mean what it says. An `Unroll` this
|
||||
// pass itself already wrote for another chain in the same nest ends the
|
||||
// walk too: everything above it was considered on that pass through.
|
||||
if (mergeInst == nullptr || mergeInst->NumOperands() != 3 ||
|
||||
mergeInst->GetSingleWordOperand(2) !=
|
||||
static_cast<uint32_t>(spv::LoopControlMask::MaskNone)) {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
mergeInst->SetOperand(
|
||||
2, {static_cast<uint32_t>(spv::LoopControlMask::Unroll)});
|
||||
nestIterations *= iterations;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -337,11 +415,11 @@ namespace MobileGL {
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
MGLOG_D("[spirv] storage-block array index: marked enclosing loops for full unrolling");
|
||||
MGLOG_D("[spirv] resource array index: marked enclosing loops for full unrolling");
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::LowerToConstantSwitch() {
|
||||
spvtools::opt::Pass::Status LegalizeResourceArrayIndexPass::LowerToConstantSwitch() {
|
||||
auto* irContext = context();
|
||||
|
||||
bool modified = false;
|
||||
@@ -357,7 +435,8 @@ namespace MobileGL {
|
||||
if (declined.count(use.accessChain->result_id()) != 0) {
|
||||
continue;
|
||||
}
|
||||
const LoweringOutcome outcome = LowerOneChain(use.accessChain, use.arrayLength);
|
||||
const LoweringOutcome outcome =
|
||||
LowerOneChain(use.accessChain, use.arrayLength, use.isImageArray);
|
||||
if (outcome == LoweringOutcome::Declined) {
|
||||
declined.insert(use.accessChain->result_id());
|
||||
continue;
|
||||
@@ -383,17 +462,21 @@ namespace MobileGL {
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength) {
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength,
|
||||
bool isImageArray) {
|
||||
auto* irContext = context();
|
||||
if (arrayLength == 0 || arrayLength > kMaxLoweredArrayLength) {
|
||||
MGLOG_D("[spirv] storage-block array index: array length %u is not lowerable",
|
||||
MGLOG_D("[spirv] resource array index: array length %u is not lowerable",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (!IsLowerableIndexType(irContext, accessChain->GetSingleWordInOperand(1))) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (isImageArray) {
|
||||
return LowerImageChain(accessChain, arrayLength);
|
||||
}
|
||||
|
||||
std::vector<Instruction*> stores;
|
||||
std::vector<Instruction*> loads;
|
||||
@@ -458,8 +541,8 @@ namespace MobileGL {
|
||||
// every path. An index outside [0, length) reaches the default target, which is
|
||||
// the merge block: nothing is stored, which is what indexing a block array out of
|
||||
// range already meant.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength,
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* store) {
|
||||
auto* irContext = context();
|
||||
BasicBlock* block = irContext->get_instr_block(store);
|
||||
@@ -543,8 +626,8 @@ namespace MobileGL {
|
||||
// pick with OpSelect. Reading the elements the shader did not ask for is safe -
|
||||
// every one of them is a storage block this stage already declares, and an ES
|
||||
// driver bounds-checks a storage buffer read that lands outside what is bound.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength,
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load) {
|
||||
auto* irContext = context();
|
||||
uint32_t conditionTypeId = 0;
|
||||
@@ -594,16 +677,293 @@ namespace MobileGL {
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::MarkLoopsForUnroll));
|
||||
// An image array's chain is never stored or loaded THROUGH the way a storage
|
||||
// block's is: it is OpLoad-ed once into an opaque image object, and the image ops
|
||||
// consume that object. So this resolves the chain one CONSUMER at a time - the
|
||||
// round loop in LowerToConstantSwitch recollects after each - and refuses anything
|
||||
// that is not a plain read or write of the loaded image.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageChain(Instruction* accessChain, uint32_t arrayLength) {
|
||||
auto* irContext = context();
|
||||
|
||||
std::vector<Instruction*> loads;
|
||||
bool unsupportedUse = false;
|
||||
irContext->get_def_use_mgr()->ForEachUser(accessChain, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
return;
|
||||
case spv::Op::OpLoad:
|
||||
// Memory operands would be dropped by the per-element rebuild, so a
|
||||
// load carrying any is refused instead.
|
||||
if (user->NumInOperands() == 1) {
|
||||
loads.push_back(user);
|
||||
} else {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// OpImageTexelPointer above all: that is how an imageAtomic* reaches
|
||||
// the array, and running one per element would perform every OTHER
|
||||
// element's atomic as well - a read can be thrown away, a
|
||||
// read-modify-write cannot.
|
||||
unsupportedUse = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (unsupportedUse) {
|
||||
MGLOG_D("[spirv] image array index: chain %%%u has a use this pass cannot rewrite",
|
||||
accessChain->result_id());
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
if (loads.empty()) {
|
||||
// No uses left: the chain itself is what detection is still seeing.
|
||||
irContext->KillInst(accessChain);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
Instruction* load = loads.front();
|
||||
Instruction* consumer = nullptr;
|
||||
bool unsupportedConsumer = false;
|
||||
irContext->get_def_use_mgr()->ForEachUser(load, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
return;
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageRead:
|
||||
// imageSize()/imageSamples() carry the image in the same leading operand
|
||||
// position as an OpImageRead and produce an int or int vector, so the same
|
||||
// select ladder rebuilds them exactly - and a size query touches no memory
|
||||
// at all, which makes evaluating it for every element strictly safer than
|
||||
// the read the ladder was written for.
|
||||
case spv::Op::OpImageQuerySize:
|
||||
case spv::Op::OpImageQuerySizeLod:
|
||||
if (consumer == nullptr) consumer = user;
|
||||
return;
|
||||
default:
|
||||
// A sampled-image construction, a copy, an argument to a function:
|
||||
// shapes whose per-element rebuild this pass cannot spell exactly.
|
||||
unsupportedConsumer = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (unsupportedConsumer) {
|
||||
MGLOG_D("[spirv] image array index: the image loaded from chain %%%u is consumed by "
|
||||
"an operation this pass cannot rewrite",
|
||||
accessChain->result_id());
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (consumer == nullptr) {
|
||||
irContext->KillInst(load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
return consumer->opcode() == spv::Op::OpImageWrite
|
||||
? LowerImageWrite(accessChain, arrayLength, load, consumer)
|
||||
: LowerImageReadOrQuery(accessChain, arrayLength, load, consumer);
|
||||
}
|
||||
|
||||
// Drops |load| and |accessChain| once the rewrite above has taken their last user,
|
||||
// in that order - the load is what uses the chain. Anything still using either is
|
||||
// another consumer a later round will come back for.
|
||||
void LegalizeResourceArrayIndexPass::KillImageChainIfDead(Instruction* accessChain,
|
||||
Instruction* load) {
|
||||
auto* irContext = context();
|
||||
if (irContext->get_def_use_mgr()->NumUsers(load) == 0) {
|
||||
irContext->KillInst(load);
|
||||
}
|
||||
if (irContext->get_def_use_mgr()->NumUsers(accessChain) == 0) {
|
||||
irContext->KillInst(accessChain);
|
||||
}
|
||||
}
|
||||
|
||||
// switch (idx) { case 0: imageStore(arr[0], ...); break; case 1: ... }
|
||||
//
|
||||
// The same block split as LowerStore, for the same reason: whatever followed the
|
||||
// write still runs exactly once on every path, and an index outside [0, length)
|
||||
// reaches the default target - the merge block - so nothing is written, which is
|
||||
// what indexing an image array out of range already meant.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageWrite(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load, Instruction* imageWrite) {
|
||||
auto* irContext = context();
|
||||
BasicBlock* block = irContext->get_instr_block(imageWrite);
|
||||
if (block == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
// Splitting a loop header keeps the label - and so the back edge's target - on
|
||||
// the first half while the OpLoopMerge moves to the second, which is not a loop
|
||||
// any more. Refuse instead of producing that.
|
||||
if (block->GetLoopMergeInst() != nullptr) {
|
||||
MGLOG_D("[spirv] image array index: write sits in a loop header, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
Function* function = block->GetParent();
|
||||
if (function == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
const uint32_t imageTypeId = load->type_id();
|
||||
// Coordinate, texel and any image operands, verbatim: only the image itself is
|
||||
// per-element.
|
||||
std::vector<Operand> tailOperands;
|
||||
for (uint32_t i = 1; i < imageWrite->NumInOperands(); ++i) {
|
||||
tailOperands.push_back(imageWrite->GetInOperand(i));
|
||||
}
|
||||
|
||||
const uint32_t mergeLabelId = irContext->TakeNextId();
|
||||
block->SplitBasicBlock(irContext, mergeLabelId, BasicBlock::iterator(imageWrite));
|
||||
// |imageWrite| now heads the merge block; the per-element writes replace it.
|
||||
irContext->KillInst(imageWrite);
|
||||
|
||||
std::vector<std::pair<Operand::OperandData, uint32_t>> targets;
|
||||
targets.reserve(arrayLength);
|
||||
BasicBlock* insertAfter = block;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t caseLabelId = irContext->TakeNextId();
|
||||
auto caseBlock = MakeUnique<BasicBlock>(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLabel, 0, caseLabelId, std::initializer_list<Operand>{}));
|
||||
caseBlock->SetParent(function);
|
||||
BasicBlock* casePtr = function->InsertBasicBlockAfter(std::move(caseBlock), insertAfter);
|
||||
// Hand-built label; see LowerStore for why it has to be registered here.
|
||||
irContext->AnalyzeDefUse(casePtr->GetLabelInst());
|
||||
irContext->set_instr_block(casePtr->GetLabelInst(), casePtr);
|
||||
|
||||
InstructionBuilder caseBuilder(
|
||||
irContext, casePtr,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(caseBuilder, irContext, accessChain, constantId);
|
||||
Instruction* elementImage =
|
||||
caseBuilder.AddLoad(imageTypeId, elementChain->result_id());
|
||||
|
||||
std::vector<Operand> writeOperands;
|
||||
writeOperands.push_back({SPV_OPERAND_TYPE_ID, {elementImage->result_id()}});
|
||||
for (const Operand& tailOperand : tailOperands) {
|
||||
writeOperands.push_back(tailOperand);
|
||||
}
|
||||
caseBuilder.AddInstruction(
|
||||
MakeUnique<Instruction>(irContext, spv::Op::OpImageWrite, 0, 0, writeOperands));
|
||||
caseBuilder.AddBranch(mergeLabelId);
|
||||
|
||||
targets.push_back({Operand::OperandData{element}, caseLabelId});
|
||||
insertAfter = casePtr;
|
||||
}
|
||||
|
||||
InstructionBuilder switchBuilder(
|
||||
irContext, block, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
switchBuilder.AddSwitch(indexId, mergeLabelId, targets, mergeLabelId);
|
||||
|
||||
KillImageChainIfDead(accessChain, load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] image array index: lowered a dynamic imageStore to a %u-way switch",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
// A value-producing consumer - an OpImageRead, or an imageSize()/imageSamples() query -
|
||||
// needs no control flow: run it against every element through a constant index and pick
|
||||
// with OpSelect. The selection happens on the RESULT, not on the image object - an
|
||||
// opaque type may not be selected at all (pre-1.4 OpSelect takes pointers, scalars
|
||||
// and vectors only, and ESSL has no ternary on an image), so what is duplicated is
|
||||
// the consuming instruction itself. Every such consumer carries the image in in-operand
|
||||
// 0 and nothing else that is per-element, so one rebuild spells all of them.
|
||||
//
|
||||
// Running the elements the shader did not ask for is safe: every one of them is an
|
||||
// image this stage already declares, and GL 4.6 7.11.2 makes a load through an
|
||||
// image unit whose binding is missing or incompatible return undefined DATA - never
|
||||
// an error, and never a fault - which the select then discards. A size query does not
|
||||
// even touch memory. Contrast an imageAtomic*, which LowerImageChain refuses for
|
||||
// exactly the opposite reason.
|
||||
LegalizeResourceArrayIndexPass::LoweringOutcome
|
||||
LegalizeResourceArrayIndexPass::LowerImageReadOrQuery(Instruction* accessChain,
|
||||
uint32_t arrayLength, Instruction* load,
|
||||
Instruction* consumer) {
|
||||
auto* irContext = context();
|
||||
uint32_t conditionTypeId = 0;
|
||||
uint32_t dimension = 0;
|
||||
if (!TryGetSelectConditionType(irContext, consumer->type_id(), &conditionTypeId,
|
||||
&dimension)) {
|
||||
MGLOG_D("[spirv] image array index: result type is not selectable, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
const uint32_t boolTypeId = irContext->get_type_mgr()->GetBoolTypeId();
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
const uint32_t imageTypeId = load->type_id();
|
||||
std::vector<Operand> tailOperands;
|
||||
for (uint32_t i = 1; i < consumer->NumInOperands(); ++i) {
|
||||
tailOperands.push_back(consumer->GetInOperand(i));
|
||||
}
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, consumer,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
uint32_t selectedId = 0;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(builder, irContext, accessChain, constantId);
|
||||
Instruction* elementImage = builder.AddLoad(imageTypeId, elementChain->result_id());
|
||||
|
||||
std::vector<Operand> elementOperands;
|
||||
elementOperands.push_back({SPV_OPERAND_TYPE_ID, {elementImage->result_id()}});
|
||||
for (const Operand& tailOperand : tailOperands) {
|
||||
elementOperands.push_back(tailOperand);
|
||||
}
|
||||
Instruction* elementResult = builder.AddInstruction(
|
||||
MakeUnique<Instruction>(irContext, consumer->opcode(), consumer->type_id(),
|
||||
irContext->TakeNextId(), elementOperands));
|
||||
if (element == 0) {
|
||||
// Element 0 is the else-arm of the whole ladder, so an out-of-range
|
||||
// index reads it - an undefined element for an undefined index.
|
||||
selectedId = elementResult->result_id();
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* isElement =
|
||||
builder.AddBinaryOp(boolTypeId, spv::Op::OpIEqual, indexId, constantId);
|
||||
uint32_t conditionId = isElement->result_id();
|
||||
if (dimension > 1) {
|
||||
std::vector<uint32_t> components(dimension, conditionId);
|
||||
conditionId = builder.AddCompositeConstruct(conditionTypeId, components)->result_id();
|
||||
}
|
||||
selectedId = builder
|
||||
.AddSelect(consumer->type_id(), conditionId,
|
||||
elementResult->result_id(), selectedId)
|
||||
->result_id();
|
||||
}
|
||||
|
||||
irContext->ReplaceAllUsesWith(consumer->result_id(), selectedId);
|
||||
irContext->KillInst(consumer);
|
||||
KillImageChainIfDead(accessChain, load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] image array index: lowered a dynamic image read/query to %u "
|
||||
"constant-indexed operations",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass() {
|
||||
LegalizeResourceArrayIndexPass::CreateMarkLoopsForUnrollPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::LowerToConstantSwitch));
|
||||
MakeUnique<LegalizeResourceArrayIndexPass>(Mode::MarkLoopsForUnroll));
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeResourceArrayIndexPass::CreateLowerToConstantSwitchPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeResourceArrayIndexPass>(Mode::LowerToConstantSwitch));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
@@ -0,0 +1,158 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Desktop GL lets an ARRAY OF SHADER STORAGE BLOCKS and an ARRAY OF IMAGE UNIFORMS
|
||||
// alike be indexed with any dynamically-uniform expression (GL 4.6 core / GLSL 4.30
|
||||
// 4.1.9). GLSL ES keeps the stricter ES 3.1 rule for BOTH - the index must be a
|
||||
// *constant integral expression* - and the drivers enforce it to the letter:
|
||||
//
|
||||
// Qualcomm, storage blocks:
|
||||
// '[' : indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted
|
||||
// Mesa, images:
|
||||
// image arrays indexed with non-constant expressions are forbidden in GLSL ES
|
||||
//
|
||||
// glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints
|
||||
// `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` - or
|
||||
// `uniform image2D g_image[4];` plus `imageStore(g_image[i], ...)` - verbatim, and
|
||||
// the stage never compiles. The backend program then links nothing and every draw
|
||||
// or dispatch that uses it is a silent no-op, which reads back as "the buffer was
|
||||
// never written" rather than as an error - the frontend has already published
|
||||
// GL_LINK_STATUS = TRUE from glslang's own link.
|
||||
//
|
||||
// Verified on the device for the storage-block half: an Adreno 830 ES probe with no
|
||||
// MobileGL in the loop rejects the non-constant subscript with AND without
|
||||
// GL_EXT_gpu_shader5 (which the driver does advertise), and accepts a constant one.
|
||||
// Verified again for the image half on llvmpipe / Mesa 26.1.4 at ES 3.2, on a raw
|
||||
// GLES probe: a scalar image and an array with literal subscripts both write the
|
||||
// units they name, and both a loop-variable subscript and a `const int[]` table
|
||||
// lookup are refused with the message above. So the ES 3.2 "dynamically uniform"
|
||||
// relaxation is not a way out for either resource - every index really has to
|
||||
// become a compile-time constant.
|
||||
//
|
||||
// SAMPLER arrays are deliberately NOT covered. ESSL 3.20 4.1.7 does allow a sampler
|
||||
// array a dynamically-uniform index, and the same probe confirms it: a sampler array
|
||||
// subscripted by a loop variable, and one reached through a const table, both compile
|
||||
// and link. Lowering them would cost code for a rule that does not exist.
|
||||
//
|
||||
// Two modes, used as two halves of one legalization in
|
||||
// ShaderCompiler::LegalizeResourceArrayIndexingForEssl - the same shape, for
|
||||
// the same reasons, as LegalizeFragmentOutputIndexPass:
|
||||
//
|
||||
// MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the
|
||||
// common shape, and full unrolling turns its index into a literal at no cost
|
||||
// in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose
|
||||
// OpLoopMerge carries the Unroll control, so this mode sets that hint on
|
||||
// exactly the loops that enclose an offending access chain, and only when
|
||||
// their trip count is known and small. Must run AFTER ssa-rewrite: both the
|
||||
// trip-count check and the unroller need the induction variable as an OpPhi.
|
||||
// Resource-kind-blind: the offending chain is the same instruction either way.
|
||||
//
|
||||
// LowerToConstantSwitch - the fallback for a genuinely dynamic index
|
||||
// (uniform-sourced, which is what the CTS indirect-addressing and resource-max
|
||||
// cases use). A write through such a chain becomes an OpSwitch over the
|
||||
// array's range with one constant-indexed access per case; a read becomes one
|
||||
// constant-indexed access per element combined with OpSelect. This is what ANGLE
|
||||
// does for the same ES 3.1 rule.
|
||||
//
|
||||
// This half IS kind-specific, because the two resources are consumed
|
||||
// differently. A storage block is reached by OpStore/OpLoad THROUGH the access
|
||||
// chain, so the chain's own users are rewritten. An image's access chain is
|
||||
// first OpLoad-ed into an opaque image OBJECT, which OpImageWrite/OpImageRead
|
||||
// then consume - and an opaque type may not be selected (OpSelect is restricted
|
||||
// to pointers, scalars and vectors before SPIR-V 1.4, and ESSL has no ternary on
|
||||
// image types at all), so it is the image OPERATION that is duplicated per
|
||||
// element, not the loaded object.
|
||||
//
|
||||
// A UNIFORM block array is a different namespace with its own (less strictly
|
||||
// enforced) rule and no observed failure, so it is deliberately left alone rather
|
||||
// than lowered on speculation.
|
||||
//
|
||||
// DirectGLES transpile path only: the original module is legal for Vulkan, which
|
||||
// has no such restriction, and DirectVulkan must keep seeing the array as one
|
||||
// descriptor array.
|
||||
//
|
||||
// The pass DECLINES - leaving the module untouched rather than half-transforming
|
||||
// it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a
|
||||
// function or chained further, an atomic or an OpArrayLength through the chain, a
|
||||
// load carrying memory operands, a spec-constant array length, an index that is
|
||||
// not a 32-bit integer, an image operation other than a plain read or write (an
|
||||
// OpImageTexelPointer, i.e. an imageAtomic*, above all - executing it per element
|
||||
// would perform the other elements' atomics too), or a store sitting in a loop
|
||||
// header block (splitting there would move the OpLoopMerge away from the back
|
||||
// edge's target).
|
||||
class LegalizeResourceArrayIndexPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode {
|
||||
MarkLoopsForUnroll,
|
||||
LowerToConstantSwitch,
|
||||
};
|
||||
|
||||
explicit LegalizeResourceArrayIndexPass(Mode mode) : m_mode(mode) {}
|
||||
|
||||
const char* name() const override {
|
||||
return m_mode == Mode::MarkLoopsForUnroll
|
||||
? "mobilegl-mark-resource-array-index-loops"
|
||||
: "mobilegl-lower-resource-array-index";
|
||||
}
|
||||
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass();
|
||||
static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass();
|
||||
|
||||
// The detection half, on a serialized module: true when an array of storage
|
||||
// blocks or of image uniforms is indexed with anything but an OpConstant. Cheap
|
||||
// enough to gate the whole legalization on (one BuildModule, no serialization)
|
||||
// and used again after the folding chain to decide whether the fallback has to
|
||||
// run at all.
|
||||
static bool BinaryHasDynamicResourceArrayIndexing(const std::vector<uint32_t>& binary);
|
||||
|
||||
private:
|
||||
enum class LoweringOutcome {
|
||||
// The shape is not one this pass can rewrite exactly; the module keeps
|
||||
// the illegal chain rather than a half-transform of it.
|
||||
Declined,
|
||||
Changed,
|
||||
};
|
||||
|
||||
Status MarkLoopsForUnroll();
|
||||
Status LowerToConstantSwitch();
|
||||
|
||||
LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
bool isImageArray);
|
||||
LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* store);
|
||||
LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load);
|
||||
LoweringOutcome LowerImageChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength);
|
||||
void KillImageChainIfDead(spvtools::opt::Instruction* accessChain,
|
||||
spvtools::opt::Instruction* load);
|
||||
LoweringOutcome LowerImageWrite(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load,
|
||||
spvtools::opt::Instruction* imageWrite);
|
||||
LoweringOutcome LowerImageReadOrQuery(spvtools::opt::Instruction* accessChain,
|
||||
uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load,
|
||||
spvtools::opt::Instruction* consumer);
|
||||
|
||||
Mode m_mode;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,120 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL 4.3 lets an ARRAY OF SHADER STORAGE BLOCKS be indexed with any
|
||||
// dynamically-uniform expression (GL 4.6 core / GLSL 4.30 4.1.9). GLSL ES keeps
|
||||
// the stricter ES 3.1 rule - the index must be a *constant integral expression* -
|
||||
// and the Qualcomm ES compiler enforces it to the letter:
|
||||
//
|
||||
// '[' : indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted
|
||||
//
|
||||
// glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints
|
||||
// `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` verbatim
|
||||
// and the stage never compiles. The backend program then links nothing and every
|
||||
// draw or dispatch that uses it is a silent no-op, which reads back as "the buffer
|
||||
// was never written" rather than as an error - the frontend has already published
|
||||
// GL_LINK_STATUS = TRUE from glslang's own link.
|
||||
//
|
||||
// Verified on the device: an Adreno 830 ES probe with no MobileGL in the loop
|
||||
// rejects the non-constant subscript with AND without GL_EXT_gpu_shader5 (which
|
||||
// the driver does advertise), and accepts a constant one. So the ES 3.2
|
||||
// "dynamically uniform" relaxation is not a way out - every index really has to
|
||||
// become a compile-time constant.
|
||||
//
|
||||
// Two modes, used as two halves of one legalization in
|
||||
// ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl - the same shape, for
|
||||
// the same reasons, as LegalizeFragmentOutputIndexPass:
|
||||
//
|
||||
// MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the
|
||||
// common shape, and full unrolling turns its index into a literal at no cost
|
||||
// in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose
|
||||
// OpLoopMerge carries the Unroll control, so this mode sets that hint on
|
||||
// exactly the loops that enclose an offending access chain, and only when
|
||||
// their trip count is known and small. Must run AFTER ssa-rewrite: both the
|
||||
// trip-count check and the unroller need the induction variable as an OpPhi.
|
||||
//
|
||||
// LowerToConstantSwitch - the fallback for a genuinely dynamic index
|
||||
// (uniform-sourced, which is what the CTS indirect-addressing and resource-max
|
||||
// cases use). A write through such a chain becomes an OpSwitch over the
|
||||
// array's range with one constant-indexed store per case; a read becomes one
|
||||
// constant-indexed load per element combined with OpSelect. This is what ANGLE
|
||||
// does for the same ES 3.1 rule.
|
||||
//
|
||||
// Storage blocks only. A UNIFORM block array is a different namespace with its own
|
||||
// (less strictly enforced) rule and no observed failure, so it is deliberately left
|
||||
// alone rather than lowered on speculation.
|
||||
//
|
||||
// DirectGLES transpile path only: the original module is legal for Vulkan, which
|
||||
// has no such restriction, and DirectVulkan must keep seeing the array as one
|
||||
// descriptor array.
|
||||
//
|
||||
// The pass DECLINES - leaving the module untouched rather than half-transforming
|
||||
// it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a
|
||||
// function or chained further, an atomic or an OpArrayLength through the chain, a
|
||||
// load carrying memory operands, a spec-constant array length, an index that is
|
||||
// not a 32-bit integer, or a store sitting in a loop header block (splitting there
|
||||
// would move the OpLoopMerge away from the back edge's target).
|
||||
class LegalizeStorageBlockArrayIndexPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode {
|
||||
MarkLoopsForUnroll,
|
||||
LowerToConstantSwitch,
|
||||
};
|
||||
|
||||
explicit LegalizeStorageBlockArrayIndexPass(Mode mode) : m_mode(mode) {}
|
||||
|
||||
const char* name() const override {
|
||||
return m_mode == Mode::MarkLoopsForUnroll
|
||||
? "mobilegl-mark-storage-block-array-index-loops"
|
||||
: "mobilegl-lower-storage-block-array-index";
|
||||
}
|
||||
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass();
|
||||
static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass();
|
||||
|
||||
// The detection half, on a serialized module: true when an array of storage
|
||||
// blocks is indexed with anything but an OpConstant. Cheap enough to gate the
|
||||
// whole legalization on (one BuildModule, no serialization) and used again
|
||||
// after the folding chain to decide whether the fallback has to run at all.
|
||||
static bool BinaryHasDynamicStorageBlockArrayIndexing(const std::vector<uint32_t>& binary);
|
||||
|
||||
private:
|
||||
enum class LoweringOutcome {
|
||||
// The shape is not one this pass can rewrite exactly; the module keeps
|
||||
// the illegal chain rather than a half-transform of it.
|
||||
Declined,
|
||||
Changed,
|
||||
};
|
||||
|
||||
Status MarkLoopsForUnroll();
|
||||
Status LowerToConstantSwitch();
|
||||
|
||||
LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength);
|
||||
LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* store);
|
||||
LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load);
|
||||
|
||||
Mode m_mode;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,652 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "Lower1DSampledImagesPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
|
||||
// 5 Sampled, 6 Format.
|
||||
constexpr uint32_t kDimOperand = 1;
|
||||
constexpr uint32_t kArrayedOperand = 3;
|
||||
constexpr uint32_t kSampledOperand = 5;
|
||||
|
||||
// Sampled == 1 is SPIR-V's "used WITH a sampler", i.e. exactly the sampler
|
||||
// uniforms this pass exists for. Sampled == 2 is the storage image
|
||||
// Lower1DArrayImagesPass owns, and Sampled == 0 ("either") is a shape glslang
|
||||
// never emits from GLSL - left out so an unexpected module is declined rather
|
||||
// than rewritten on a guess.
|
||||
bool Is1DSampledImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kSampledOperand &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) ==
|
||||
spv::Dim::Dim1D &&
|
||||
imageType->GetSingleWordInOperand(kSampledOperand) == 1u;
|
||||
}
|
||||
|
||||
bool Is1DSampledImageTypeOfArrayedness(const Instruction* imageType, bool arrayed) {
|
||||
return Is1DSampledImageType(imageType) &&
|
||||
(imageType->GetSingleWordInOperand(kArrayedOperand) == 1u) == arrayed;
|
||||
}
|
||||
|
||||
// Any Dim1D image still declared with Sampled == 1. Used only to decide whether
|
||||
// the Sampled1D capability is still needed after the rewrite.
|
||||
bool AnyDim1DSampledTypeLeft(IRContext* context) {
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DSampledImageType(&type)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a bare image, a
|
||||
// sampled image, or a pointer/array of either. Same unwrapping as
|
||||
// Lower1DArrayImagesPass, which needs the identical walk.
|
||||
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(objectId);
|
||||
if (object == nullptr) return nullptr;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
while (type != nullptr) {
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeImage:
|
||||
return type;
|
||||
case spv::Op::OpTypeSampledImage:
|
||||
case spv::Op::OpTypePointer:
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
// Each names its element type in its last in-operand, except arrays,
|
||||
// whose element type is the FIRST. Both are reached here because a
|
||||
// sampler uniform may be declared as an array of samplers.
|
||||
type = defUseMgr->GetDef(
|
||||
type->opcode() == spv::Op::OpTypeArray ||
|
||||
type->opcode() == spv::Op::OpTypeRuntimeArray
|
||||
? type->GetSingleWordInOperand(0)
|
||||
: type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
||||
continue;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// How this pass classifies an opcode that can touch one of these images.
|
||||
enum class OpKind {
|
||||
// Not an image operation at all: it may CARRY the image or sampled-image
|
||||
// value (OpLoad, OpSampledImage, OpCopyObject, ...) but it names no
|
||||
// coordinate, so the rewrite does not reach it.
|
||||
NotImageOp,
|
||||
// Addresses texels: has a coordinate at in-operand 1 and, from
|
||||
// `imageOperandsIndex`, an optional image-operands mask.
|
||||
Texel,
|
||||
// Reads a property whose result does not depend on Dim. Safe to leave.
|
||||
DimIndependentQuery,
|
||||
// Recognised, and refused: rewriting the type would change the shape of what
|
||||
// the shader consumes, or the operation is one this pass has no translation
|
||||
// for.
|
||||
Decline,
|
||||
};
|
||||
|
||||
struct OpClassification {
|
||||
OpKind kind = OpKind::NotImageOp;
|
||||
uint32_t coordinateOperand = 1;
|
||||
// In-operand index of the ImageOperands mask, when the opcode has one. The
|
||||
// mask itself is OPTIONAL for the implicit-Lod, fetch and gather forms, so
|
||||
// this is an index to test against NumInOperands(), not a promise.
|
||||
uint32_t imageOperandsIndex = 0;
|
||||
};
|
||||
|
||||
OpClassification ClassifyOpcode(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
// (image, coordinate, [operands]) - the mask, when present, is in-operand 2.
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageFetch:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSparseFetch:
|
||||
return {OpKind::Texel, 1u, 2u};
|
||||
|
||||
// (image, coordinate, D_ref, [operands]) - one operand more before the mask.
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleProjDrefExplicitLod:
|
||||
return {OpKind::Texel, 1u, 3u};
|
||||
|
||||
// OpImageQueryLod names a coordinate and no mask. Its coordinate is the PLANE
|
||||
// components only (no array layer), which the insert-at-1 rule widens just as
|
||||
// correctly as a sampling coordinate.
|
||||
case spv::Op::OpImageQueryLod:
|
||||
return {OpKind::Texel, 1u, /*no mask*/ 0xFFFFFFFFu};
|
||||
|
||||
// Scalar result, identical for Dim1D and Dim2D.
|
||||
case spv::Op::OpImageQueryLevels:
|
||||
return {OpKind::DimIndependentQuery, 0u, 0u};
|
||||
|
||||
// textureSize: int for a sampler1D, ivec2 for the sampler2D it would become.
|
||||
// There is no correct narrower answer to substitute, so the module is left
|
||||
// alone - the sibling pass refuses the same shape for the same reason.
|
||||
case spv::Op::OpImageQuerySize:
|
||||
case spv::Op::OpImageQuerySizeLod:
|
||||
// Gather is not available for 1D samplers in GLSL, so reaching one here means
|
||||
// an input this pass did not anticipate; and its ConstOffsets operand is an
|
||||
// ARRAY of offsets whose widening this pass does not implement.
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
// Storage-image traffic has no business reaching a Sampled == 1 image; if it
|
||||
// does, the module is not the shape this pass reasoned about.
|
||||
case spv::Op::OpImageRead:
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageSparseRead:
|
||||
case spv::Op::OpImageTexelPointer:
|
||||
case spv::Op::OpImageQuerySamples:
|
||||
return {OpKind::Decline, 0u, 0u};
|
||||
|
||||
default:
|
||||
return {OpKind::NotImageOp, 0u, 0u};
|
||||
}
|
||||
}
|
||||
|
||||
// How many ids each ImageOperands bit contributes, in the bit order SPIR-V lays
|
||||
// them out in. Only the bits that carry ids need an entry; the rest contribute
|
||||
// nothing and are skipped by having a count of zero.
|
||||
struct ImageOperandBit {
|
||||
spv::ImageOperandsMask bit;
|
||||
uint32_t idCount;
|
||||
};
|
||||
constexpr ImageOperandBit kImageOperandBits[] = {
|
||||
{spv::ImageOperandsMask::Bias, 1u},
|
||||
{spv::ImageOperandsMask::Lod, 1u},
|
||||
{spv::ImageOperandsMask::Grad, 2u},
|
||||
{spv::ImageOperandsMask::ConstOffset, 1u},
|
||||
{spv::ImageOperandsMask::Offset, 1u},
|
||||
{spv::ImageOperandsMask::ConstOffsets, 1u},
|
||||
{spv::ImageOperandsMask::Sample, 1u},
|
||||
{spv::ImageOperandsMask::MinLod, 1u},
|
||||
{spv::ImageOperandsMask::MakeTexelAvailable, 1u},
|
||||
{spv::ImageOperandsMask::MakeTexelVisible, 1u},
|
||||
{spv::ImageOperandsMask::NonPrivateTexel, 0u},
|
||||
{spv::ImageOperandsMask::VolatileTexel, 0u},
|
||||
{spv::ImageOperandsMask::SignExtend, 0u},
|
||||
{spv::ImageOperandsMask::ZeroExtend, 0u},
|
||||
{spv::ImageOperandsMask::Nontemporal, 0u},
|
||||
{spv::ImageOperandsMask::Offsets, 1u},
|
||||
};
|
||||
|
||||
// Where each of the operands this pass rewrites sits, for one instruction. An
|
||||
// index of 0 means "not present" - in-operand 0 is always the image, so it can
|
||||
// never be a real position for one of these.
|
||||
struct OperandPositions {
|
||||
uint32_t gradX = 0;
|
||||
uint32_t gradY = 0;
|
||||
uint32_t constOffset = 0;
|
||||
uint32_t offset = 0;
|
||||
// A bit this pass does not know how to widen appeared on a covered image.
|
||||
bool unsupported = false;
|
||||
|
||||
bool Any() const { return gradX != 0 || constOffset != 0 || offset != 0; }
|
||||
};
|
||||
|
||||
OperandPositions LocateOperands(const Instruction& instruction,
|
||||
uint32_t imageOperandsIndex) {
|
||||
OperandPositions positions;
|
||||
if (imageOperandsIndex == 0xFFFFFFFFu ||
|
||||
instruction.NumInOperands() <= imageOperandsIndex) {
|
||||
return positions;
|
||||
}
|
||||
const uint32_t mask = instruction.GetSingleWordInOperand(imageOperandsIndex);
|
||||
uint32_t next = imageOperandsIndex + 1u;
|
||||
for (const ImageOperandBit& entry : kImageOperandBits) {
|
||||
if ((mask & static_cast<uint32_t>(entry.bit)) == 0u) continue;
|
||||
switch (entry.bit) {
|
||||
case spv::ImageOperandsMask::Grad:
|
||||
positions.gradX = next;
|
||||
positions.gradY = next + 1u;
|
||||
break;
|
||||
case spv::ImageOperandsMask::ConstOffset:
|
||||
positions.constOffset = next;
|
||||
break;
|
||||
case spv::ImageOperandsMask::Offset:
|
||||
positions.offset = next;
|
||||
break;
|
||||
case spv::ImageOperandsMask::ConstOffsets:
|
||||
case spv::ImageOperandsMask::Offsets:
|
||||
// An array of offsets, only meaningful for gather - which is declined
|
||||
// above. Refuse rather than translate half of it.
|
||||
positions.unsupported = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
next += entry.idCount;
|
||||
}
|
||||
// Every id the mask claimed has to actually be there; a truncated operand
|
||||
// list means the instruction is not the shape this walk assumed.
|
||||
if (next > instruction.NumInOperands()) {
|
||||
positions.unsupported = true;
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
// Whether this instruction so much as mentions a value whose type resolves to a
|
||||
// covered image. Used to make sure nothing reaches these images through an opcode
|
||||
// this pass never considered: the answer decides between rewriting and declining,
|
||||
// never between two different rewrites.
|
||||
template <typename CoveredFn>
|
||||
bool MentionsCoveredImage(IRContext* context, const Instruction& instruction,
|
||||
const CoveredFn& covered) {
|
||||
bool mentions = false;
|
||||
instruction.ForEachInId([&](const uint32_t* id) {
|
||||
if (mentions || id == nullptr) return;
|
||||
if (covered(ResolveImageType(context, *id))) mentions = true;
|
||||
});
|
||||
return mentions;
|
||||
}
|
||||
|
||||
// The component type of a value, and how many of them it has. A scalar reports a
|
||||
// count of 1; anything that is neither an int/float scalar nor a vector of one
|
||||
// reports 0, which every caller treats as "not a shape this pass translates".
|
||||
struct ValueShape {
|
||||
const analysis::Type* componentType = nullptr;
|
||||
uint32_t componentCount = 0;
|
||||
bool IsScalar() const { return componentCount == 1u; }
|
||||
};
|
||||
|
||||
ValueShape DescribeValue(IRContext* context, uint32_t valueId) {
|
||||
ValueShape shape;
|
||||
Instruction* def = context->get_def_use_mgr()->GetDef(valueId);
|
||||
if (def == nullptr) return shape;
|
||||
const analysis::Type* type = context->get_type_mgr()->GetType(def->type_id());
|
||||
if (type == nullptr) return shape;
|
||||
const analysis::Vector* asVector = type->AsVector();
|
||||
const analysis::Type* component =
|
||||
asVector != nullptr ? asVector->element_type() : type;
|
||||
if (component == nullptr) return shape;
|
||||
if (component->AsInteger() == nullptr && component->AsFloat() == nullptr) {
|
||||
return shape;
|
||||
}
|
||||
shape.componentType = component;
|
||||
shape.componentCount = asVector != nullptr ? asVector->element_count() : 1u;
|
||||
return shape;
|
||||
}
|
||||
|
||||
// Which 1D sampled images this module is to be rewritten for, decided per
|
||||
// arrayed-ness because that is the granularity of the OpTypeImage declarations
|
||||
// glslang emits. A category is in scope only when the module actually performs a
|
||||
// lookup on it carrying an Offset, ConstOffset or Grad - the operands SPIRV-Cross
|
||||
// prints with the wrong arity - so a shader that only samples and fetches keeps
|
||||
// SPIRV-Cross's own correct emission untouched.
|
||||
struct LoweringScope {
|
||||
bool arrayed = false;
|
||||
bool nonArrayed = false;
|
||||
|
||||
bool Any() const { return arrayed || nonArrayed; }
|
||||
bool Covers(const Instruction* imageType) const {
|
||||
return (arrayed && Is1DSampledImageTypeOfArrayedness(imageType, true)) ||
|
||||
(nonArrayed && Is1DSampledImageTypeOfArrayedness(imageType, false));
|
||||
}
|
||||
};
|
||||
|
||||
LoweringScope ResolveLoweringScope(IRContext* context) {
|
||||
LoweringScope scope;
|
||||
// The type table settles the common case, and it is nearly every shader: no
|
||||
// 1D sampled image declared at all, so the code is never walked.
|
||||
bool declared = false;
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DSampledImageType(&type)) {
|
||||
declared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!declared) return scope;
|
||||
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
const OpClassification classification =
|
||||
ClassifyOpcode(instruction.opcode());
|
||||
if (classification.kind != OpKind::Texel ||
|
||||
instruction.NumInOperands() <= classification.coordinateOperand) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(context, instruction.GetSingleWordInOperand(0));
|
||||
if (!Is1DSampledImageType(imageType)) continue;
|
||||
const OperandPositions positions =
|
||||
LocateOperands(instruction, classification.imageOperandsIndex);
|
||||
if (!positions.Any()) continue;
|
||||
if (imageType->GetSingleWordInOperand(kArrayedOperand) == 1u) {
|
||||
scope.arrayed = true;
|
||||
} else {
|
||||
scope.nonArrayed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
// Everything this pass will touch, collected before a single word is changed.
|
||||
// Planning first is what lets every refusal be a clean "leave the module alone":
|
||||
// there is no point at which the module is half converted and the pass then
|
||||
// discovers it cannot finish.
|
||||
struct RewritePlan {
|
||||
struct Site {
|
||||
Instruction* instruction = nullptr;
|
||||
uint32_t coordinateOperand = 0;
|
||||
OperandPositions operands;
|
||||
};
|
||||
std::vector<Site> sites;
|
||||
bool declined = false;
|
||||
};
|
||||
|
||||
RewritePlan PlanRewrite(IRContext* context, const LoweringScope& scope) {
|
||||
RewritePlan plan;
|
||||
const auto covered = [&scope](const Instruction* type) {
|
||||
return scope.Covers(type);
|
||||
};
|
||||
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
const OpClassification classification =
|
||||
ClassifyOpcode(instruction.opcode());
|
||||
|
||||
if (classification.kind == OpKind::NotImageOp ||
|
||||
classification.kind == OpKind::DimIndependentQuery) {
|
||||
// These name no coordinate, so they need no rewrite - but an
|
||||
// opcode this pass has never classified must not reach one of
|
||||
// these images unnoticed. NotImageOp is the catch-all, so the
|
||||
// check is on it.
|
||||
if (classification.kind == OpKind::NotImageOp &&
|
||||
instruction.opcode() != spv::Op::OpLoad &&
|
||||
instruction.opcode() != spv::Op::OpStore &&
|
||||
instruction.opcode() != spv::Op::OpCopyObject &&
|
||||
instruction.opcode() != spv::Op::OpSampledImage &&
|
||||
instruction.opcode() != spv::Op::OpImage &&
|
||||
instruction.opcode() != spv::Op::OpAccessChain &&
|
||||
instruction.opcode() != spv::Op::OpInBoundsAccessChain &&
|
||||
instruction.opcode() != spv::Op::OpPhi &&
|
||||
instruction.opcode() != spv::Op::OpSelect &&
|
||||
instruction.opcode() != spv::Op::OpFunctionCall &&
|
||||
MentionsCoveredImage(context, instruction, covered)) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction.NumInOperands() < 1) continue;
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(context, instruction.GetSingleWordInOperand(0));
|
||||
if (!scope.Covers(imageType)) continue;
|
||||
|
||||
if (classification.kind == OpKind::Decline) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
if (instruction.NumInOperands() <= classification.coordinateOperand) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
const OperandPositions positions =
|
||||
LocateOperands(instruction, classification.imageOperandsIndex);
|
||||
if (positions.unsupported) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Confirm here, before anything is written, that every operand
|
||||
// about to be widened has the shape the widening assumes. The
|
||||
// coordinate may be a scalar or a short vector; the offset and
|
||||
// the two gradients must be SCALARS, which for a Dim1D image is
|
||||
// not an assumption but the validator's own rule
|
||||
// (GetPlaneCoordSize(1D) == 1). Checking it up front is what
|
||||
// keeps the apply phase total.
|
||||
const ValueShape coordinate = DescribeValue(
|
||||
context, instruction.GetSingleWordInOperand(
|
||||
classification.coordinateOperand));
|
||||
if (coordinate.componentCount == 0u || coordinate.componentCount > 3u) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
const uint32_t scalarOperands[] = {positions.gradX, positions.gradY,
|
||||
positions.offset,
|
||||
positions.constOffset};
|
||||
for (const uint32_t position : scalarOperands) {
|
||||
if (position == 0u) continue;
|
||||
if (!DescribeValue(context,
|
||||
instruction.GetSingleWordInOperand(position))
|
||||
.IsScalar()) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
// ConstOffset has to stay a constant expression, so its widened
|
||||
// form is built as a module-scope constant - which is only
|
||||
// possible if the operand really is one.
|
||||
if (positions.constOffset != 0u &&
|
||||
context->get_constant_mgr()->FindDeclaredConstant(
|
||||
instruction.GetSingleWordInOperand(positions.constOffset)) ==
|
||||
nullptr) {
|
||||
plan.declined = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.sites.push_back(
|
||||
{&instruction, classification.coordinateOperand, positions});
|
||||
}
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(
|
||||
const Vector<Uint32>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1,
|
||||
[](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
return ResolveLoweringScope(context.get()).Any();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status Lower1DSampledImagesPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
const LoweringScope scope = ResolveLoweringScope(irContext);
|
||||
if (!scope.Any()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
RewritePlan plan = PlanRewrite(irContext, scope);
|
||||
if (plan.declined) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// A zero of a given 32-bit scalar type. The literal word is the VALUE's bit
|
||||
// pattern, which for a float zero is 0 as well - so one helper serves the integer
|
||||
// coordinate of a fetch, the float coordinate of a sample and the float gradients
|
||||
// alike, without a second spelling to keep in step.
|
||||
const auto zeroOf = [&](const analysis::Type* componentType,
|
||||
uint32_t componentTypeId) -> uint32_t {
|
||||
const analysis::Constant* constant =
|
||||
constantMgr->GetConstant(componentType, {0u});
|
||||
if (constant == nullptr) return 0u;
|
||||
const Instruction* defining =
|
||||
constantMgr->GetDefiningInstruction(constant, componentTypeId);
|
||||
return defining != nullptr ? defining->result_id() : 0u;
|
||||
};
|
||||
|
||||
// The whole of the arity repair, in one place: insert a zero at component 1.
|
||||
// Scalar u becomes (u, 0); (u, layer) becomes (u, 0, layer); (u, q) becomes
|
||||
// (u, 0, q). See the header for why one rule covers every shape.
|
||||
const auto widen = [&](uint32_t valueId, Instruction* before,
|
||||
bool mustBeConstant) -> uint32_t {
|
||||
const ValueShape shape = DescribeValue(irContext, valueId);
|
||||
if (shape.componentCount == 0u) return 0u;
|
||||
|
||||
const uint32_t componentTypeId = typeMgr->GetTypeInstruction(shape.componentType);
|
||||
if (componentTypeId == 0u) return 0u;
|
||||
analysis::Vector widenedCandidate(shape.componentType, shape.componentCount + 1u);
|
||||
const uint32_t widenedTypeId = typeMgr->GetTypeInstruction(&widenedCandidate);
|
||||
const uint32_t zeroId = zeroOf(shape.componentType, componentTypeId);
|
||||
if (widenedTypeId == 0u || zeroId == 0u) return 0u;
|
||||
|
||||
// ConstOffset must remain a constant expression - the validator says so
|
||||
// outright ("Expected Image Operand ConstOffset to be a const object") - so
|
||||
// for it the widened value is built as a module-scope OpConstantComposite
|
||||
// rather than as an instruction in the block. Only the scalar shape is
|
||||
// reachable: the plan phase refuses anything else, because a Dim1D image's
|
||||
// offset has exactly one component by the validator's own arity rule.
|
||||
if (mustBeConstant) {
|
||||
if (!shape.IsScalar()) return 0u;
|
||||
const analysis::Type* widenedType = typeMgr->GetType(widenedTypeId);
|
||||
const analysis::Constant* widenedConstant =
|
||||
widenedType != nullptr
|
||||
? constantMgr->GetConstant(widenedType, {valueId, zeroId})
|
||||
: nullptr;
|
||||
if (widenedConstant == nullptr) return 0u;
|
||||
const Instruction* defining =
|
||||
constantMgr->GetDefiningInstruction(widenedConstant, widenedTypeId);
|
||||
return defining != nullptr ? defining->result_id() : 0u;
|
||||
}
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, before,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
std::vector<uint32_t> componentIds;
|
||||
componentIds.reserve(shape.componentCount + 1u);
|
||||
if (shape.IsScalar()) {
|
||||
componentIds.push_back(valueId);
|
||||
componentIds.push_back(zeroId);
|
||||
} else {
|
||||
for (uint32_t i = 0; i < shape.componentCount; ++i) {
|
||||
Instruction* extracted =
|
||||
builder.AddCompositeExtract(componentTypeId, valueId, {i});
|
||||
if (extracted == nullptr) return 0u;
|
||||
componentIds.push_back(extracted->result_id());
|
||||
if (i == 0u) componentIds.push_back(zeroId);
|
||||
}
|
||||
}
|
||||
Instruction* widened =
|
||||
builder.AddCompositeConstruct(widenedTypeId, componentIds);
|
||||
return widened != nullptr ? widened->result_id() : 0u;
|
||||
};
|
||||
|
||||
for (RewritePlan::Site& site : plan.sites) {
|
||||
Instruction* instruction = site.instruction;
|
||||
|
||||
struct Target {
|
||||
uint32_t position;
|
||||
bool mustBeConstant;
|
||||
};
|
||||
const Target targets[] = {
|
||||
{site.coordinateOperand, false},
|
||||
{site.operands.gradX, false},
|
||||
{site.operands.gradY, false},
|
||||
{site.operands.offset, false},
|
||||
{site.operands.constOffset, true},
|
||||
};
|
||||
for (const Target& target : targets) {
|
||||
// Position 0 is the image operand, so it is this plan's "absent" marker
|
||||
// for everything except the coordinate, which is never 0.
|
||||
if (target.position == 0u) continue;
|
||||
const uint32_t widenedId =
|
||||
widen(instruction->GetSingleWordInOperand(target.position), instruction,
|
||||
target.mustBeConstant);
|
||||
if (widenedId == 0u) {
|
||||
// Reachable only if the module's shapes disagree with what the plan
|
||||
// recorded. Failing here makes the caller keep the input binary,
|
||||
// which is the same outcome as a decline.
|
||||
return Status::Failure;
|
||||
}
|
||||
instruction->SetInOperand(target.position, {widenedId});
|
||||
}
|
||||
irContext->UpdateDefUse(instruction);
|
||||
}
|
||||
|
||||
// Only now, with no lookup still spelling a 1D coordinate, does the type become
|
||||
// the 2D one - which is what ES stores a GL_TEXTURE_1D(_ARRAY) as anyway
|
||||
// (MapToBackendTextureTarget), and what SPIRV-Cross was already PRINTING for it.
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (scope.Covers(&type)) {
|
||||
type.SetInOperand(kDimOperand, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
|
||||
// Sampled1D describes the types just rewritten. Drop it only if no 1D SAMPLED
|
||||
// image is left at all - a module may still hold one this pass left alone (a
|
||||
// category with no offset or gradient on it), and that one still needs the
|
||||
// capability. Image1D is deliberately untouched: it belongs to the storage images
|
||||
// Lower1DArrayImagesPass owns, and they may still be Dim1D here. Shader is
|
||||
// declared by any module reaching this point, so restating it keeps the
|
||||
// instruction valid and RemoveDuplicates collapses the pair.
|
||||
if (!AnyDim1DSampledTypeLeft(irContext)) {
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
const auto value =
|
||||
static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (value == spv::Capability::Sampled1D) {
|
||||
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken Lower1DSampledImagesPass::CreateLower1DSampledImagesPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<Lower1DSampledImagesPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,116 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// The SAMPLED-image half of the 1D story. Lower1DArrayImagesPass owns the storage
|
||||
// half and says there, correctly for what it needed, that SPIRV-Cross's SAMPLER path
|
||||
// "already handles the 1D-array shape correctly and must be left to it". That is true
|
||||
// of the COORDINATE and false of everything else the lookup carries.
|
||||
//
|
||||
// ES has no 1D texture, so SPIRV-Cross emits a 1D sampler as a 2D one - `case Dim1D:
|
||||
// res += options.es ? "2D" : "1D"` - and fakes the missing coordinate component at
|
||||
// each call site (spirv_glsl.cpp, the `imgtype.image.dim == Dim1D && options.es`
|
||||
// branches: `vec2(coord, 0.0)` non-arrayed, `vec3(coord.x, 0.0, coord.y)` arrayed,
|
||||
// which is the same (u, 0, layer) the 2D-array texture actually stores). But the
|
||||
// OFFSET operand and the two GRADIENT operands are printed straight through with
|
||||
// their original 1D arity:
|
||||
//
|
||||
// if (args.offset) { ...; farg_str += bitcast_expression(SPIRType::Int, args.offset); }
|
||||
// if (args.grad_x || args.grad_y) { ...; farg_str += to_expression(args.grad_x); ... }
|
||||
//
|
||||
// So a `textureLodOffset(sampler1DArray, vec2, float, int)` comes out as
|
||||
// `textureLodOffset(sampler2DArray, vec3, float, int)`, for which ESSL has no
|
||||
// overload, and the driver answers "'textureLodOffset' : no matching overloaded
|
||||
// function found". That loses the stage, and with it the program - which is how ONE
|
||||
// sampler1DArray lookup took down the nine-sampler compute shader of
|
||||
// KHR-GL43.compute_shader.resource-texture, whose dispatch then silently did nothing
|
||||
// and left the SSBO reading back the zeros the test uploaded.
|
||||
//
|
||||
// Observed failing on an Adreno 830 by isolating each form: textureOffset,
|
||||
// textureLodOffset and texelFetchOffset on both sampler1D and sampler1DArray, and
|
||||
// textureGrad on sampler1DArray. The same shaders with a 2D sampler compile, so the
|
||||
// functions exist - only the argument arity is wrong.
|
||||
//
|
||||
// WHY NOT PATCH SPIRV-CROSS. 3rdparty/SPIRV-Cross is a submodule pinned to KhronosGroup
|
||||
// upstream, not to a MobileGL fork (contrast 3rdparty/glslang), so an in-tree edit
|
||||
// would live outside this repository's history.
|
||||
//
|
||||
// WHY NOT WIDEN JUST THE OPERANDS. Emitting an ivec2 offset against a type still
|
||||
// declared Dim1D is an INVALID module, not a clever shortcut: the validator computes
|
||||
// the required arity from the image's own Dim (validate_image.cpp, GetPlaneCoordSize
|
||||
// -> "Expected Image Operand Offset to have 1 component") and would latch a failure on
|
||||
// every validating lane. So the type has to move too, and once it does the coordinate
|
||||
// has to move with it - which is what this pass does, in the module, before
|
||||
// SPIRV-Cross ever applies its own emulation.
|
||||
//
|
||||
// The rewrite is exactly SPIRV-Cross's own, restated on the SPIR-V side so that
|
||||
// coordinate, offset and gradient are all widened by one piece of code: a zero is
|
||||
// INSERTED AT COMPONENT 1 of each. That single rule is right for every shape, because
|
||||
// a 1D coordinate lays out as [u][array layer][proj q] and the plane occupies index 0
|
||||
// alone - so (u) -> (u, 0), (u, layer) -> (u, 0, layer) and (u, q) -> (u, 0, q) all
|
||||
// fall out of it, and so do the scalar offset -> ivec2 and the scalar gradients ->
|
||||
// vec2. The Dref value is a separate SPIR-V operand rather than a coordinate
|
||||
// component, so the shadow forms need nothing extra.
|
||||
//
|
||||
// NO CROSS-STAGE HAZARD, and this is the one place this pass is on firmer ground than
|
||||
// its storage-image sibling, whose header records the opposite as a known limitation.
|
||||
// That pass can rewrite uimage1DArray to uimage2DArray in one stage and decline in
|
||||
// another, and the two then spell the SAME uniform `uimage2D` and `uimage2DArray` and
|
||||
// the ES link fails on a type mismatch. Here the two spellings COINCIDE: SPIRV-Cross
|
||||
// prints Dim1D as "2D" on ES already, so a stage this pass rewrote and a stage it left
|
||||
// alone both declare `sampler2D` / `sampler2DArray`. Partial application across a
|
||||
// program's stages is therefore invisible at the interface.
|
||||
//
|
||||
// Deliberately narrow, on three axes - the sibling's reasoning, applied to this
|
||||
// resource:
|
||||
//
|
||||
// * SAMPLED images only (Sampled == 1). Storage images are the sibling's.
|
||||
// * Only when the module actually carries an Offset, ConstOffset or Grad operand on
|
||||
// a 1D sampled image, i.e. only where SPIRV-Cross's emission is ALREADY broken.
|
||||
// A shader that only calls texture()/textureLod()/texelFetch() on a sampler1D
|
||||
// keeps taking SPIRV-Cross's own (correct) output byte for byte, so this pass has
|
||||
// no way to regress it. The gate is decided per arrayed-ness, matching the two
|
||||
// distinct OpTypeImage declarations glslang emits.
|
||||
// * ESSL only. Vulkan has VK_IMAGE_VIEW_TYPE_1D natively and the offset and gradient
|
||||
// arities are the ones the module already spells, so DirectVulkan must see the
|
||||
// module unchanged.
|
||||
//
|
||||
// A size query on a covered image is DECLINED rather than half-translated, for the
|
||||
// sibling's reason: textureSize(sampler1D) yields an int and textureSize(sampler2D) an
|
||||
// ivec2, so rewriting the type while leaving the query would hand the shader a value of
|
||||
// the wrong shape. Refusing leaves the module byte for byte and is no worse than today.
|
||||
//
|
||||
// Every decline is decided BEFORE anything is rewritten - the pass plans the whole
|
||||
// edit, and only then applies it - so there is no state in which it has half-converted
|
||||
// a module and then given up. Anything it does not recognise reaching one of these
|
||||
// images (a gather, an unexpected image opcode) is a decline, not a guess.
|
||||
class Lower1DSampledImagesPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-lower-1d-sampled-images"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether a module carries the shape this pass exists for: a 1D SAMPLED image
|
||||
// reached by a lookup with an Offset, ConstOffset or Grad operand. One parse
|
||||
// answers it, and the answer is no for very nearly every shader - the common path
|
||||
// must not build an Optimizer at all.
|
||||
static bool BinaryHasOffsetOrGrad1DSampledImage(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLower1DSampledImagesPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Emulates the desktop-GL image formats GLSL ES cannot spell by CHANNEL WIDENING: a
|
||||
// storage image DECLARED `layout(rg32f)` is re-declared `layout(rgba32f)` and every
|
||||
// access through it is masked back to the two channels GL says it has.
|
||||
//
|
||||
// WHY IT IS NEEDED AT ALL. GL 4.2 has forty image formats; GLSL ES 3.1 has thirteen,
|
||||
// and GL_NV_image_formats - the only extension that adds the rest - is advertised by
|
||||
// none of Adreno 830, Mali-G1-Ultra MC12 or Mali-G925-Immortalis MC12 (probed on all
|
||||
// three, with `#extension ... : enable` also rejected, so "the driver implements it
|
||||
// unadvertised" is refuted rather than assumed). A shader that declares one of the
|
||||
// other twenty-six therefore has NO legal ESSL spelling, and it fails in one of two
|
||||
// ways: SPIRV-Cross throws for its is_desktop_only_format set and no text is produced
|
||||
// at all, or the token reaches the driver and is rejected ("'rg32f' : not a legal
|
||||
// layout qualifier id"). Either way the stage is lost, the backend program is
|
||||
// unusable, and every draw with it silently renders nothing while the frontend keeps
|
||||
// reporting GL_LINK_STATUS = TRUE. Dropping the qualifier instead is not an escape:
|
||||
// all three drivers reject a format-LESS image declaration outright ("all images have
|
||||
// to define layout format" / "S0001: Image must specify a format layout qualifier"),
|
||||
// readonly and writeonly alike, at both #version 310 es and 320 es. And unlike a
|
||||
// numeric limit there is nothing honest to report either - GL has no "this image
|
||||
// format is unsupported" query - so the format has to be emulated.
|
||||
//
|
||||
// WHAT WIDENING MEANS. Seventeen of the twenty-six have a core ESSL format of the
|
||||
// SAME PER-CHANNEL WIDTH AND COMPONENT TYPE, differing only in channel count
|
||||
// (rg32f -> rgba32f, r8ui -> rgba8ui, rg8_snorm -> rgba8_snorm, ...). Carried in one
|
||||
// of those the emulation is EXACT, not approximate: every value is representable bit
|
||||
// for bit, and GL's own image semantics do the rest -
|
||||
//
|
||||
// * imageLoad on a format with fewer than four channels returns (r, 0, 0, 1);
|
||||
// * imageStore drops the components the format does not have.
|
||||
//
|
||||
// so the two surplus channels of the carrier are not free storage, they are values GL
|
||||
// already defines. This pass pins them: every OpImageWrite through a widened image has
|
||||
// its texel replaced by (r[, g[, b]], 0.., 1) and every OpImageRead has its result
|
||||
// masked the same way. Masking BOTH is deliberate belt and braces - the write mask
|
||||
// alone keeps the storage canonical for a sampler and for glGetTexImage, the read mask
|
||||
// alone survives storage this shader never wrote (glTexStorage with no upload, whose
|
||||
// surplus channels are undefined).
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// glBindImageTexture has to be handed the carrier (on Adreno the bind of the narrow
|
||||
// format is GL_INVALID_VALUE for nineteen of the twenty-six, and on both Malis for
|
||||
// twenty-five). Both are done in DirectGLES against the same table below, so the two
|
||||
// sides agree by construction rather than by convention. Binding a narrow texture
|
||||
// through a wide image is NOT an option: every tested driver accepts it silently, so
|
||||
// it reads and writes out of bounds undetected.
|
||||
//
|
||||
// ESSL ONLY. DirectVulkan takes the declared format natively and resolves the view
|
||||
// format from the same bind state, so the module must reach it unchanged.
|
||||
class WidenImageFormatsPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
// `onlyFormatsSpirvCrossRefusesToPrint` narrows the pass to the formats that have
|
||||
// no ESSL route even on a driver that DOES advertise GL_NV_image_formats.
|
||||
// SPIRV-Cross's is_desktop_only_format set - r8ui, rg16f, r16i and fifteen others -
|
||||
// makes it THROW for an ESSL target rather than print a token, and the throw takes
|
||||
// the stage with it whatever the driver could have accepted. Mesa is exactly that
|
||||
// case: it advertises the extension, so nothing else needs widening there, and
|
||||
// `layout(r8ui) uimage2D` still lost its whole program until this ran for it.
|
||||
//
|
||||
// Off, the pass widens every format in the table, which is what a driver without
|
||||
// the extension needs. The caller sets it from
|
||||
// g_GLESCapabilities.SupportsExtendedImageFormats, and the SAME rule decides
|
||||
// whether the ES texture storage and the glBindImageTexture argument widen
|
||||
// (TextureImpl::GetImageBindableStorageWidening) - all three have to agree or the
|
||||
// shader addresses a texel size the storage does not have.
|
||||
explicit WidenImageFormatsPass(bool onlyFormatsSpirvCrossRefusesToPrint = false)
|
||||
: m_onlyFormatsSpirvCrossRefusesToPrint(onlyFormatsSpirvCrossRefusesToPrint) {}
|
||||
|
||||
const char* name() const override { return "mobilegl-widen-image-formats"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether the module declares a storage image whose format this pass would widen,
|
||||
// i.e. whether running it could change anything. Answered from a single parse so
|
||||
// the caller can skip the optimizer run entirely - which is every shader but a
|
||||
// handful. `onlyFormatsSpirvCrossRefusesToPrint` must match what the run will use,
|
||||
// or the gate answers a question the pass is not being asked.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
// The same question asked of a module the caller has ALREADY parsed, so a stage
|
||||
// that has to answer several gate questions pays one BuildModule rather than one
|
||||
// per gate - see ShaderCompiler::ProbeSpirvGateFeatures, and the ~10% it cost
|
||||
// compile-heavy CTS cases when two gates each parsed for themselves.
|
||||
static bool DeclaresWidenableImageFormat(spvtools::opt::IRContext* context,
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
|
||||
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
|
||||
// when the format needs no widening (it is core already) or cannot be widened
|
||||
// exactly (the nine above, and anything that is not an image format at all).
|
||||
// Used by DirectGLES for the texture storage and the glBindImageTexture argument,
|
||||
// so that all three layers pick the same carrier.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
|
||||
// Channels the GL internal format really has (1-4), or 0 when it is not one of the
|
||||
// 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);
|
||||
|
||||
private:
|
||||
bool m_onlyFormatsSpirvCrossRefusesToPrint = false;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -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;
|
||||
|
||||
@@ -26,7 +26,15 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
||||
// prints it into the emitted ESSL), and L1c was added.
|
||||
// 3: L2 gained the two interface-block rename maps (wave4's UniquifyIoBlockNames).
|
||||
constexpr Uint32 kKeyLayoutVersion = 3u;
|
||||
// 4: the glslang-capture migration. L1 DROPPED explicitOpaqueUniformBindings from its
|
||||
// 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.
|
||||
// 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
|
||||
@@ -119,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));
|
||||
@@ -128,7 +137,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
builder.NameMap(inputs.explicitVertexInLocations ? *inputs.explicitVertexInLocations : kEmpty);
|
||||
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
|
||||
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
|
||||
builder.NameMap(inputs.explicitOpaqueUniformBindings ? *inputs.explicitOpaqueUniformBindings : kEmpty);
|
||||
static const Vector<String> kNoXfb;
|
||||
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
|
||||
builder.Value(inputs.xfbBufferMode);
|
||||
@@ -146,7 +154,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
return MakeTranslationCacheKey(builder);
|
||||
}
|
||||
|
||||
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) { return verdict.infoLog.size(); }
|
||||
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) {
|
||||
SizeT bytes = verdict.infoLog.size();
|
||||
for (const auto& [name, location] : verdict.explicitUniformLocations) {
|
||||
bytes += name.size() + sizeof(Int);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Leaked for the same exit-order reason as the other two; see the note below.
|
||||
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
|
||||
@@ -160,8 +174,9 @@ 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);
|
||||
builder.Value(inputs.maxIntegerSamples);
|
||||
builder.Value(inputs.maxDepthTextureSamples);
|
||||
|
||||
@@ -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
|
||||
@@ -365,15 +369,27 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// covers backend identity and the advertised extension vector;
|
||||
// * per stage, in link order: the GL stage enum and the FULL preprocessed
|
||||
// source, which is literally the text ParseShaderSource was given;
|
||||
// * the four link-time request maps mapIO resolves against
|
||||
// * the three link-time request maps mapIO resolves against
|
||||
// (glBindAttribLocation / glBindFragDataLocation /
|
||||
// glBindFragDataLocationIndexed, and the merged layout(binding=) opaque
|
||||
// units) - these steer TMglGlslIoResolver and therefore the Locations and
|
||||
// Bindings baked into every module;
|
||||
// glBindFragDataLocationIndexed) - these steer TMglGlslIoResolver and
|
||||
// therefore the Locations and Bindings baked into every module. NOT the
|
||||
// merged layout(binding=) opaque units, which used to sit here: they are
|
||||
// an OUTPUT of mapIO (TMglGlslIoResolver writes that map and never reads
|
||||
// it), so they are a pure function of the stage sources already in this
|
||||
// key and keying on them discriminated nothing;
|
||||
// * 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
|
||||
@@ -393,9 +409,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
const UnorderedMap<String, Uint>* explicitVertexInLocations = nullptr;
|
||||
const UnorderedMap<String, Uint>* explicitFragmentOutLocations = nullptr;
|
||||
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
||||
const UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = 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
|
||||
@@ -476,6 +494,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// successful parse, so a successful compile's observable log is empty no matter what
|
||||
// glslang wrote into it. Stored rather than assumed so the two cannot drift.
|
||||
String infoLog;
|
||||
// The explicit default-block uniform locations the parse recovered
|
||||
// (CollectExplicitUniformLocations), empty when `parsed` is false.
|
||||
//
|
||||
// IN THE PAYLOAD BECAUSE A HIT SKIPS THE PARSE. These used to come from a lexical scan
|
||||
// of the source, which ran in the half a hit still executes; they now come from the
|
||||
// glslang snapshot, which a hit never produces. They belong to the same key as the
|
||||
// verdict itself - a pure function of (front-end env, stage, preprocessed source) - so
|
||||
// no key widening is needed, only this field. Without it an L1c hit would publish a
|
||||
// shader with no explicit locations at all and the program would first-fit them from 0.
|
||||
UnorderedMap<String, Int> explicitUniformLocations;
|
||||
};
|
||||
using ShaderParseVerdictPtr = SharedPtr<const ShaderParseVerdict>;
|
||||
|
||||
@@ -544,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;
|
||||
@@ -566,12 +595,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
//
|
||||
// Unconditional passes take no input but the module and so need no key material:
|
||||
// StripUboMemberRelaxedPrecision, LowerRectImages, Lower1DArrayImages,
|
||||
// LegalizeStorageBlockArrayIndexing and FlattenAtomicCounterBlockOffsets. Each self-gates
|
||||
// on the module's own content and is armed by nothing, so the SPIR-V already in this key
|
||||
// covers them completely.
|
||||
// Lower1DSampledImages, LegalizeResourceArrayIndexing and
|
||||
// FlattenAtomicCounterBlockOffsets. Each self-gates on the module's own content and is
|
||||
// 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
|
||||
@@ -600,8 +629,18 @@ 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
|
||||
// masks its accesses back - so a driver that HAS the extension and one that does not get
|
||||
// materially different ESSL from the same module.
|
||||
Bool supportsExtendedImageFormats = false;
|
||||
Int32 maxColorTextureSamples = 0;
|
||||
Int32 maxIntegerSamples = 0;
|
||||
Int32 maxDepthTextureSamples = 0;
|
||||
|
||||
@@ -69,7 +69,13 @@ namespace MobileGL {
|
||||
// Dual-source blend color index per fragment output (glBindFragDataLocationIndexed) ->
|
||||
// emitted as layout(index = N).
|
||||
UnorderedMap<String, Uint> explicitFragmentOutIndices;
|
||||
// ---- OUT parameters, written by TMglGlslIoResolver during mapIO ----
|
||||
// Neither is an input: the resolver only ever writes them. They exist because
|
||||
// the IO mapper's collect callback is the last point at which a resource's
|
||||
// qualifier still says what the SHADER declared rather than what glslang
|
||||
// assigned - see the comment on TMglGlslIoResolver::reserverResourceSlot.
|
||||
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr;
|
||||
};
|
||||
|
||||
struct ProgramBinaryAttrib {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
#include "TMglGlslIoResolver.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
namespace MobileGL {
|
||||
bool TMglGlslIoResolver::ShouldAssignPlainUniformLocation(const glslang::TType& type) const {
|
||||
if (!doAutoLocationMapping()) {
|
||||
@@ -149,12 +153,47 @@ namespace MobileGL {
|
||||
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
||||
}
|
||||
|
||||
// THE COLLECT CALLBACK IS THE CAPTURE POINT, and the reason is a matter of ten lines of
|
||||
// glslang. mapIO gathers every declared symbol of every stage and calls this on each of
|
||||
// them (iomapper.cpp addStage -> TSlotCollector) BEFORE it resolves anything; only
|
||||
// afterwards, in doMap(), does it write the slots it chose back into the types
|
||||
// (iomapper.cpp:240, `layoutBinding = at->second.newBinding`). Up to here
|
||||
// `qualifier.hasBinding()` still answers "did the SHADER say so?"; past it, every resource
|
||||
// carries a number and the question can no longer be asked at all.
|
||||
//
|
||||
// Both captures below used to be lexical scans of the shader source, which had to run
|
||||
// before the preprocessor's macros were expanded and therefore could not read
|
||||
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
|
||||
// its storage blocks. Asking the AST instead makes the macro case ordinary.
|
||||
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||
const glslang::TType& type = ent.symbol->getType();
|
||||
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||
// getAccessName() is the BLOCK TYPE name for a block and the declared name for
|
||||
// everything else (IntermTraverse.cpp TIntermSymbol::getAccessName) - which is exactly
|
||||
// the key both consumers want.
|
||||
const glslang::TString& name = ent.symbol->getAccessName();
|
||||
|
||||
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
||||
type.getQualifier().hasBinding()) {
|
||||
const glslang::TString& name = ent.symbol->getAccessName();
|
||||
(*m_explicitOpaqueUniformBindings)[name.c_str()] = type.getQualifier().layoutBinding;
|
||||
qualifier.hasBinding()) {
|
||||
(*m_explicitOpaqueUniformBindings)[name.c_str()] = qualifier.layoutBinding;
|
||||
}
|
||||
|
||||
// A storage block that declared no binding. UNION across stages by construction - one
|
||||
// resolver serves the whole program - which is what GLSL's "every stage must declare
|
||||
// the same block identically" rule makes correct.
|
||||
//
|
||||
// NOT the atomic-counter blocks glslang SYNTHESIZES, which are storage blocks by every
|
||||
// structural test available here and are still not what this set means. Relaxed parsing
|
||||
// folds each atomic_uint into a "gl_AtomicCounterBlock_<GL binding>" block
|
||||
// (ParseContextBase::growAtomicCounterBlock) and leaves it unbound because MobileGL asks
|
||||
// for auto-mapped bindings - so it arrives looking exactly like an unqualified
|
||||
// application block. Seeding one to GL binding 0 would overwrite the counter buffer's
|
||||
// real binding, which is the trailing number in that very name.
|
||||
if (m_storageBlocksWithoutBinding != nullptr && type.getBasicType() == glslang::EbtBlock &&
|
||||
qualifier.storage == glslang::EvqBuffer && !qualifier.hasBinding() &&
|
||||
name.compare(0, std::strlen(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX),
|
||||
MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX) != 0) {
|
||||
m_storageBlocksWithoutBinding->insert(name.c_str());
|
||||
}
|
||||
|
||||
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <glslang/Public/ShaderLang.h>
|
||||
@@ -27,14 +28,17 @@ namespace MobileGL {
|
||||
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
|
||||
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
|
||||
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
|
||||
ExplicitVarSlotMap* opaqueUniformBindings)
|
||||
ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr)
|
||||
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
|
||||
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings) {}
|
||||
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
|
||||
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding) {}
|
||||
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
|
||||
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
|
||||
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings)
|
||||
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr)
|
||||
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
|
||||
opaqueUniformBindings) {}
|
||||
opaqueUniformBindings, storageBlocksWithoutBinding) {}
|
||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||
@@ -47,7 +51,17 @@ namespace MobileGL {
|
||||
const ExplicitVarSlotMap& m_explicitVertexIns;
|
||||
const ExplicitVarSlotMap& m_explicitFragOuts;
|
||||
const ExplicitVarSlotMap& m_explicitFragOutIndices;
|
||||
// Two OUT channels, both filled from reserverResourceSlot and never read back by this
|
||||
// resolver. They exist because the collect callback is the LAST place the shader's own
|
||||
// declaration is still legible: ten lines later (iomapper.cpp:240) mapIO writes its
|
||||
// auto-assigned binding into the very qualifier that says whether the shader declared
|
||||
// one. Anything downstream that needs "as DECLARED" rather than "as ASSIGNED" has to be
|
||||
// handed it from here.
|
||||
ExplicitVarSlotMap* m_explicitOpaqueUniformBindings = nullptr;
|
||||
// Block TYPE names of the shader storage blocks that reached mapIO carrying NO
|
||||
// layout(binding = N). GL 4.3 core 7.8 gives such a block binding ZERO; see
|
||||
// ProgramLinkTask::SeedDefaultStorageBlockBindings for what is done with them.
|
||||
std::set<String>* m_storageBlocksWithoutBinding = nullptr;
|
||||
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||
bool m_plainUniformLocationsAssigned = false;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -368,6 +368,8 @@ public:
|
||||
TIntermTyped* vkRelaxedRemapFunctionCall(const TSourceLoc&, TFunction*, TIntermNode*);
|
||||
// returns true if the variable was remapped to something else
|
||||
void recordUniformInitializer(const TString&, const TType&, const TConstUnionArray&);
|
||||
void recordUniformLocation(const TString&, const TType&);
|
||||
void atomicCounterOffsetCheck(const TSourceLoc&, const TString&, const TType&);
|
||||
bool vkRelaxedRemapUniformVariable(const TSourceLoc&, TString&, const TPublicType&, TArraySizes*, TIntermTyped*, TType&);
|
||||
void vkRelaxedRemapUniformMembers(const TSourceLoc&, const TPublicType&, const TType&, const TString&);
|
||||
void vkRelaxedRemapFunctionParameter(TFunction*, TParameter&, std::vector<int>* newParams = nullptr);
|
||||
|
||||
@@ -637,6 +637,29 @@ public:
|
||||
void addUniformInitializer(TUniformInitializer&& init) { uniformInitializers.push_back(std::move(init)); }
|
||||
const std::vector<TUniformInitializer>& getUniformInitializers() const { return uniformInitializers; }
|
||||
|
||||
// A default-block uniform's explicit layout(location = N), recorded where Vulkan-relaxed
|
||||
// rules DROP it.
|
||||
//
|
||||
// Desktop GLSL 4.3 / ARB_explicit_uniform_location lets a default-block uniform name the
|
||||
// number glGetUniformLocation will answer for it. Vulkan-relaxed parsing sweeps such
|
||||
// uniforms into a uniform BLOCK, where a location qualifier means nothing, so the
|
||||
// qualifier is dropped with a warning - and once it is gone no later stage can tell the
|
||||
// uniform ever carried one: mapIO sees layoutLocationEnd and reflection reports whatever
|
||||
// the client's own assigner chose. The CLIENT is the only party that can still honor it,
|
||||
// so the declared number is handed out here instead of discarded.
|
||||
//
|
||||
// arraySizes is the declared array shape, outer dimension first, and empty when the
|
||||
// uniform is not an array. A client that keys these by REFLECTION name has to spell the
|
||||
// same elements glslang's reflection will ("u[1][0]" for a float u[2][3]), and only the
|
||||
// declaration knows the shape; a dimension glslang could not size appears as 0.
|
||||
struct TUniformLocation {
|
||||
std::string name;
|
||||
int location = -1;
|
||||
std::vector<int> arraySizes;
|
||||
};
|
||||
void addUniformLocation(TUniformLocation&& location) { uniformLocations.push_back(std::move(location)); }
|
||||
const std::vector<TUniformLocation>& getUniformLocations() const { return uniformLocations; }
|
||||
|
||||
void setAtomicCounterBlockName(const char* name) { atomicCounterBlockName = std::string(name); }
|
||||
const char* getAtomicCounterBlockName() const { return atomicCounterBlockName.c_str(); }
|
||||
void setAtomicCounterBlockSet(unsigned int set) { atomicCounterBlockSet = set; }
|
||||
@@ -1250,6 +1273,7 @@ protected:
|
||||
std::string globalUniformBlockName;
|
||||
std::string atomicCounterBlockName;
|
||||
std::vector<TUniformInitializer> uniformInitializers;
|
||||
std::vector<TUniformLocation> uniformLocations;
|
||||
unsigned int globalUniformBlockSet;
|
||||
unsigned int globalUniformBlockBinding;
|
||||
unsigned int atomicCounterBlockSet;
|
||||
|
||||
Reference in New Issue
Block a user