Compare commits

..
Author SHA1 Message Date
Claude 37b20f2fca [Fix] (DirectGLES): repair the emulation-guard mask restore and scissor the resolve fallback's staging blit correctly
Final audit round over the DirectGLES scratch/shadow mechanisms; three verified
defects fixed:

- ~ScopedEmulationDrawState restored the APPLICATION's per-buffer colour masks,
  not what SyncRenderState actually pushed: a widened attachment's alpha-off
  doctoring (g_syncedColorMaskAlphaWidenMask) was dropped while the memo still
  claimed it applied, so the next sync early-outed and draws wrote fragment
  alpha into the widened buffer - breaking the stored-alpha==1.0 invariant the
  widen discipline exists to protect. The restore now re-applies the doctoring.
- The same restore loop gated on the core glColorMaski name only, while the sync
  push falls back to glColorMaskiEXT/OES: EXT/OES-only devices were left holding
  buffer 0's mask broadcast across every draw buffer with the shadow recording
  the divergent set (never repaired). The restore now uses the same three-way
  pointer fallback.
- ResolveThenBlit ran its resolve-into-scratch staging blit under the
  application's scissor: a box not covering the scratch-origin rect clipped the
  resolve silently (no GL error), and the second blit then copied stale scratch
  renderbuffer texels into the destination. The staging blit now runs scissor-off
  (shadow-tracked, like ScopedScissorDisable); the caller-visible blit keeps its
  native scissor semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 17:05:05 +00:00
Claude ee908aab46 [Fix] (DirectVulkan, GLImpl, MG_State, ShaderTranspiler): second audit round over the remaining memo sites
Six more verified defects from the residual memo/cache mechanisms:

- Program resource cache (DirectVulkan reflection): glShaderStorageBlockBinding
  deliberately does not bump the backend state version, and the SSO pipeline
  composite is unnamed so the by-name in-place patch can never reach its slot -
  the composite kept serving pre-rebind SSBO bindings. The cache now keys on the
  program's block-binding version; a binding-only change re-applies the overrides
  by name instead of re-running spirv-reflect. SetShaderStorageBlockBinding also
  gains the equality bail-out its uniform-block sibling has, so the composite
  mirror's replay stops churning the version every draw.
- LinkProgram's allowVSOnlyPrograms function-static latch never set its own
  initialized flag (dead memo, re-read every call) - and completing it would have
  frozen a per-backend capability across re-initialization. Replaced with a fresh
  per-link read from the null-checked active backend.
- Query object registry: drained at full library teardown (DestroyAllQueryObjects,
  mirroring DestroyAllSyncObjects) - undeleted queries and their backend wrappers
  leaked across Destroy/Initialize cycles, stale ids stayed IsQuery == GL_TRUE in
  the re-initialized library, and a later delete could hand the old backend's
  wrapper to a different backend's DeleteBackendQuery.
- Converted vertex streams and the host-side EBO max-index scan now SyncGpuWrites
  before reading the coherent mapping: XFB/SSBO/image writes are merely recorded
  at that point, so the conversion read pre-write bytes (the restart-index
  rewrite already synced; these two host reads did not).
- Zero-stride converted bindings: both converters rejected stride 0, making the
  factory's documented single-element conversion unreachable and silently
  dropping every draw using such a binding; the stride is substituted with the
  element size for the one-element case.
- DemoteFloat64Pass block relayout: measurement queued into the module eagerly,
  so a mid-struct failure left a half-relaid-out block (compacted offsets before
  the failing member, 64-bit offsets after) while claiming the block was left
  alone. Decoration writes are now collected and committed only when the whole
  block measures successfully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 16:48:15 +00:00
Claude a4ccf3a837 [Fix] (DirectGLES, DirectVulkan, MG_State): close stale-cache, A-B-A and state-leak holes across the memo layers
Audit of every memoization implementation; sixteen verified defects fixed:

DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
  sibling shadows; its identity+version key is only monotonic within one GLContext,
  so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
  generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
  FBOs re-attach instead of keeping the deleted texture name; the attachment walk
  re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
  generation the VAO twin sync compares, forcing a full re-emit of the baked
  glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
  slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).

DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
  shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
  the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
  as PipelineFactory): a transient driver failure was permanently disabling every
  dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
  filter/aniso/LOD setters bump only that counter, so the old key served a stale
  verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
  generation (the snapshot bakes the LOD verdict into its pipeline), and
  recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
  cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
  state-pointer memos outlive the factory across renderer recreation, and a fresh
  factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
  clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
  creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
  fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
  version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
  ring on both failure paths instead of a synchronous destructor under the GPU.

MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
  a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
  glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
  snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
  attach list and current compile nodes: post-link glAttachShader/glCompileShader
  must not leak into the composite while the (lifetimeId, linkVersion) signature
  still hits - GL's "as last linked" rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 16:35:39 +00:00
91 changed files with 569 additions and 8095 deletions
-3
View File
@@ -420,9 +420,6 @@ jobs:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
-9
View File
@@ -265,9 +265,6 @@ jobs:
# crash stack without burning a CI round on an in-workflow debugger.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
@@ -642,12 +639,6 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
-1
View File
@@ -27,4 +27,3 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
+1 -11
View File
@@ -279,20 +279,17 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
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/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
@@ -465,7 +462,7 @@ set(MOBILEGL_INCLUDE_DIR
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
@@ -677,10 +674,3 @@ if (NOT ANDROID)
add_subdirectory(tools/trace_replay)
endif()
endif()
# The integration binary is also useful as a standalone adb-shell executable.
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
# links libMobileGL.so and creates an AImageReader-backed window instead.
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
-4
View File
@@ -100,10 +100,6 @@ namespace MobileGL::MG_Config {
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool IterationRPFixBarrier = false;
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
-1
View File
@@ -171,7 +171,6 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.FixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
+2 -40
View File
@@ -14,7 +14,6 @@ namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
@@ -25,19 +24,6 @@ namespace MobileGL {
};
namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
// one of the two pointers is set; neither is set when the name named nothing, which is
// the INVALID_VALUE the frontend validator reports.
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
};
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
@@ -174,9 +160,9 @@ namespace MobileGL {
GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void (*CopyImageSubData)(const CopyImageEndpoint& src,
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
@@ -250,14 +236,6 @@ namespace MobileGL {
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
// frontend's own accounting wherever that accounting is exact - a capture with no
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
// the written count for a vertex-only capture that follows a large render pass,
// where the desktop-exact answer is the one the frontend already computed. Defaults
// to false, so a backend that never sets it keeps using its GPU result.
Bool PrefersCpuXfbPrimitiveAccounting = false;
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
@@ -340,22 +318,6 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
@@ -8,7 +8,6 @@
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
@@ -407,12 +406,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete;
}
// `samples` only reaches the multisample targets; every other target ignores it. The
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
// repeating the gen/bind/completeness/delete dance.
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
Bool* outRenderable, Int samples = 1) {
Bool* outRenderable) {
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
return false;
}
@@ -432,11 +428,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
if (isMultisample) {
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE);
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
} else {
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
gl.glDeleteTextures(1, &texture);
@@ -532,29 +527,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return sampleCounts;
}
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
// for every format on the multisample targets even where glTexImage2DMultisample happily
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
// two answers cannot both be right. Completeness is required at every count, exactly as
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
// probe has already succeeded, so 1 terminates the list without being re-probed.
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> sampleCounts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
Bool renderable = false;
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
&renderable, samples);
if (created && renderable) {
sampleCounts.push_back(samples);
}
}
sampleCounts.push_back(1);
return sampleCounts;
}
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) {
@@ -655,11 +627,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
if (IsGLESProbeMultisampleTarget(target)) {
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, maxSamples);
cache.SampleCounts[targetIndex][formatIndex] = {1};
}
}
shouldProbeFallback = !nativeCreated || !nativeRenderable;
@@ -677,11 +645,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
}
if (IsGLESProbeMultisampleTarget(target)) {
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, maxSamples);
cache.SampleCounts[targetIndex][formatIndex] = {1};
}
}
}
@@ -783,29 +747,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
}
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples) {
if (samples <= 1) {
return samples;
}
Int maxSamples = 0;
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts =
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
return std::min(samples, std::max(maxSamples, 1));
}
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
DestroyEGLContext();
}
@@ -1166,12 +1107,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
// own count is the desktop-exact one and the ES driver's is only as good as the
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
// follows a large render pass). The query above stays installed: it is still what
// answers an amplifying span, and PRIMITIVES_GENERATED always.
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
@@ -1251,31 +1186,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
// graphics stage except fragment) and is the only answer that lets an application take
// its own fallback instead of building a program the driver will refuse to link. The
// stage limit cannot exceed the combined limit or the number of binding points there
// are to bind buffers to, so clamp to both.
const auto clampStageStorageBlocks = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
};
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxVertexShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
@@ -18,16 +18,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
// allocation call has to come through here. The shadow state keeps the requested count, so
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples);
class BackendObject_DirectGLES : public BackendObject {
public:
~BackendObject_DirectGLES() override;
+73 -327
View File
@@ -300,32 +300,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
Clear();
}
#else
// Error HYGIENE is not a debugging feature: every site that brackets a risky ES call with
// Clear()/Loop() relied on these to empty the driver's queue, and compiling them to
// nothing left whatever the driver raised sitting there for an unrelated later
// `glGetError() == GL_NO_ERROR` probe to read as its own failure. The callback stays
// unused because MGLOG_D is compiled out at this level, but the queue still gets drained.
// Bounded like DrainESErrors: a driver that never returns GL_NO_ERROR (a lost context is
// the usual way) must not spin here.
constexpr Int kMaxDrainedESErrors = 32;
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {
static_cast<void>(func);
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
void ErrorLopper::Clear() {
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
ErrorLopper::ErrorLopper() {
Clear();
}
ErrorLopper::~ErrorLopper() {
Clear();
}
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {}
void ErrorLopper::Clear() {}
ErrorLopper::ErrorLopper() = default;
ErrorLopper::~ErrorLopper() = default;
#endif
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
@@ -1341,16 +1319,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
// the state target instead answered "no" for it and pinned every 1D-array image binding
// to layer 0, whatever the application passed.
//
// `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for
// word) makes them one rule: "If the texture identified by texture does not have
// multiple layers or faces, the entire texture level is bound, regardless of the values
// of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so
// the driver must not be handed a layer index the texture has no room for. Adreno takes
// such a request literally and leaves the image unit reading zero, which is what failed
// KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the
// GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow
// is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed.
static Bool SupportsLayeredImageBinding(TextureTarget target) {
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
@@ -1406,11 +1374,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
const GLint layer = layerable ? imageBinding.Layer : 0;
const GLboolean layered =
SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()) ? imageBinding.Layered : GL_FALSE;
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
layered, layer, imageBinding.Access, imageBinding.Format);
layered, imageBinding.Layer, imageBinding.Access, imageBinding.Format);
}
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
@@ -3878,28 +3845,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
}
// Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a
// lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would
// otherwise spin an unbounded drain forever inside whichever GL entry point happened to be
// cleaning up, which is how a GPU reset reads as an unkillable process whose log simply
// stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the
// diagnostic. Every drain in this backend goes through here so the bound cannot drift apart
// between them.
static constexpr Int kMaxDrainedGLErrors = 32;
static void DrainDriverErrors(const char* site) {
Int drained = 0;
while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
++drained;
}
if (drained == kMaxDrainedGLErrors) {
MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost",
site, kMaxDrainedGLErrors);
static void DrainBlitErrors() {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
}
}
static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); }
// Sized internal format of the currently bound READ framebuffer's read colour
// attachment, 0 when it cannot be determined.
static GLenum QueryReadColorAttachmentInternalFormat() {
@@ -4674,46 +4624,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return;
}
// The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH
// aspect groups still owe their copy, and each has to be retried on its own. Re-issuing
// the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it
// silently whenever the colour half could not be emulated - and on a framebuffer whose
// only attachment is depth it never can, because the colour emulation has no attachment
// to take a format from (KHR-GL33.framebuffer_blit's depth config test blits
// COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value).
const GLbitfield colourBit = mask & static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
const GLbitfield dsBits = mask & static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// The colour group's one emulation is the multisample resolve that also converts format,
// which is the shape this names. It used to double as an early-out for the whole
// function, which is what cost a depth-only mask its single-aspect retry.
const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0;
if (colourBit != 0) {
DrainBlitErrors();
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
const Bool emulated =
multisampleResolve &&
ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter);
if (!emulated) {
MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its "
"own and no emulation applies");
}
}
if (readSamples <= 0 || drawSamples > 0 || (mask & GL_COLOR_BUFFER_BIT) == 0) {
return;
}
if (dsBits != 0) {
if (ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter) &&
(mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT)) != 0) {
DrainBlitErrors();
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT), filter);
DrainBlitErrors();
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
// Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate
// pass runs in the opposite direction, so a driver that declines a multisample
// depth/stencil resolve leaves the destination holding its clear value. The log
// is the whole diagnostic - the frontend performs no validation of its own, so
// this never reaches the application as a GL error.
MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on "
"its own and no emulation applies");
}
}
DrainBlitErrors();
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
@@ -5070,7 +4990,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); }
static void ClearGLErrors() {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
}
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
// scope (GenerateMipmap must respecify texture storage while no incomplete
@@ -5708,87 +5630,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target.
//
// GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and
// GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while
// ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand
// the driver GL_UNKNOWN_MGL.
struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D;
// Exactly one of the two is set. The backend object is kept rather than its id, because
// the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture),
// so it is read at the point of use.
SharedPtr<TextureImpl::BackendTextureObject> texture;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
GLint x = 0;
GLint y = 0;
GLint z = 0;
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
GLuint Name() const {
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
return texture ? texture->GetBackendTextureId() : 0u;
}
};
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
// reachable from a path that has a renderbuffer but no framebuffer.
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *slot;
} else {
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newSlot) {
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newSlot;
}
backendRenderbufferObject->SyncToBackend(renderbufferObject);
return backendRenderbufferObject;
}
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
GLint z, GLESCopyImageEndpoint& out) {
if (endpoint.IsRenderbuffer()) {
out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer);
if (!out.renderbuffer) return false;
out.target = GL_RENDERBUFFER;
out.x = x;
out.y = y;
out.z = z;
return true;
}
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
// slot inside the backend texture registry, and the second call mutates that very map:
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
// robin-hood displacement well under the load factor), and Find drops any
// entry whose state object has expired - which, with the map open-addressed and erasing
// by shifting the probe cluster backwards, relocates entries other than the erased one.
// Either way a reference taken by the first call is stale by the time the second returns,
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
// a path that is already doing a texture copy.
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
// reaches here - but the assertion that says so is compiled out of a release build, and
// SyncTextureObjectToBackend would register a null state object.
if (!endpoint.Texture) return false;
out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture);
if (!out.texture) return false;
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
GLESCopyImageEndpoint endpoint{};
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
if (stateTarget == TextureTarget::Texture1DArray) {
out.x = x;
out.y = 0;
out.z = y;
return true;
endpoint.x = x;
endpoint.y = 0;
endpoint.z = y;
return endpoint;
}
out.x = x;
out.y = y;
out.z = z;
return true;
endpoint.x = x;
endpoint.y = y;
endpoint.z = z;
return endpoint;
}
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
@@ -5804,172 +5666,85 @@ namespace MobileGL::MG_Backend::DirectGLES {
std::swap(height, depth);
}
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown;
}
// Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one
// upload target (so not a cube map, whose six chains the z axis selects between) and layers on
// the z axis (GL_TEXTURE_1D_ARRAY carries them on y).
static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (!texture) return false;
if (texture->GetTarget() == TextureTarget::Texture1DArray) return false;
return texture->GetUploadTargets().size() == 1;
}
// glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU
// shadow has to stay authoritative - a packed format with redundant encodings, where a GPU
// readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) -
// the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already
// put these texels on the GPU, and flagging the level would only schedule a redundant upload
// back over them.
//
// Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot
// address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level
// whose shadow is missing or not a plain texel grid, a region outside either level, or a
// self-copy within one level, where the row copies could overlap.
static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX,
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei width, GLsizei height, GLsizei depth) {
if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return;
if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return;
if (width <= 0 || height <= 0 || depth <= 0) return;
if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) {
return;
}
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
if (!srcMipmap || !dstMipmap) return;
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(dstLevel));
// A shadow that is not exactly texels x texelSize bytes is one this cannot index (a
// compressed blob, or a level whose allocation disagrees with its recorded extent).
const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels;
if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 ||
dstBytes != dstTexels * texelBytes) {
return;
}
if (static_cast<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
return;
}
const auto* srcBase = static_cast<const Uint8*>(
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
if (!srcBase || !dstBase) return;
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
for (GLsizei slice = 0; slice < depth; ++slice) {
for (GLsizei row = 0; row < height; ++row) {
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
static_cast<SizeT>(srcY + row)) *
static_cast<SizeT>(srcSize.x()) +
static_cast<SizeT>(srcX)) *
texelBytes;
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
static_cast<SizeT>(dstY + row)) *
static_cast<SizeT>(dstSize.x()) +
static_cast<SizeT>(dstX)) *
texelBytes;
Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes);
}
}
MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height,
depth);
}
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dstEndpoint,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
GLESCopyImageEndpoint src{};
GLESCopyImageEndpoint dst{};
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
// slot inside the backend texture registry, and the second call mutates that very map:
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
// robin-hood displacement well under the load factor), and Find drops any
// entry whose state object has expired - which, with the map open-addressed and erasing
// by shifting the probe cluster backwards, relocates entries other than the erased one.
// Either way a reference taken by the first call is stale by the time the second returns,
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
// a path that is already doing a texture copy.
const SharedPtr<TextureImpl::BackendTextureObject> srcBackendTexture =
TextureImpl::SyncTextureObjectToBackend(srcTexture);
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
TextureImpl::SyncTextureObjectToBackend(dstTexture);
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
// supposed to catch it expands to nothing. The four Name() calls below are the same
// dereference. The frontend validator is what keeps this unreachable and what reports
// the error the application is owed; declining is only how a future gap up there stops
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
// are the same dereference. The frontend validator is what keeps this unreachable and
// what reports the error the application is owed; declining is only how a future gap up
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!srcBackendTexture || !dstBackendTexture) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
return;
}
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
GLsizei copyHeight = srcHeight;
GLsizei copyDepth = srcDepth;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
// turns it down.
const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer();
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat);
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat);
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat);
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat);
if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) {
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat());
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat());
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return;
}
if (!anyRenderbuffer &&
(srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
if (srcTexture->GetFormat() == TextureInternalFormat::R32F ||
dstTexture->GetFormat() == TextureInternalFormat::R32F) {
// The single glGetError below decides the fallback dispatch, and
// ErrorLopper::Clear is compiled out at the default log level - drain
// with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback.
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) {
return;
}
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dst.Name(), dst.target, dstLevel, dst.x, dst.y);
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
return;
}
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend
// validator, so a driver error here is an internal invariant violation, not something
@@ -5985,14 +5760,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
return;
}
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
// CPU shadow is what glGetTexImage answers from, so it has to follow the same move -
// otherwise it hands back whatever the level held before this copy.
if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) {
MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
}
@@ -7283,7 +7050,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
data = std::move(expanded);
}
static void DrainESErrors() { DrainDriverErrors("ReadPixels"); }
static void DrainESErrors() {
for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
static GLenum QueryReadAttachmentComponentType() {
GLint framebufferId = 0;
@@ -7916,30 +7686,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
const GLsizei sliceCount = std::max(size.z(), 1);
const Bool multiSlice = size.z() > 1;
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
// is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes,
// which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same
// value 8064, different words), and the conformance suite compares the words
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
// shadow branch further down was unreachable. Serve the verbatim-word pairs from the
// shadow first and keep the GPU attempts as the fallback for a level the shadow never
// received. Every other format still prefers the GPU, so a rendered-into texture is
// unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative -
// and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the
// shadow for exactly this reason.
const Bool verbatimPackedShadowRead =
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (verbatimPackedShadowRead &&
GetTexImageViaShadowConversion(textureMipmapObject,
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)");
return;
}
// A multi-slice read used to go to the CPU shadow outright, on the grounds that the
// scratch FBO can only expose one layer at a time. But the shadow only holds what was
// uploaded, so every slice that was rendered to came back stale - which is exactly what
+2 -2
View File
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
+23 -256
View File
@@ -9,7 +9,6 @@
#include "Managers.h"
#include "Utils.h"
#include "DirectGLES.h"
#include "BackendObject_DirectGLES.h"
#include <Config.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
@@ -135,18 +134,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
[] { std::atexit(+[] { g_processTeardown = true; }); });
}
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks) {
// One block is all the indirect-params view needs, so this is a >= 1 test and not a
// budget calculation. Negative is treated as unusable rather than clamped: a driver
// that leaves the out-param untouched is telling us nothing, and guessing "yes" here
// is what produces an unlinkable program.
return maxVertexShaderStorageBlocks >= 1;
}
static Bool CanUseVertexStageStorageBlock() {
return VertexStageStorageBlockUsable(g_GLESCapabilities.MaxVertexShaderStorageBlocks);
}
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
return source;
@@ -219,48 +206,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
: 0;
// The whole indirect half of this machinery is a storage block read from the VERTEX
// stage, and a storage block in the vertex stage is optional in both APIs: the
// minimum for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2
// table 21.44) and ARM's GLES driver takes that allowance - a Mali-G925 reports 0.
// Emitting the block anyway does not make it work; it makes the program UNLINKABLE
// ("The number of vertex shader storage blocks (1) is greater than the maximum
// number allowed (0)"), and because the frontend's LINK_STATUS is glslang's and not
// the driver's, the application never learns: every draw with that program silently
// renders nothing. Dropping just the indirect half costs strictly less.
const Bool canReadIndirectParamsFromVertexStage = CanUseVertexStageStorageBlock();
String machinery;
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
}
if (!canReadIndirectParamsFromVertexStage) {
// Degraded, but contained and loud. gl_BaseInstance collapses to the plain
// mg_BaseInstance uniform, which the non-indirect draw entry points do set
// correctly - so ordinary instanced draws are unaffected. What is lost is the
// per-command baseInstance of an INDIRECT draw, which lives in the (possibly
// GPU-written) command buffer and can only be read through this block: those
// draws now see the last uniform value rather than their own command's. No
// alternative path is attempted, deliberately - there is nowhere else in the
// vertex stage to read a GPU-written buffer from.
//
// MGLOG_E_ONCE, not _D: this silently changes rendering for exactly the
// workloads (Create/Flywheel indirect instancing) whose bug reports are
// impossible to read without it, and once per process is bounded.
MGLOG_E_ONCE("gl_BaseInstance: this driver reports GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d, so the "
"%s storage block an indirect draw's baseInstance must be read through cannot be "
"declared in the vertex stage. Dropping indirect baseInstance support: non-indirect "
"draws are correct, indirect draws will see a stale per-command baseInstance.",
g_GLESCapabilities.MaxVertexShaderStorageBlocks, INDIRECT_PARAMS_BLOCK_NAME);
if (rebaseInstanceId) {
// Without the block there is no per-command baseInstance to subtract, and
// the uniform is the same value the define below resolves to, so rebasing
// by it would cancel the base out of gl_InstanceID twice.
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " gl_InstanceID\n";
}
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " (" + BASE_INSTANCE_UNIFORM_NAME + ")";
source.replace(pos, declaration.size(), machinery);
break;
}
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
@@ -2477,26 +2426,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return packedData.data();
}
// "Some level of this texture holds an image", which is all the sync gate below actually
// needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also
// answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with
// undefined lower levels - but such a texture still has to be uploaded, or the level that
// IS defined never reaches the driver at all.
static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) {
const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject);
if (mipmapObject == nullptr) return false;
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) {
return true;
}
}
}
return false;
}
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
@@ -2544,14 +2473,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 3. Size changed
// 4. Mipmap levels changed
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
// and ARB_clear_texture's conformance cases both produce. Bailing out on that shape
// left the backend name with no levels whatsoever, so the level that WAS defined could
// never be sampled or read back. Sync whenever some level holds an image; the per-level
// loops below skip the degenerate ones individually.
if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
if (!stateTextureObject->IsComplete()) {
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
@@ -2653,13 +2576,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
// A level the application never defined reads back as {0, 0, 0}; now that a
// sparse chain is synced rather than skipped whole, leave those undefined on
// the driver instead of giving the name a 0x0 image at that index.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -2732,59 +2648,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
// The frontend validates against the count MobileGL advertises, which can
// exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4,
// GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call:
// stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and
// framebuffer completeness still report what the application asked for.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetFormatCapabilityTargetIndex(targetInternal), textureMipmapObject->GetFormat(),
glFormat, static_cast<Int>(stateTextureObject->GetSamples())));
// ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1,
// so a degenerate size has nothing to allocate and must not reach the
// driver. The frontend deallocates such an image rather than defining it
// (GL 4.6 core 8.8), so this is belt and braces for any path that still
// syncs one.
const Bool hasAllocatableSize =
baseSize.x() >= 1 && baseSize.y() >= 1 &&
(targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1);
if (!hasAllocatableSize) {
MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)",
m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z());
} else {
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
break;
case TextureTarget::Texture2DMultisampleArray:
g_GLESFuncs.glTexStorage3DMultisample(
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(baseSize.z()),
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
break;
default:
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d",
static_cast<Int>(targetInternal));
break;
}
m_backendStorageImmutable = true;
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
break;
case TextureTarget::Texture2DMultisampleArray:
g_GLESFuncs.glTexStorage3DMultisample(
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(baseSize.z()),
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
break;
default:
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d", static_cast<Int>(targetInternal));
break;
}
// The one storage branch that cleared the ES error queue without ever
// draining it again, so anything this call raised was left for an
// unrelated later query to trip over. Paired with its two siblings now.
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target,
glInternalFormat, backendSamples](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, "
"samples=%d",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
static_cast<Int>(backendSamples));
});
m_backendStorageImmutable = true;
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
@@ -2887,13 +2769,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
// See the append-mips loop: an undefined level stays undefined on the
// driver rather than becoming a 0x0 image.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 ||
levelTexelSize.z() <= 0) {
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -4989,78 +4864,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &loweredSpirv;
}
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
// there IS an extension that provides it - so this runs only when the driver does
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
// gets the `#extension` request added to the decompiled source below instead.
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
// viewport 0), which is the degradation ViewportArrayScenario already documents
// for this backend; NOT demoting it costs the whole program, because the stage
// fails to compile and every draw made with it silently renders nothing.
// Gated on the module actually declaring the output, so no other stage pays an
// optimizer round trip for it.
// One parse of the module answers every armed pass gate below. The per-gate
// Declares* probes each cost a BuildModule per stage, and on a driver where both
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
// Probing the pre-lowering module is sound for both gates: demoting
// gl_ViewportIndex neither adds nor removes multisampled image types.
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
// this is backend code and must not reach into the GL frontend. 4 is that
// translation unit's kFrontendMaxSamples, which is the source of truth -
// keep the two in step.
constexpr Int kFrontendMaxSamples = 4;
const Int advertisedMaxSamples =
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
const Bool sampleClampArmed =
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
if (viewportLoweringArmed || sampleClampArmed) {
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
*effectiveSpirv);
}
Vector<unsigned int> loweredViewportSpirv;
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
!loweredViewportSpirv.empty()) {
effectiveSpirv = &loweredViewportSpirv;
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
"global; every invocation renders into viewport 0.",
m_backendProgramId,
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
// reports - but the realised allocation cannot be, and
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
// texture the ONE sample Adreno and Mali actually support for it. A shader
// written against the advertised ceiling then fetches a sample that storage does
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
// the Sample operand to the backend-real per-category maximum so the fetch lands
// inside the allocation. Gated on some category actually being squeezed AND the
// module actually declaring a multisampled image, so no other stage pays an
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
// allocates the sample count it was asked for, so its modules are already right.
Vector<unsigned int> clampedSampleSpirv;
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
*effectiveSpirv, clampedSampleSpirv,
g_GLESCapabilities.MaxColorTextureSamples,
g_GLESCapabilities.MaxIntegerSamples,
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
enableSpirvValidation) &&
!clampedSampleSpirv.empty()) {
effectiveSpirv = &clampedSampleSpirv;
}
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
// rather than emulating them, so this has to happen before it sees the binary.
Vector<unsigned int> splitArrayInputSpirv;
@@ -5242,17 +5045,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RequestExtendedImageFormats(std::move(source),
imageFormatBake.needsExtendedImageFormats &&
g_GLESCapabilities.SupportsExtendedImageFormats);
// The third header-level rewrite, for the builtin SPIRV-Cross prints bare:
// gl_ViewportIndex is in no version of ESSL core, so without this directive the
// stage does not compile and the whole program - not just its viewport routing -
// is lost. The token probe keeps the line off every other program and the
// capability gate keeps it off drivers that would hard-error on an unadvertised
// name; a driver without the extension took the LowerViewportIndexPass fallback
// above and its source no longer names the builtin at all, so the two are mutually
// exclusive by construction. Read `source` BEFORE it is moved from.
const Bool needsViewportArrayExtension = g_GLESCapabilities.SupportsViewportArray &&
source.find("gl_ViewportIndex") != String::npos;
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
// The completion half of the format bake, for the formats SPIRV-Cross throws on
@@ -5828,39 +5620,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
// The allocation is deferred to here, so an ES driver that refuses it (a
// multi-gigabyte renderbuffer is refused routinely) used to leave m_isInitialized
// true over a renderbuffer with no storage and say nothing at all: the attachment
// then rendered nowhere. Drain first so the check cannot pick up an unrelated stale
// flag, and report GL_OUT_OF_MEMORY to the application. The error lands on whatever
// entry point triggered the sync rather than on glRenderbufferStorage itself, which
// is where the deferred model puts it - still far better than silence.
DebugImpl::ErrorLopper::Clear();
if (samples > 0) {
// Same clamp as the multisample texture path: the frontend accepts the count it
// advertised, the driver only takes the count it supports for this format, and
// the state object keeps reporting the requested one.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
g_GLESFuncs.glRenderbufferStorageMultisample(
GL_RENDERBUFFER, static_cast<GLsizei>(samples), glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
}
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
stateRBOObject->GetExternalIndex(), width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
if (MG_State::pGLContext) {
MG_State::pGLContext->RecordError(
ErrorCode::OutOfMemory,
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
"The ES driver could not allocate the renderbuffer storage."));
}
}
DebugImpl::ErrorLopper::Clear();
m_cacheInternalFormat = internalFormat;
m_cacheWidth = width;
-35
View File
@@ -21,16 +21,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
// 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.
//
// The indirect half of the gl_BaseInstance lowering in PromoteDrawParameterGlobalsToUniforms
// is the only thing that needs this, and it needs exactly one block. A driver reporting 0 is
// conformant - the minimum is 0 in GL 4.6 table 23.64 and ES 3.2 table 21.44 - and ARM's
// GLES driver does report 0, so this is a live path, not a defensive one.
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks);
// True once the process has entered exit(): past that point the EGL library and
// the driver may already be unloaded, so a backend twin's destructor must not
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
@@ -139,28 +129,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Twin creation is the moment a driver-owned id starts needing a guarded
// destructor; cold path, so the once-guard costs nothing per draw.
EnsureProcessTeardownSentinel();
// Sweep BEFORE the entry reference below exists: the map is open-addressed and an
// erase relocates the rest of the probe cluster, so collecting once that reference
// is taken would invalidate it. The sweep is therefore owed from an earlier call
// rather than triggered by this one.
if (m_creationTick >= kCreationGCInterval) {
m_creationTick = 0;
CollectGarbage();
}
const SizeT entryCountBeforeInsert = m_entries.size();
auto& entry = m_entries[stateObj.get()];
if (m_entries.size() != entryCountBeforeInsert) {
// A key the registry has never held. Nothing tells the backend that a texture or
// renderbuffer was DELETED - the twin, and the driver storage it owns, lives
// until a collection - and CollectGarbageIfNeeded is ticked only from the
// per-draw sync paths, which a CTS-shaped workload runs about ten times per
// case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead
// (and, for this suite, gigabyte-sized) objects stay allocated at once. Object
// CHURN rather than draw count is what makes the sweep urgent, so a twin the
// registry has never seen ticks it too - and it does so on the path that is
// about to allocate, which is exactly when the memory is needed.
++m_creationTick;
}
if (entry.stateRef.expired()) {
// The previous owner of this address is gone and the allocator handed it
// to a new object: its twin describes ids the new state object never made.
@@ -234,12 +203,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
static constexpr Uint32 kGCInterval = 1024;
// Creations are far rarer than draws, so this counts in a much smaller unit than
// kGCInterval does.
static constexpr Uint32 kCreationGCInterval = 64;
BackendMap m_entries;
Uint32 m_gcTick = 0;
Uint32 m_creationTick = 0;
Bool m_isCollecting = false;
};
-37
View File
@@ -569,43 +569,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String RequestViewportArrayExtension(String glslCode, Bool needed) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// gl_ViewportIndex is desktop GL 4.1 core and is in ESSL only under
// GL_OES_viewport_array. SPIRV-Cross prints the identifier as-is and requests no
// extension for it - three lines away from the BuiltInLayer case, which DOES ask for
// one on ES - so an untouched decompile reaches the driver naming a builtin its core
// language has never heard of. The stage then fails to compile, the program is marked
// unusable and every draw made with it renders nothing while raising no GL error.
//
// Same `needed` contract as RequestExtendedImageFormats, and the same hard rule:
// `#extension` on a name the driver does not advertise is itself a compile error
// (ARM's compiler is strict about it), so this must never be emitted speculatively.
// A driver without the extension does not come through here at all - its module took
// the LowerViewportIndexPass fallback and the emitted source no longer names the
// builtin.
static constexpr const char* kDirective = "#extension GL_OES_viewport_array : require\n";
static constexpr const char* kExtName = "GL_OES_viewport_array";
if (!needed || glslCode.find(kExtName) != String::npos) {
return glslCode;
}
// Right after the #version line, for the reason spelled out above: it is the only
// position that must stay first, and ForceSupporterOutput's scan for the LAST
// #extension directive still finds whichever one that ends up being.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
-10
View File
@@ -154,16 +154,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
// spelling for it at any version, so the request has to be made here or the stage does
// not compile - which loses the whole program, not just the multi-viewport routing.
// `needed` is the caller's answer for the same reason as above: only it knows whether the
// driver advertises the extension, and requesting an unadvertised one is itself a compile
// error, so this is never emitted speculatively. A no-op when not needed or already
// present.
String RequestViewportArrayExtension(String glslCode, Bool needed);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
@@ -847,38 +847,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks);
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
// carries), so the stage limits differ only by whether the stage can have blocks at all.
//
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
// through this backend - those write their result through a storage block declared in a
// tessellation stage. Gating would report 0 and turn 433 passing cases into
// "unsupported", removing function that demonstrably works.
//
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
// the driver refuses outright at link time; the honest limit converts a silent
// wrong-render into a capability an application can route around. Here there is no such
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
// pipeline creation where the rejection is observable, not on a feature bit this driver
// reports inaccurately.
{
const Int maxPerStageStorageBlocks =
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
// The one hard capability in the set: no geometry stage means no blocks in it.
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
}
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
@@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
}
void GenerateMipmap(GLenum target) {
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -3190,25 +3190,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
// operations execute natively; module repairs keep the GL contract intact
// operations execute natively; two 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) {
// 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
// fingerprint before either native or emulated subgroup lowering.
if (m_subgroupPolicy.fixIterationRPBarrier) {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
"Program 203 keeps its shared-scratch race",
program.GetExternalIndex());
}
}
if (m_subgroupPolicy.emulateSubgroups) {
Vector<Uint> emulatedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
@@ -3228,7 +3213,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
m_subgroupPolicy.maxComputeSharedMemoryBytes,
enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
@@ -381,7 +381,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
Uint32 nativeSubgroupSize = 0;
@@ -1848,19 +1848,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) {
// Nothing at two samples or above. Reachable because the frontend validates
// multisample allocations against the count MobileGL ADVERTISES (GL requires
// GL_MAX_SAMPLES >= 4) rather than against the device's per-format support, so
// a format this device cannot multisample at all now gets here instead of
// being refused up front. Keeping the unsupported count would hand
// vkCreateImage an invalid VkImageCreateInfo; one sample is at least a legal
// image, and the samples-08726 hazard above is the lesser of the two.
MGLOG_W_ONCE("Multisample texture format %d supports no count above one on this device; "
"backing it with a single sample",
static_cast<Int>(format));
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
}
if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
@@ -3063,7 +3063,6 @@ void main() {
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
subgroupPolicy.fixIterationRPSubgroupScratch =
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
subgroupPolicy.deriveNumSubgroups =
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
@@ -8869,7 +8868,7 @@ void main() {
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
// equal the array side's layerCount".
struct CopyImageSliceMapping {
struct CopyImageEndpoint {
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
Bool slicesAreDepth = false;
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
@@ -8883,35 +8882,13 @@ void main() {
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
};
// The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
// 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
// renderbuffer is a single-level, single-layer 2D image, so its shape answers are
// constants rather than a mip walk. `trackedLayout` points AT the owning resource's own
// layout field - both resource maps are node-based, so the pointer survives the further
// lookups the clear materialization below makes.
struct CopyImageVkImage {
Bool isRenderbuffer = false;
VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
Uint32 mipLevels = 1;
VkExtent2D extent = {0, 0};
Uint32 depth = 1;
Uint32 arrayLayers = 1;
};
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) {
Bool TryResolveCopyImageEndpoint(TextureTarget target,
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
if (glZ < 0 || glDepth <= 0) {
return false;
}
const Uint32 baseSlice = static_cast<Uint32>(glZ);
if (image.isRenderbuffer) {
// A renderbuffer holds one 2D image and nothing else; GL still requires the
// z/depth pair and it can only name that one slice.
outMapping = {};
return baseSlice == 0 && glDepth == 1;
}
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
@@ -8919,12 +8896,12 @@ void main() {
case TextureTarget::Texture2DMultisample:
// Not layered at all: GL still requires the z/depth pair, and it can only name the
// one slice these targets have.
outMapping = {};
outEndpoint = {};
return baseSlice == 0 && glDepth == 1;
case TextureTarget::Texture3D:
outMapping.slicesAreDepth = true;
outMapping.baseSlice = baseSlice;
outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
outEndpoint.slicesAreDepth = true;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
return true;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
@@ -8933,9 +8910,9 @@ void main() {
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
// numbers its faces on the same z axis an array texture numbers its layers, so both
// arrive as a plain layer range.
outMapping.slicesAreDepth = false;
outMapping.baseSlice = baseSlice;
outMapping.availableSlices = image.arrayLayers;
outEndpoint.slicesAreDepth = false;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = resource.arrayLayers;
return true;
default:
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
@@ -8945,20 +8922,15 @@ void main() {
return false;
}
}
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
}
} // namespace
void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dstEndpoint,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
"CopyImageSubData requires valid source and destination images.");
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
"CopyImageSubData requires valid source and destination textures.");
// The frontend already declines a zero or negative extent, so anything else here is a
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
// zero extent.depth is as invalid as a zero width.
@@ -8975,9 +8947,9 @@ void main() {
// and an overlap check). Refused outright, and refused for real rather than through an
// assertion the release build drops: recording the pair anyway is a validation error and,
// on a tiler, a copy whose source has already been overwritten.
if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
CopyImageEndpointName(srcEndpoint));
if (srcTexture.get() == dstTexture.get()) {
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
srcTexture->GetExternalIndex());
return;
}
@@ -8990,42 +8962,8 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
// One resolver for both object kinds. The texture arm is the same
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
// render-pass manager, which is where a renderbuffer's VkImage lives.
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
if (endpoint.IsRenderbuffer()) {
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
if (resource == nullptr) return false;
out.isRenderbuffer = true;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = 1;
out.extent = resource->extent;
out.depth = 1;
out.arrayLayers = 1;
return out.image != VK_NULL_HANDLE;
}
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
// reaches here - but the assertion that says so is compiled out of a release build.
if (endpoint.Texture == nullptr) return false;
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture);
if (resource == nullptr) return false;
out.isRenderbuffer = false;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = resource->mipLevels;
out.extent = resource->extent;
out.depth = resource->depth;
out.arrayLayers = resource->arrayLayers;
return true;
};
CopyImageVkImage srcImage{};
CopyImageVkImage dstImage{};
const Bool srcResolved = resolveImage(srcEndpoint, srcImage);
const Bool dstResolved = resolveImage(dstEndpoint, dstImage);
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture);
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture);
// Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in
// a release build, which is where both observed failures happened - a null resource
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
@@ -9041,29 +8979,29 @@ void main() {
// The frontend validator (ValidateTextureLevelExists) is what produces the
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap
// up there declines a copy instead of taking the process down.
if (!srcResolved || !dstResolved) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
if (srcResource == nullptr || dstResource == nullptr) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
return;
}
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels ||
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) {
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels);
return;
}
const VkImageAspectFlags copyAspectMask =
srcImage.aspect & dstImage.aspect &
srcResource->aspect & dstResource->aspect &
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
MOBILEGL_ASSERT(copyAspectMask != 0 &&
(srcImage.aspect & copyAspectMask) == srcImage.aspect &&
(dstImage.aspect & copyAspectMask) == dstImage.aspect,
(srcResource->aspect & copyAspectMask) == srcResource->aspect &&
(dstResource->aspect & copyAspectMask) == dstResource->aspect,
"CopyImageSubData source and destination aspects are incompatible.");
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel);
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel);
// Promoted for the same reason as the level range above, and it is the same bug class:
// a VkImageCopy whose region runs past the image is an out-of-bounds promise to the
// driver, and the frontend does not check the region at all (there is a CTS sibling,
@@ -9086,10 +9024,10 @@ void main() {
// here: every target whose slices this function can address on one of the two Vulkan axes.
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
CopyImageSliceMapping srcSlices;
CopyImageSliceMapping dstSlices;
if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
!TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
CopyImageEndpoint srcEndpoint;
CopyImageEndpoint dstEndpoint;
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
@@ -9100,53 +9038,40 @@ void main() {
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
// both come from the endpoint that resolved them.
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) {
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
"declining the copy",
__func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
return;
}
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) {
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
}
return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture);
};
const Bool clearReady = materializeClear(srcEndpoint);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u",
__func__, CopyImageEndpointName(srcEndpoint));
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
__func__, srcTexture->GetExternalIndex());
// A clear still parked on the destination would otherwise materialize AFTER this copy and
// wipe the texels it just wrote.
const Bool dstClearReady = materializeClear(dstEndpoint);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
__func__, CopyImageEndpointName(dstEndpoint));
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
__func__, dstTexture->GetExternalIndex());
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
const VkImageLayout srcOriginalLayout = srcResource->layout;
const VkImageLayout dstOriginalLayout = dstResource->layout;
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
// undefined by the same spec sentence that lets the application ask. Both sides therefore
// take the same shape - transition the whole image out of UNDEFINED and settle it on a
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
// A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is
// the layout MaterializePendingClearForRenderbuffer leaves it in.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) {
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
return originalLayout;
}
const Bool depthStencil =
(copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
if (isRenderbuffer) {
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
}
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
};
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
@@ -9157,15 +9082,15 @@ void main() {
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
srcImage.aspect, 0, srcImage.mipLevels);
srcResource->aspect, 0, srcResource->mipLevels);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
srcCopyLayout = *srcImage.trackedLayout;
srcCopyLayout = srcResource->layout;
} else {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcImage.image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
@@ -9177,15 +9102,15 @@ void main() {
VkImageLayout dstCopyLayout = dstOriginalLayout;
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
dstImage.aspect, 0, dstImage.mipLevels);
dstResource->aspect, 0, dstResource->mipLevels);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
dstCopyLayout = *dstImage.trackedLayout;
dstCopyLayout = dstResource->layout;
} else {
Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
@@ -9195,18 +9120,18 @@ void main() {
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
VkImageCopy copyRegion{};
copyRegion.srcSubresource.aspectMask = copyAspectMask;
copyRegion.srcSubresource.mipLevel = srcMipLevel;
copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
copyRegion.dstSubresource.aspectMask = copyAspectMask;
copyRegion.dstSubresource.mipLevel = dstMipLevel;
copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()};
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
copyCrossesDepthAxis ? copySliceCount : 1u};
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
@@ -9217,8 +9142,8 @@ void main() {
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
vkCmdCopyImage(frame.commandBuffer,
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copyRegion);
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -9226,14 +9151,14 @@ void main() {
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
srcImage.aspect, 0, srcImage.mipLevels);
srcResource->aspect, 0, srcResource->mipLevels);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
} else {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
@@ -9244,14 +9169,14 @@ void main() {
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
dstImage.aspect, 0, dstImage.mipLevels);
dstResource->aspect, 0, dstResource->mipLevels);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
} else {
Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
@@ -23,7 +23,6 @@
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <vk_mem_alloc.h>
#include "../VkIncludes.h"
@@ -198,9 +197,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dstEndpoint,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -18,11 +18,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
//
// Native subgroups are the implementation whenever the device has them, whatever
// their width - subgroup operations execute on the hardware paths they were made
// for. Module-level repairs keep the GL contract intact around them:
// for. Two module-level repairs keep the GL contract intact around them:
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
// reusing that scratch, when explicitly enabled;
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
// (gl_NumSubgroups) with the value the rest of the topology implies.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
@@ -49,10 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.IterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
// contract to hold, and the derived ceil() value is the one the renderer can pin
-1
View File
@@ -44,4 +44,3 @@ add_subdirectory(Program)
add_subdirectory(Buffer)
add_subdirectory(Driver)
add_subdirectory(Container)
add_subdirectory(Transpile)
@@ -1,20 +0,0 @@
cmake_minimum_required(VERSION 3.24)
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
# breakdown of one program build, which needs its own clock around sub-steps that share
# set-up, and a plain main() keeps the output a table this can be read straight out of.
add_executable(
TranspileProfile
TranspileProfile.cpp
)
target_include_directories(TranspileProfile PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranspileProfile PRIVATE
${LINK_LIBRARIES}
)
File diff suppressed because it is too large Load Diff
+4 -45
View File
@@ -108,12 +108,6 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) {
// 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) {
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
}
// Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
@@ -133,11 +127,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
// Only draws that get this far are in the written counter at all. The instanced and
// indirect entry points never call this function, so a span that contains one is NOT
// fully accounted, and the queries must be able to tell: they compare this counter's
// delta against zero before standing in for the backend's own result.
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
}
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
@@ -162,23 +151,11 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the
// state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM
// unconditionally, while "no current program" is not even a spec-listed draw error - it is
// MobileGL's own null-dereference guard - so it must never shadow the enum check
// (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode
// GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM).
static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) {
if (IsAcceptedPrimitiveMode(mode)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
return false;
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
if (!ValidatePrimitiveModeEnum(functionName, mode)) {
if (!IsAcceptedPrimitiveMode(mode)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
return false;
}
@@ -619,14 +596,12 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
@@ -740,14 +715,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElements_Backend(mode, start, end, count, type, indices);
@@ -755,7 +728,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
@@ -764,7 +736,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
@@ -772,21 +743,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
@@ -796,21 +764,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount);
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
@@ -818,7 +783,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
@@ -826,7 +790,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
@@ -834,7 +797,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) {
@@ -848,7 +810,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
@@ -856,7 +817,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
@@ -867,7 +827,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
@@ -13,7 +13,6 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Impl/GLImpl/Texture/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -618,17 +617,16 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
return GetAdvertisedMaxSamples();
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
}
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
// count it actually hands the driver.
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path already resolves the limit per format
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
@@ -647,10 +645,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State();
}
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
// clamps to it, while the frontend honours what it advertised.
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
return std::max(dynamicParameters.MaxIntegerSamples, 1);
}
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -3153,55 +3148,15 @@ namespace MobileGL::MG_Impl::GLImpl {
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
}
// The three argument errors GL 4.6 core 18.3.1 asks a blit for. They have to be raised here,
// in the backend-independent frontend: DirectGLES drains the driver's error queue around the
// blit on purpose (that is how the resolve fallback probes the driver), so an ES-side
// rejection never reaches the application and glGetError() answered GL_NO_ERROR for a call
// the spec requires to fail (KHR-GL30.api.coverage's glBlitFramebuffer sub-check). DirectVulkan
// already dropped the bad-filter and LINEAR-with-depth/stencil calls on the floor with a log
// line (VulkanRenderer::BlitFramebuffer), so the only thing that changes for it is that the
// error is now visible where the spec says it should be.
static Bool ValidateBlitMaskAndFilter(const char* functionName, GLbitfield mask, GLenum filter) {
constexpr GLbitfield kBlitMaskBits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
if ((mask & ~kBlitMaskBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"mask contains bits other than GL_COLOR_BUFFER_BIT, "
"GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT."));
return false;
}
if (filter != GL_NEAREST && filter != GL_LINEAR) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"filter must be GL_NEAREST or GL_LINEAR."));
return false;
}
// Depth and stencil have no meaningful interpolation, so GL_LINEAR is rejected outright
// rather than downgraded - even when the mask also carries the colour bit.
if (filter == GL_LINEAR && (mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"GL_LINEAR filtering is not allowed when mask includes "
"GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT."));
return false;
}
return true;
}
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter);
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
}
+9 -39
View File
@@ -213,23 +213,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
}
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
// constant to be "restored" - these used to return a flat 16 for vertex, geometry and
// both tessellation stages, which is wrong on any host that does not serve storage
// blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
// 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
// why the conformance suite gates each such test on the query instead of assuming it.
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
// bought nothing: the program still failed to link inside the backend, the frontend
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
const MG_Backend::DynamicBackendParameters& parameters =
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: kBackendlessDefaults;
return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
}
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
if (pname == GL_DRAW_BUFFER) {
drawBufferIndex = 0;
@@ -439,18 +422,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
} // namespace
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
// just told the application to use. The backends clamp the realised count instead.
GLint GetAdvertisedMaxSamples() {
if (MG_Backend::pActiveBackendObject == nullptr) {
return kFrontendMaxSamples;
}
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
const GLubyte* GetString(GLenum name) {
static String vendorString;
@@ -1565,7 +1536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentAtomicCounters;
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
*params = kFrontendMaxFragmentInputComponents;
@@ -1591,7 +1562,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryAtomicCounterBuffers;
return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
*params = kFrontendMaxGeometryInputComponents;
@@ -1662,11 +1633,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0;
return;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
*params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO
@@ -1692,7 +1662,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
*params = kFrontendMaxVertexUniformComponents;
@@ -2147,7 +2117,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxClipDistances;
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
*params = dynamicParameters.MaxColorTextureSamples;
break;
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
@@ -2177,7 +2147,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxCubeMapTextureSize;
break;
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
*params = dynamicParameters.MaxDepthTextureSamples;
break;
case GL_MAX_FRAMEBUFFER_WIDTH:
*params = dynamicParameters.MaxFramebufferWidth;
@@ -2204,7 +2174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeImageUniforms;
break;
case GL_MAX_INTEGER_SAMPLES:
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
*params = dynamicParameters.MaxIntegerSamples;
break;
case GL_MAX_RENDERBUFFER_SIZE:
*params = dynamicParameters.MaxRenderbufferSize;
@@ -2370,7 +2340,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: dynamicParameters.MaxDrawBuffers;
break;
case GL_MAX_SAMPLES:
*params = GetAdvertisedMaxSamples();
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
break;
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
@@ -24,8 +24,4 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError();
GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum. Frontend multisample validators have to honour this ceiling for every
// format, otherwise MobileGL rejects a sample count it advertised itself.
GLint GetAdvertisedMaxSamples();
} // namespace MobileGL::MG_Impl::GLImpl
+6 -66
View File
@@ -31,15 +31,8 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
// The transform feedback primitive counter matching this query's target, at
// BeginQuery time.
// Transform feedback primitive counter at BeginQuery time.
Uint64 counterSnapshot = 0;
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
// accounting had reproduced exactly, and how many of those it could not (a
// geometry stage amplifies). Their deltas decide whether the CPU result may
// stand in for the backend's.
Uint64 accountedCaptureDrawSnapshot = 0;
Uint64 geometryCaptureDrawSnapshot = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
@@ -129,46 +122,6 @@ namespace MobileGL::MG_Impl::GLImpl {
g_activeTimeElapsedQueryId = 0;
}
// The CPU accounting counter a transform feedback query target reads: what the capture
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
// for both targets would report the clamped written count as the generated one.
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
}
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
// context switch between Begin and End, a counter that never moved) would otherwise
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
}
// Whether this ended span's result should come from the CPU accounting rather than from
// the backend query it also ran. Three conditions, all necessary:
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
// one; DirectVulkan never sets the bit and so is untouched by any of this);
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
// whether or not a capture is active, and the accounting only ever sees capture
// draws, so the backend's counter is the more complete answer there;
// * the span was fully accounted: at least one capture draw reached the accounting
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
// span made of those is invisible to it) and none of them amplified through a
// geometry stage, which the CPU cannot model.
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
queryObject->geometryCaptureDrawSnapshot) {
return false;
}
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
queryObject->accountedCaptureDrawSnapshot;
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back. `outValueProduced`, when given,
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
@@ -454,11 +407,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
queryObject->accountedCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
} else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
@@ -499,21 +448,12 @@ namespace MobileGL::MG_Impl::GLImpl {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle);
}
}
// A backend query that is not going to be read is released here, not left to be
// collected later: the span is over, the driver object has nothing left to say.
// Ending it first is what makes that legal.
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
// Result comes from the GPU query at read time.
} else {
queryObject->cachedResult =
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
queryObject->resultCached = true;
}
// Otherwise the result comes from the GPU query at read time.
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
-29
View File
@@ -8,7 +8,6 @@
#include "GL_Sync.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -36,22 +35,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace
GLsync FenceSync(GLenum condition, GLbitfield flags) {
// GL 4.6 core 4.1.2: GL_SYNC_GPU_COMMANDS_COMPLETE is the only condition and the only
// legal flags value is zero; both violations return 0 rather than a handle. A caller that
// then hands the 0 back to glDeleteSync hits the glDeleteSync(0) no-op below.
if (condition != GL_SYNC_GPU_COMMANDS_COMPLETE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"condition must be GL_SYNC_GPU_COMMANDS_COMPLETE."));
return nullptr;
}
if (flags != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "flags must be zero."));
return nullptr;
}
auto* syncObject = new SyncObject;
syncObject->condition = condition;
syncObject->flags = flags;
@@ -81,18 +64,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.2: the server-side wait takes no flags and no finite timeout - both
// arguments exist only to be forward-compatible, and anything else is INVALID_VALUE.
// Neither backend ever honored a nonzero timeout (DirectGLES hard-codes
// 0/GL_TIMEOUT_IGNORED, DirectVulkan's queue ordering makes the wait implicit), so
// rejecting the call loses no wait that used to happen.
if (flags != 0 || timeout != GL_TIMEOUT_IGNORED) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero and timeout must be GL_TIMEOUT_IGNORED."));
return;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
return;
+90 -357
View File
@@ -474,48 +474,15 @@ namespace MobileGL::MG_Impl::GLImpl {
target == TextureTarget::Texture2DMultisampleArray;
}
// The largest count the backend actually probed for this format on this target, or 0 when
// it has no answer for the pair. Both backends build the list in descending order.
Int GetProbedMaxTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 0;
}
const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(textureTarget);
const SizeT formatIndex = static_cast<SizeT>(textureInternalFormat);
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
return 0;
}
const auto& sampleCounts =
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
// the driver; the shadow state keeps reporting what the application asked for.
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
TextureInternalFormat textureInternalFormat) {
Int GetMaxSupportedTextureSamples(TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format
// maximum - validating against anything else is how the two answers drifted apart.
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
if (probedMaxSamples > 0) {
return std::max(probedMaxSamples, advertisedMaxSamples);
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
return std::max(dynamicParameters.MaxDepthTextureSamples, 1);
}
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
@@ -528,7 +495,7 @@ namespace MobileGL::MG_Impl::GLImpl {
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
: dynamicParameters.MaxColorTextureSamples,
advertisedMaxSamples);
1);
}
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
@@ -565,7 +532,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, textureInternalFormat);
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) {
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
// exceeds what the format supports, and the native Adreno driver agrees.
@@ -590,20 +557,6 @@ namespace MobileGL::MG_Impl::GLImpl {
"AllocateMultisampleTextureStorage requires mipmap-backed storage");
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
// GL 4.6 core 8.8: a zero-sized image DEALLOCATES the image rather than defining an
// empty one. Only the multisample pair cares, and it cares a great deal: the CTS's
// per-case state reset clears both DEFAULT multisample textures this way on every
// texture unit, and a "defined" 0x0 default texture stops being skipped by
// IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
// every unit the reset touched, and reaches an ES glTexStorage*Multisample(..., 0, 0)
// that ES 3.1 8.19 makes INVALID_VALUE on every driver there is. A proxy target holds
// no image at all, only the query result, so it keeps recording what was asked for.
if ((width <= 0 || height <= 0 || depth <= 0) &&
!TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
textureObject->SetInternalFormat(TextureInternalFormat::Unknown);
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 0);
return;
}
textureObject->SetInternalFormat(textureInternalFormat);
textureObject->SetSamples(samples);
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
@@ -691,34 +644,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject;
}
// Whether a raw internalformat enum names a compressed format - the question GL asks whenever an
// entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no
// block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION
// rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug
// 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again).
// Written against the enum ranges rather than a name list because the families are contiguous
// and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this
// check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(
@@ -754,21 +679,6 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture level {} is not defined.", level));
return nullptr;
}
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear
// entry points. Two tags to ask, because they answer different questions: the stored
// one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat
// defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL
// deliberately backs with uncompressed storage (see MipmapStorage) and that would
// otherwise look like an ordinary RGBA8 image by the time the clear runs.
const auto& uploadTargets = mipmapTexture->GetUploadTargets();
if (!uploadTargets.empty() &&
(mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) != GL_NONE ||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
GL_NONE)) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Compressed textures cannot be cleared.");
return nullptr;
}
return mipmapTexture;
}
@@ -2240,26 +2150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
// The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State):
// GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and
// GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData
// sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2
// array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and
// the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above
// clears the tag, so this has to follow it.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
if (compressedInfo.blockWidth != 0) {
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}));
}
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
}
if (!originalPixels) {
@@ -2406,13 +2296,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
}
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
}
if (!originalPixels) {
@@ -2501,13 +2384,6 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
// After AllocateStorage, which clears the tag. No block-compressed format has a 1D
// layout, so only the specific-format tag the 2D/3D paths record is skipped here - the
// request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalFormat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalFormat));
}
}
if (!originalPixels) {
@@ -3489,9 +3365,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const MG_Backend::CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
@@ -3502,7 +3378,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support image-to-image copies."));
return;
}
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX,
dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
@@ -3549,9 +3425,9 @@ namespace MobileGL::MG_Impl::GLImpl {
// the ~30 entry points that reach it through a BOUND object (where the name was never
// in question and the fault is the binding), so this is a local rule rather than a
// change to the helper.
Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* endpointName) {
if (endpoint.Exists()) return true;
if (textureObject) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
@@ -3575,106 +3451,21 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false;
}
// ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
// A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
// no sampler state, and it carries its own internal format and extent.
Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
return endpoint.Texture->GetSamples();
}
TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture->GetFormat();
}
// A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE
// ValidateTextureLevelExists records for a level a texture does not have.
Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level,
const char* caller) {
if (!endpoint.IsRenderbuffer()) {
return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller);
}
if (level == 0) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
return false;
}
// Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
// minification filter can make them mipmap incomplete - while the shared predicate derives
// q from the base level's size alone and would call a 16x16 multisample image incomplete.
Bool CopyImageTargetHasMipmapChain(TextureTarget target) {
switch (target) {
case TextureTarget::TextureRectangle:
case TextureTarget::TextureBuffer:
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return false;
default:
return true;
}
}
Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) {
// A renderbuffer is complete exactly when it has storage - there is nothing else it
// could be missing.
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated();
const auto* texture = endpoint.Texture.get();
if (!texture) return false;
// 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the
// MIP CHAIN whenever the minification filter samples it - and ITextureObject::
// IsComplete() only answers the storage half (an internal format, and no zero-size
// level in the middle of the chain). A texture with level 0 alone and the default
// NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how
// KHR-GL43.copy_image.incomplete_tex builds its subject.
//
// The filter is the texture's OWN: copy-image never goes through a texture unit, so no
// sampler object is in play. An immutable texture is unaffected - glTexStorage clamps
// TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture
// mipmap complete under any filter.
const auto& sampler = texture->GetSamplerObject();
const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler &&
sampler->GetMipmapMode() != SamplerMipmapMode::None;
return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped);
}
GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) return GL_NONE;
return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level);
}
IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) {
return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1};
}
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
}
} // namespace
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
const MG_Backend::CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!ValidateCopyImageObjectExists(src, "source") ||
!ValidateCopyImageObjectExists(dst, "destination")) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
return false;
}
// GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
// whole-image target, and the endpoint that carries it was resolved from the renderbuffer
// namespace, so it matches its object by construction.
const auto srcTextureTarget =
src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget =
dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) ||
(!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) {
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) ||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
return false;
}
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
@@ -3682,8 +3473,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false;
}
if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
return false;
}
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
@@ -3697,8 +3488,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
!ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
return false;
}
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
@@ -3714,41 +3505,37 @@ namespace MobileGL::MG_Impl::GLImpl {
// A multisample image can only be copied to one with the same sample count, and a
// single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one.
const Int srcSamples = GetCopyImageEndpointSamples(src);
const Int dstSamples = GetCopyImageEndpointSamples(dst);
if (srcSamples != dstSamples) {
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).",
srcSamples, dstSamples)));
srcTexture->GetSamples(), dstTexture->GetSamples())));
return false;
}
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into.
const Bool srcComplete = IsCopyImageEndpointComplete(src);
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
if (!srcComplete || !dstComplete) {
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcComplete, dstComplete)));
srcTexture->IsComplete(), dstTexture->IsComplete())));
return false;
}
const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false;
}
const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
@@ -4264,14 +4051,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
// and for a cube map that is exactly what IsComplete() answers (all six faces defined at
// every level). It must not speak for any other target: on a mip chain it also rejects
// "level N defined, the levels below it not", which is a perfectly readable texture at
// level N - and the shape glClearTexImage's conformance cases build, since they define
// only the level they clear. The requested level's own existence is checked below.
if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
!textureObject->IsComplete()) {
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces.
if (!textureObject->IsComplete()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
@@ -4303,8 +4084,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
// integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
// storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8
// (not advertised by MobileGL).
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
return false;
@@ -4330,48 +4111,33 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto* textureMipmapObject =
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
const auto& uploadTargets = textureObject->GetUploadTargets();
// The half of the completeness gate above that GL does keep: the REQUESTED level has
// to hold an image. A name that was never given one carries no levels at all (which is
// also what an Unknown internal format answers), and a chain grown to reach level N
// leaves every level below it at {0, 0, 0}.
if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) {
// Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
// Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
return false;
}
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > bufferSize - offset) {
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
return false;
}
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
return false;
}
}
}
}
@@ -4606,12 +4372,6 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (IsCompressedGLInternalFormat(internalformat)) {
// After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed
// format has a 1D block layout, but glClearTexImage still has to refuse the request.
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly.
@@ -4680,12 +4440,6 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1}));
}
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4693,6 +4447,32 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
}
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
// .compressed_data). Written against the enum ranges rather than a name list because the
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
// carry, which would make this check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -4735,10 +4515,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
// The same specific-compressed-format tag glTexStorage2D records, for the array targets a
// compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means
// a generic format, which MobileGL answers with uncompressed storage, so it is not tagged.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
@@ -4748,19 +4524,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (compressedInfo.blockWidth != 0) {
// After AllocateStorage, which clears the tag.
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, levelDepth}));
}
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
@@ -4921,22 +4684,6 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
}
// Unlike glTexImage*Multisample, where a zero-sized image is a legal deallocation (see
// AllocateMultisampleTextureStorage), the immutable forms take a strictly positive size: GL
// 4.6 core 8.19 makes width, height or depth < 1 INVALID_VALUE. Without this the shared
// _State helper would deallocate the image and TexStorageMultisample_State would then freeze
// the now-imageless texture as immutable.
static Bool ValidateTexStorageMultisampleSize(GLsizei width, GLsizei height, GLsizei depth, const char* caller) {
if (width >= 1 && height >= 1 && depth >= 1) {
return true;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Immutable multisample storage requires width, height and depth >= 1."));
return false;
}
// The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and
// then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION
// (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed
@@ -4957,7 +4704,6 @@ namespace MobileGL::MG_Impl::GLImpl {
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
if (!ValidateTexStorageMultisampleSize(width, height, 1, __func__)) return;
TexStorageMultisample_State(
target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations),
__func__);
@@ -4968,7 +4714,6 @@ namespace MobileGL::MG_Impl::GLImpl {
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
if (!ValidateTexStorageMultisampleSize(width, height, depth, __func__)) return;
TexStorageMultisample_State(target,
TexImage3DMultisample_State(target, samples, internalformat, width, height, depth,
fixedsamplelocations),
@@ -5970,29 +5715,17 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
// INVALID_OPERATION - so resolve through the plain lookups, which answer a null
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
// SharedPtr, and let the validator record the error this entry point owes.
//
// The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
// renderbuffer name has nothing to do with a texture name. Resolving both through
// GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
// happened to collide with a live texture, INVALID_ENUM from the target check.
const auto resolveEndpoint = [](GLuint name, GLenum target) {
MG_Backend::CopyImageEndpoint endpoint{};
if (target == GL_RENDERBUFFER) {
endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name);
} else {
endpoint.Texture = MG_State::pGLContext->GetTextureObject(name);
}
return endpoint;
};
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, dst, dstTarget,
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
MG_State::pGLContext->GetTextureObject(srcName);
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
MG_State::pGLContext->GetTextureObject(dstName);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
return;
}
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
@@ -313,13 +313,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false;
}
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
// STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
// and expects INVALID_OPERATION).
if (format == TextureInputFormat::StencilIndex &&
internalFormat != TextureInternalFormat::StencilIndex8) {
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
if (format == TextureInputFormat::StencilIndex) {
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
}
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
+4 -39
View File
@@ -24,14 +24,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
# Desktop links the static implementation directly. Android runs the same
# executable from adb shell and links the shipping shared library instead.
if (ANDROID)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
elseif (TARGET MobileGL_s)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
else()
message(STATUS "No MobileGL library target is available; skipping the integration test module")
# Only meaningful where MobileGL_s exists (i.e. not Android).
if (NOT TARGET MobileGL_s)
message(STATUS "MobileGL_s is not available; skipping the integration test module")
return()
endif()
@@ -74,7 +69,6 @@ add_executable(MobileGLIntegrationTest
Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/IterationRPFirstReductionScenario.cpp
Scenarios/IterationRPProgram203Scenario.cpp
Scenarios/IterationRPScratchFixScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
@@ -86,7 +80,6 @@ add_executable(MobileGLIntegrationTest
Scenarios/BufferTextureScenario.cpp
Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/XfbPrimitiveQueryScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
@@ -101,20 +94,9 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
target_link_libraries(MobileGLIntegrationTest PRIVATE
GTest::gtest
${MGL_ITEST_MOBILEGL_TARGET}
MobileGL_s
)
if (ANDROID)
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
target_link_libraries(MobileGLIntegrationTest PRIVATE
${MGL_ITEST_ANDROID_LIBRARY}
${MGL_ITEST_LOG_LIBRARY}
${MGL_ITEST_MEDIANDK_LIBRARY}
)
endif()
if (MSVC)
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
# as dllimport on Windows, so the in-library GL entry-point definitions only
@@ -123,10 +105,6 @@ if (MSVC)
endif()
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
if (ANDROID)
return()
endif()
# --- ctest wiring --------------------------------------------------------
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
@@ -247,19 +225,6 @@ endif()
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
if (MOBILEGL_ITEST_VK_ICD)
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
# The three iterationRP repairs are tri-state quirks that default to device
# auto-detection, and lavapipe is not on any auto list - so on lavapipe the
# iterationRP scenarios run unrepaired and Program 203 misses its golden
# output. CI's integration-gpu job exports these three by hand; pinning them
# to the ICD instead means a local `ctest -L integration-gpu` measures the
# same thing the gate does, with no environment to remember.
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
list(APPEND MGL_ITEST_VULKAN_ENV
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
endif()
endif()
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
@@ -18,11 +18,6 @@
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImage.h>
#include <media/NdkImageReader.h>
#endif
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
@@ -42,7 +37,7 @@
// the only construction that is actually predictive here: MobileGL ABORTS
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
// platform, so nothing the parent can call in-process is allowed to be wrong.
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
#define MGITEST_HAVE_FORK_PREFLIGHT 1
#include <csignal>
#include <ctime>
@@ -83,58 +78,12 @@ namespace MGITest {
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
}
#elif defined(__ANDROID__)
AImageReader* g_imageReader = nullptr;
ANativeWindow* g_imageReaderWindow = nullptr;
void DrainImageReader(void*, AImageReader* reader) {
AImage* image = nullptr;
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
AImage_delete(image);
}
}
bool CreateImageReaderWindow() {
if (g_imageReaderWindow != nullptr) return true;
constexpr int kMaxImages = 4;
const media_status_t status = AImageReader_newWithUsage(
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
kMaxImages, &g_imageReader);
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
AImageReader_setImageListener(g_imageReader, &listener);
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
g_imageReaderWindow == nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
return false;
}
ANativeWindow_acquire(g_imageReaderWindow);
return true;
}
void DestroyImageReaderWindow() {
if (g_imageReaderWindow != nullptr) {
ANativeWindow_release(g_imageReaderWindow);
g_imageReaderWindow = nullptr;
}
if (g_imageReader != nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
}
}
#endif
bool UseWindowSurface() {
#if defined(_WIN32)
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
#elif defined(__ANDROID__)
return true;
#else
return false;
#endif
@@ -174,10 +123,10 @@ namespace MGITest {
// callers). surfaceless is the platform with no window-system dependency at
// all; the surface this file then creates is still a pbuffer, which every
// platform supports and which the amendment to this rule requires as the
// fallback shape on desktop. Android instead supplies an AImageReader
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// driver that consults them directly cannot reintroduce the dependency
// behind EGL's back.
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
// for Android, so no device path is affected.
void EnsureHeadlessPlatform() {
#if defined(__linux__) && !defined(__ANDROID__)
static bool done = false;
@@ -265,21 +214,12 @@ namespace MGITest {
return 6;
}
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
#elif defined(__ANDROID__)
if (!CreateImageReaderWindow()) {
outReason = "failed to create the Android AImageReader integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
#endif
} else {
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
}
if (surface == EGL_NO_SURFACE) {
#if defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
: "eglCreatePbufferSurface failed");
return 6;
@@ -608,8 +548,6 @@ namespace MGITest {
DestroyWindow(g_testWindow);
g_testWindow = nullptr;
}
#elif defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
m_context = nullptr;
m_surface = nullptr;
@@ -14,11 +14,11 @@
// inspects backend state - both bugs this module pins were invisible to
// state-level assertions and visible only in pixels.
//
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
// boundary the cross-frame scenarios need to be real).
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
// cross-frame scenarios need to be real).
//
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
// initialization, so the CMake wiring runs this binary once per backend rather
+1 -7
View File
@@ -31,14 +31,8 @@ namespace {
// silently bound to a workstation's window system is a different
// run from CI's and must be visible as one in the log.
const char* eglPlatform = std::getenv("EGL_PLATFORM");
#if defined(__ANDROID__)
constexpr const char* surfaceKind = "AImageReader window";
#else
constexpr const char* surfaceKind = "pbuffer";
#endif
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
gl.RendererString().c_str(), gl.Width(), gl.Height(),
surfaceKind,
eglPlatform != nullptr ? eglPlatform : "<unset>");
} else if (MGITest::RequireGpu()) {
std::fprintf(stderr,
@@ -374,86 +374,6 @@ namespace MGITest {
glUseProgram(0);
}
// The same texture, bound four times over, varying nothing but `layered` and `layer`.
//
// GL 4.6 core 8.26 (and ES 3.2 8.22, word for word): "If the texture identified by
// texture does not have multiple layers or faces, the entire texture level is bound,
// regardless of the values of layered and layer." REGARDLESS means ignored - not
// clamped, and not an error - so every one of the four rows has to read the same texel
// out of a target that has no layers, including the two rows that name layer 1 on a
// texture whose only layer is 0. DirectGLES used to normalize `layered` and forward
// `layer` verbatim; Adreno honours the bogus layer by leaving the image unit reading
// zero, which is exactly the two rows KHR-GL42.bind_image_texture.single_layer failed.
//
// The bindings are checked back as well, because the fix depends on WHERE the
// normalization happens: the frontend shadow must keep echoing the application's own
// values (gl4cShaderImageLoadStoreTests' CheckBinding compares them exactly), and only
// the backend's driver call may drop the layer.
void RunNonLayerableLayerSweepCase(const TargetKind& kind) {
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
if (program == 0) return;
const GLuint texture = MakeTexture(kind, true);
if (texture == 0) return;
// A multisample texture has no TexSubImage, so MakeTexture leaves it unwritten and
// it is seeded the way the store cases do it - through a dispatch of its own.
const GLuint expected = kind.multisample ? kStoredValue : kFilledValue;
if (kind.multisample) {
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
if (storeProgram == 0) return;
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
glUseProgram(storeProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": seeding the multisample texture errored";
}
const GLuint ssbo = MakeResultBuffer();
glUseProgram(program);
glUniform1i(0, 0);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
// glcBindImageTextureTests' own four rows, in its own order.
struct LayerRow {
GLboolean layered;
GLint layer;
};
static constexpr LayerRow kRows[] = {{GL_TRUE, 1}, {GL_TRUE, 0}, {GL_FALSE, 1}, {GL_FALSE, 0}};
for (const LayerRow& row : kRows) {
const std::string where = std::string(kind.name) +
": layered=" + (row.layered == GL_TRUE ? "TRUE" : "FALSE") +
" layer=" + std::to_string(row.layer);
// Re-zeroed per row, so a row whose binding reads nothing cannot pass on the
// previous row's answer.
const GLuint zero = 0u;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &zero);
glBindImageTexture(0, texture, 0, row.layered, row.layer, GL_READ_ONLY, GL_R32UI);
EXPECT_EQ(FirstGLError(), 0u) << where << ": glBindImageTexture errored";
GLint reportedLayered = -1;
GLint reportedLayer = -1;
glGetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &reportedLayered);
glGetIntegeri_v(GL_IMAGE_BINDING_LAYER, 0, &reportedLayer);
EXPECT_EQ(reportedLayered, row.layered == GL_TRUE ? 1 : 0)
<< where << ": GL_IMAGE_BINDING_LAYERED stopped reporting the application's value";
EXPECT_EQ(reportedLayer, row.layer)
<< where << ": GL_IMAGE_BINDING_LAYER stopped reporting the application's value";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << where << ": the dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), expected)
<< where
<< ": the texel did not come back, so the binding named a layer the texture "
"does not have instead of the whole level";
}
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
std::vector<GLuint> m_buffers;
@@ -515,31 +435,6 @@ namespace MGITest {
#undef MGL_DEFINE_LOAD_CASE
#undef MGL_DEFINE_STORE_CASE
// ---- and the same texture bound four times, varying only layered/layer ---
//
// KHR-GL42.bind_image_texture.single_layer's sweep, on the kinds whose backend target has
// neither layers nor faces. Two of its four rows name layer 1 on a single-layer texture,
// which the spec says is to be ignored outright rather than honoured or rejected - and
// which DirectGLES used to forward to the ES driver as written.
#define MGL_DEFINE_LAYER_SWEEP_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, IgnoresLayerFor##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunNonLayerableLayerSweepCase(Kind); \
}
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2D, kKind2D)
MGL_DEFINE_LAYER_SWEEP_CASE(Texture1D, kKind1D)
MGL_DEFINE_LAYER_SWEEP_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2DMultisample, kKind2DMS)
#undef MGL_DEFINE_LAYER_SWEEP_CASE
// ---- and all of them at once -------------------------------------------
//
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
@@ -1,379 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.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
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Full iterationRP Program 203 golden input/output fixture. The original shader
// consumes deterministic complete textures and uniforms, then its complete
// 512x513 RG16F output image is compared against fixed half-float golden bits.
// This catches both a wrong exposure slot and collateral scratch corruption.
#include <array>
#include <bit>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <sstream>
#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 kSceneWidth = 854;
constexpr int kSceneHeight = 480;
constexpr int kPixelDataWidth = 512;
constexpr int kPixelDataHeight = 513;
constexpr std::size_t kSceneTexelCount =
static_cast<std::size_t>(kSceneWidth) * kSceneHeight;
constexpr std::size_t kPixelDataTexelCount =
static_cast<std::size_t>(kPixelDataWidth) * kPixelDataHeight;
struct Rgba32f {
float r, g, b, a;
};
struct Rg16 {
std::uint16_t r, g;
};
static_assert(sizeof(Rgba32f) == 16);
static_assert(sizeof(Rg16) == 4);
// Captured from the fixed fixture on Adreno 830. These are the exact
// RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal
// comparisons performed by the test.
constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u};
constexpr const char* kCommonSource = R"glsl(
#version 430 core
#extension GL_KHR_shader_subgroup_arithmetic : require
uniform int frameCounter;
uniform float frameTime;
uniform float aspectRatio;
uniform vec2 pixelSize;
uniform float nightVision;
uniform float darknessLightFactor;
uniform sampler2D colortex2;
uniform sampler2D pixelData2D;
layout(rg16f) uniform image2D img_pixelData2D;
float remapSaturate(float x, float e0, float e1) {
return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
}
float GetExposureValue(float luminance) {
float aeCurve = 0.65f;
aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision);
aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f;
float ae = pow(luminance, -aeCurve);
ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f);
ae *= 8.5f;
return ae;
}
)glsl";
constexpr const char* kOriginalMain = R"glsl(
layout(local_size_x = 32, local_size_y = 16) in;
shared vec2 prefixSumCache[32];
void main() {
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
vec2 sampleCoord = texCoord * (1.0f / 64.0f);
sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb,
vec3(0.2125f, 0.7154f, 0.0721f));
vec2 sampleLuminance = vec2(tileExposure, 0.0f);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0f;
barrier();
float avg = prefixSumCache[0].x;
vec2 tileDistance = texCoord * 2.0f - 1.0f;
tileDistance.y /= aspectRatio;
float centerDistance = length(tileDistance);
float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f);
tileExposure = max(7.0E-7f, tileExposure);
float lumaWeight = avg / tileExposure;
lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f);
tileWeight *= lumaWeight;
vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight);
sampleExposure = subgroupInclusiveAdd(sampleExposure);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u) {
float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f);
avgExposure = log2(avgExposure);
float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x);
float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f;
float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f);
avgExposure = mix(prevAvgExposure, avgExposure, exposureTime);
avgExposure = max(exp2(avgExposure), 1.0E-5f);
float exposure = GetExposureValue(avgExposure);
imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f));
}
}
)glsl";
GLuint CompileCompute(const char* mainSource, std::string* error) {
const std::array<const GLchar*, 2> sources = {kCommonSource, mainSource};
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, static_cast<GLsizei>(sources.size()), sources.data(), nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
std::array<char, 8192> log{};
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
std::array<char, 8192> log{};
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteProgram(program);
return 0;
}
return program;
}
std::vector<Rgba32f> MakeSceneInput() {
std::vector<Rgba32f> texels(kSceneTexelCount);
for (int y = 0; y < kSceneHeight; ++y) {
for (int x = 0; x < kSceneWidth; ++x) {
std::uint32_t h = static_cast<std::uint32_t>(x) * 0x9e3779b9u;
h ^= static_cast<std::uint32_t>(y) * 0x85ebca6bu;
h ^= h >> 16u;
h *= 0x7feb352du;
h ^= h >> 15u;
const float noise = static_cast<float>(h & 0xffffu) / 65535.0f;
float base = 0.0002f + noise * 0.075f;
const float dx = static_cast<float>(x - 420);
const float dy = static_cast<float>(y - 4);
base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f);
if (((x + y * 17) % 113) == 0) base += 1.75f;
texels[static_cast<std::size_t>(y) * kSceneWidth + x] =
{base * 0.83f, base * 1.07f, base * 1.31f, 1.0f};
}
}
return texels;
}
std::uint16_t FloatToHalf(float value) {
const std::uint32_t bits = std::bit_cast<std::uint32_t>(value);
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
const std::uint32_t exponent = (bits >> 23u) & 0xffu;
std::uint32_t mantissa = bits & 0x7fffffu;
if (exponent == 0xffu) {
return static_cast<std::uint16_t>(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
}
int halfExponent = static_cast<int>(exponent) - 127 + 15;
if (halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
if (halfExponent <= 0) {
if (halfExponent < -10) return static_cast<std::uint16_t>(sign);
mantissa |= 0x800000u;
const unsigned shift = static_cast<unsigned>(14 - halfExponent);
const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) +
((mantissa >> shift) & 1u);
return static_cast<std::uint16_t>(sign | (rounded >> shift));
}
mantissa += 0xfffu + ((mantissa >> 13u) & 1u);
if ((mantissa & 0x800000u) != 0) {
mantissa = 0;
if (++halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
}
return static_cast<std::uint16_t>(sign | (static_cast<std::uint32_t>(halfExponent) << 10u) |
(mantissa >> 13u));
}
std::vector<Rg16> MakePixelDataInput() {
std::vector<Rg16> texels(kPixelDataTexelCount);
for (std::size_t i = 0; i < texels.size(); ++i) {
texels[i] = {FloatToHalf(0.35f + static_cast<float>(i % 97u) * 0.0025f),
FloatToHalf(-0.45f + static_cast<float>(i % 89u) * 0.01f)};
}
texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)};
return texels;
}
std::vector<Rg16> MakeGoldenOutput() {
std::vector<Rg16> golden = MakePixelDataInput();
golden[0] = kGoldenExposure;
return golden;
}
GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height,
const void* data) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), width, height, 0, format,
type, data);
return texture;
}
void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) {
glUseProgram(program);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, scene);
glUniform1i(glGetUniformLocation(program, "colortex2"), 3);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, pixelData);
glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4);
glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F);
glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0);
glUniform1i(glGetUniformLocation(program, "frameCounter"), 100);
glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f);
glUniform1f(glGetUniformLocation(program, "aspectRatio"),
static_cast<float>(kSceneWidth) / kSceneHeight);
glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight);
glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f);
glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
}
std::vector<Rg16> ReadWholeRgTexture(GLuint texture) {
std::vector<Rg16> texels(kPixelDataTexelCount);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data());
return texels;
}
class IterationRPProgram203Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint stages = 0;
GLint features = 0;
GLint invocations = 0;
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
const GLbitfield required =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
(static_cast<GLbitfield>(features) & required) != required || invocations < 512) {
GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups";
}
std::string error;
m_original = CompileCompute(kOriginalMain, &error);
ASSERT_NE(m_original, 0u) << "original Program 203: " << error;
const std::vector<Rgba32f> scene = MakeSceneInput();
const std::vector<Rg16> pixelData = MakePixelDataInput();
m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data());
m_originalOutput =
MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight,
pixelData.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
}
void TearDown() override {
if (!Ready()) return;
const std::array<GLuint, 2> textures = {m_scene, m_originalOutput};
glDeleteTextures(static_cast<GLsizei>(textures.size()), textures.data());
if (m_original != 0) glDeleteProgram(m_original);
}
GLuint m_original = 0;
GLuint m_scene = 0;
GLuint m_originalOutput = 0;
};
} // namespace
TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) {
if (!Ready()) return;
BindAndDispatch(m_original, m_scene, m_originalOutput);
glFinish();
const std::vector<Rg16> actual = ReadWholeRgTexture(m_originalOutput);
const std::vector<Rg16> expected = MakeGoldenOutput();
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
std::size_t mismatchTexels = 0;
std::size_t firstMismatch = actual.size();
for (std::size_t i = 0; i < actual.size(); ++i) {
if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) {
if (firstMismatch == actual.size()) firstMismatch = i;
++mismatchTexels;
}
}
RecordProperty("program203_output_width", kPixelDataWidth);
RecordProperty("program203_output_height", kPixelDataHeight);
RecordProperty("program203_compared_texels", static_cast<long long>(actual.size()));
RecordProperty("program203_mismatch_texels", static_cast<long long>(mismatchTexels));
std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex
<< actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r
<< ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/'
<< actual.size() << '\n';
if (firstMismatch != actual.size()) {
const std::size_t x = firstMismatch % kPixelDataWidth;
const std::size_t y = firstMismatch / kPixelDataWidth;
ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y
<< ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x"
<< actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r
<< ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched "
<< mismatchTexels << " of " << actual.size() << " texels";
}
EXPECT_EQ(mismatchTexels, 0u);
}
} // namespace MGITest
@@ -8,13 +8,11 @@
//
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
//
// iterationRP hard-sizes the scratch its subgroup prefix scans write through
// prefixSumCache[gl_SubgroupID], and ships that idiom twice: the auto-exposure pass
// declares `shared vec2 prefixSumCache[32]` for a 512-invocation workgroup, and the
// RTW importance warp declares `shared float prefixSumCache[64]` for a 1024-invocation
// one. Both algorithms are width-agnostic; only the static lengths bake in "at most 32
// (respectively 64) subgroups", which every desktop capture satisfies and an 8-lane
// device (lavapipe: 64 and 128 subgroups) does not. DirectVulkan patches exactly that with
// iterationRP's auto-exposure pass declares `shared vec2 prefixSumCache[32]` for a
// 512-invocation workgroup and combines per-subgroup subtotals through
// prefixSumCache[gl_SubgroupID]. The algorithm is width-agnostic; only the static 32
// bakes in "at most 32 subgroups", which every desktop capture satisfies and an 8-lane
// device (lavapipe: 64 subgroups) does not. DirectVulkan patches exactly that with
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
// width) on the modules that match the pack's reduction fingerprint.
//
@@ -49,10 +47,6 @@ namespace MGITest {
constexpr std::uint32_t kInvocationCount = 512u;
// sum of 0..511, exactly representable and associativity-proof in fp32.
constexpr float kExpectedTotal = 130816.0f;
// The RTW warp's shape: 1024 invocations into a 64-entry float scratch.
constexpr std::uint32_t kWideInvocationCount = 1024u;
// sum of 0..1023, likewise exact in fp32.
constexpr float kWideExpectedTotal = 523776.0f;
constexpr const char* kComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
@@ -93,50 +87,6 @@ void main() {
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
// The RTW importance warp's shape: a plain float scan over 1024 invocations
// into a 64-entry scratch. Same idiom, different dimensions - which is exactly
// what a fingerprint pinned to the exposure pass's shape walks past.
constexpr const char* kWideComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(std430, binding = 0) buffer Output {
float total;
uint numSubgroups;
uint maxSubgroupId;
} outputData;
shared float prefixSumCache[64];
void main() {
float importance = float(gl_LocalInvocationID.x);
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) {
outputData.total = prefixSum;
outputData.numSubgroups = gl_NumSubgroups;
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
struct OutputBlock {
@@ -180,7 +130,8 @@ void main() {
"512-invocation workgroup";
}
m_maxInvocations = static_cast<std::uint32_t>(invocations);
m_program = CompileComputeProgram(kComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
glGenBuffers(1, &m_output);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
@@ -230,14 +181,7 @@ void main() {
return program;
}
// Re-poisons the block, compiles the shape under test and runs it once.
OutputBlock Dispatch(const char* source) {
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &poison);
m_program = CompileComputeProgram(source);
EXPECT_NE(m_program, 0u) << m_buildLog;
if (m_program == 0u) return OutputBlock{};
OutputBlock Dispatch() {
glUseProgram(m_program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
@@ -249,13 +193,12 @@ void main() {
GLuint m_program = 0;
GLuint m_output = 0;
std::uint32_t m_maxInvocations = 0;
std::string m_buildLog;
};
} // namespace
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
const OutputBlock block = Dispatch(kComputeSource);
const OutputBlock block = Dispatch();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// The topology diagnostics catch the failure modes by name before the sum does:
@@ -276,27 +219,4 @@ void main() {
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
// The pack's second instance of the same bug, and the one that kept the CI
// retrace red after the exposure pass alone was patched.
TEST_F(IterationRPScratchFixScenario, WideFixtureShapedReductionSumsEveryInvocation) {
if (m_maxInvocations < kWideInvocationCount) {
GTEST_SKIP() << "needs a " << kWideInvocationCount << "-invocation workgroup";
}
const OutputBlock block = Dispatch(kWideComputeSource);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 1023 never reached its store";
EXPECT_GE(block.numSubgroups, 1u);
EXPECT_LE(block.numSubgroups, kWideInvocationCount);
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
"DeriveNumSubgroupsPass exists to repair";
// Without the patch an 8-lane device writes prefixSumCache[64..127] out of
// bounds and this comparison fails.
EXPECT_EQ(block.total, kWideExpectedTotal)
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
} // namespace MGITest
@@ -1,268 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbPrimitiveQueryScenario.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 the two transform feedback queries report for a VERTEX-ONLY capture that
// OVERFLOWS its buffer - the shape of KHR-GL30.transform_feedback.query_vertex_*,
// and the one place where the two targets must disagree:
//
// * GL_PRIMITIVES_GENERATED counts what the capture stage assembled: 4 points.
// * GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN counts what the capture buffers
// took. With room for three vertices, a full buffer stops recording whole
// primitives (GL 4.6 core 13.2.2), so the answer is 3, not 4 and not 6.
//
// Both numbers came from the backend's own GPU counter until the driver underneath
// DirectGLES was caught reporting exactly twice the written count for this shape
// (Adreno 830, vertex-only capture issued right after a large render pass). The
// frontend already computes the desktop-exact number for a capture with no geometry
// stage, so that is what answers PRIMITIVES_WRITTEN there now - and this scenario is
// what pins the value, on every backend, without a device.
//
// The non-overflowing case is the negative control: with room for all four points
// the two targets must AGREE at 4, so a "written" that silently reports the
// generated count cannot pass both cases at once.
#include <cmath>
#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 float kPoison = -1234.0f;
// One vec4 per captured point.
constexpr std::size_t kFloatsPerVertex = 4;
constexpr std::size_t kBytesPerVertex = kFloatsPerVertex * sizeof(float);
// The draw: four points, whichever way the capture buffer is sized.
constexpr GLsizei kDrawnPoints = 4;
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// Vertex-only capture program - no geometry stage, so nothing amplifies and the
// primitives written are the primitives drawn (up to the buffer's capacity).
GLuint BuildCaptureProgram(std::string* log) {
const std::string vertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
const char* varying = "vs_out_value";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class XfbPrimitiveQueryScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string log;
m_program = BuildCaptureProgram(&log);
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
// Vertex i is (i, i+1, i+2, i+3), so a record that landed in the wrong slot
// is as visible as one that never landed at all.
float vertices[kDrawnPoints * kFloatsPerVertex] = {};
for (int point = 0; point < kDrawnPoints; ++point) {
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
vertices[static_cast<std::size_t>(point) * kFloatsPerVertex + component] =
static_cast<float>(point) + static_cast<float>(component);
}
}
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenQueries(2, m_queries);
ASSERT_NE(m_queries[0], 0u);
ASSERT_NE(m_queries[1], 0u);
}
void TearDown() override {
if (!Ready()) return;
glDeleteQueries(2, m_queries);
glBindVertexArray(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
glUseProgram(0);
ScenarioTest::TearDown();
}
// A capture buffer with room for exactly `vertexCapacity` records, poisoned so
// that "captured nothing" is legible, bound to capture point 0.
GLuint MakeCaptureBuffer(std::size_t vertexCapacity) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
const std::vector<float> poison(vertexCapacity * kFloatsPerVertex, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER,
static_cast<GLsizeiptr>(vertexCapacity * kBytesPerVertex), poison.data(),
GL_DYNAMIC_DRAW);
return buffer;
}
// ONE capture span, four points, with both query targets open across it - the
// order KHR-GL30.transform_feedback.query_vertex_interleaved_test uses: the
// queries wrap the whole span, never the other way round.
void RunQueriedSpan(GLuint* written, GLuint* generated) {
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kDrawnPoints);
glEndTransformFeedback();
glEndQuery(GL_PRIMITIVES_GENERATED);
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
*written = 0xFFFFFFFFu;
*generated = 0xFFFFFFFFu;
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, written);
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, generated);
}
// The capture record at slot `point` must be the vertex the draw fetched there.
static ::testing::AssertionResult CapturedVertexIs(const float* record, int point) {
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
const float expected = static_cast<float>(point) + static_cast<float>(component);
const float got = record[component];
// isfinite first: every ordered comparison against a NaN is false, so a
// pair of one-sided range tests REPORTS SUCCESS for uninitialised storage
// that happens to read as NaN.
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
return ::testing::AssertionFailure()
<< "point " << point << " component " << component << " is " << got << ", expected "
<< expected << (got == kPoison ? " (the capture never reached these bytes)" : "");
}
}
return ::testing::AssertionSuccess();
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_queries[2] = {0, 0};
};
// The negative control: the buffer holds every point the draw produces, so both
// targets must report the same 4. A "written" that is really the generated count
// passes this case and fails the next one; a "written" that is really zero fails
// this one.
TEST_F(XfbPrimitiveQueryScenario, ACaptureThatFitsReportsEveryPrimitiveOnBothTargets) {
if (!Ready()) GTEST_SKIP();
const GLuint captureBuffer = MakeCaptureBuffer(kDrawnPoints);
GLuint written = 0;
GLuint generated = 0;
RunQueriedSpan(&written, &generated);
EXPECT_EQ(written, 4u);
EXPECT_EQ(generated, 4u);
std::vector<float> readback(kDrawnPoints * kFloatsPerVertex, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kDrawnPoints * kBytesPerVertex), readback.data());
for (int point = 0; point < kDrawnPoints; ++point) {
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
point));
}
glDeleteBuffers(1, &captureBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The pin: four points into a buffer sized for three. The fourth is not written, so
// the two targets part ways at 3 and 4 - the exact pair
// KHR-GL30.transform_feedback.query_vertex_interleaved_test checks, and the pair the
// Adreno driver counter got wrong (it answered 6).
TEST_F(XfbPrimitiveQueryScenario, AnOverflowingVertexOnlyCaptureStopsWritingAtTheBufferCapacity) {
if (!Ready()) GTEST_SKIP();
constexpr std::size_t kCapacityVertices = 3;
const GLuint captureBuffer = MakeCaptureBuffer(kCapacityVertices);
GLuint written = 0;
GLuint generated = 0;
RunQueriedSpan(&written, &generated);
EXPECT_EQ(written, 3u) << "the capture buffer holds " << kCapacityVertices << " points";
EXPECT_EQ(generated, 4u) << "every point the draw assembled is generated, capacity or not";
// The three records that DID fit are the first three points, in order: an
// overflow truncates the capture, it does not scramble or drop what preceded it.
std::vector<float> readback(kCapacityVertices * kFloatsPerVertex, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kCapacityVertices * kBytesPerVertex), readback.data());
for (int point = 0; point < static_cast<int>(kCapacityVertices); ++point) {
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
point));
}
glDeleteBuffers(1, &captureBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
} // namespace
} // namespace MGITest
-26
View File
@@ -328,7 +328,6 @@ namespace MobileGL {
// transform feedback counter cannot see them - nothing was being captured.
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
m_transformFeedbackPausedPrimitiveCounter += primitives;
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
return m_transformFeedbackPausedPrimitiveCounter;
@@ -343,30 +342,8 @@ namespace MobileGL {
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives;
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
// What a GL_PRIMITIVES_GENERATED query counts over its span: every primitive the
// capture stage assembled, including the ones a paused span discarded (those are
// generated but never written). Kept as its own running total rather than derived
// from the input counter above, which BeginTransformFeedback resets per span while
// a query may cover several of them.
Uint64 GetTransformFeedbackGeneratedCounter() const {
return m_transformFeedbackGeneratedPrimitiveCounter;
}
// Capture draws whose written-primitive count the CPU accounting reproduced
// exactly, and the subset it could not: a program with a geometry stage amplifies
// by whatever the shader emits, which only the driver's own counter knows. The
// transform feedback queries diff both over their span to decide whether the CPU
// delta may stand in for the backend's GPU result (GL_Query.cpp).
void AddTransformFeedbackAccountedCaptureDraw() { ++m_transformFeedbackAccountedCaptureDraws; }
Uint64 GetTransformFeedbackAccountedCaptureDraws() const {
return m_transformFeedbackAccountedCaptureDraws;
}
void AddTransformFeedbackGeometryCaptureDraw() { ++m_transformFeedbackGeometryCaptureDraws; }
Uint64 GetTransformFeedbackGeometryCaptureDraws() const {
return m_transformFeedbackGeometryCaptureDraws;
}
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
@@ -462,9 +439,6 @@ namespace MobileGL {
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
Uint64 m_transformFeedbackGeneratedPrimitiveCounter = 0;
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
// Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState {
@@ -48,7 +48,6 @@ namespace MobileGL {
m_dirtyRects.resize(requiredLevelCount);
m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
}
m_texelSizes[level] = input.texelSize;
@@ -80,9 +79,6 @@ namespace MobileGL {
m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit();
// Same story for the requested-format tag: a respecified level is whatever this
// call asked for, and the compressed entry points re-arm it right afterwards.
m_requestedCompressedFormats[level] = GL_NONE;
}
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
@@ -114,16 +110,6 @@ namespace MobileGL {
return m_compressedData[level].data();
}
void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) {
if (level >= m_requestedCompressedFormats.size()) return;
m_requestedCompressedFormats[level] = internalFormat;
}
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
return m_requestedCompressedFormats[level];
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
if (levelCount >= m_data.size()) return;
@@ -134,7 +120,6 @@ namespace MobileGL {
m_dirtyRects.resize(levelCount);
m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount);
m_requestedCompressedFormats.resize(levelCount);
}
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -96,18 +96,6 @@ namespace MobileGL {
SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const;
// The compressed internalformat the application ASKED for, which is not the same
// question as the one above: the six generic GL_COMPRESSED_* enums let the
// implementation choose, MobileGL chooses uncompressed storage, and the level is
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
// points that must refuse a compressed image outright (glClearTexImage /
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
// recorded separately. Set right after AllocateLevel, which clears it.
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
// GL_NONE when the level was not requested with a compressed internalformat.
GLenum GetRequestedCompressedFormat(Uint level) const;
protected:
// Insert one clamped, non-empty write box, keeping the list disjoint
// and bounded (see kMaxDirtyRects).
@@ -127,7 +115,6 @@ namespace MobileGL {
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
};
} // namespace GLState
} // namespace MG_State
@@ -111,16 +111,6 @@ namespace MobileGL {
return m_storage[targetIndex].MapCompressedData(level);
}
void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid");
m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat);
}
GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid");
return m_storage[targetIndex].GetRequestedCompressedFormat(level);
}
protected:
Array<MipmapStorage, TargetCount> m_storage;
};
@@ -373,18 +373,6 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0};
@@ -220,15 +220,6 @@ namespace MobileGL::MG_State::GLState {
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
// The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
// answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
// MipmapStorage. Only the entry points GL forbids on a compressed image read it.
virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) = 0;
// GL_NONE when the level was not requested with a compressed internalformat.
virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const = 0;
};
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
@@ -295,9 +286,6 @@ namespace MobileGL::MG_State::GLState {
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -96,18 +96,6 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -39,10 +39,6 @@ namespace MobileGL {
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -1,167 +0,0 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.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 gate on the gl_BaseInstance indirect lowering in
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
// other graphics stages.
//
// Emitting the block on such a driver does not make it work. The driver refuses the program at
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
// driver's, the application is told the program linked and then every draw with it renders
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
// per-command baseInstance of an indirect draw.
//
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
using MobileGL::Bool;
using MobileGL::String;
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
namespace {
// The capability block is a process-global the backend fills in at init; restore whatever
// was there so ordering between this suite and any other that touches it cannot matter.
struct ScopedGLESCapabilitiesOverride {
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
MobileGL::MG_External::GLESCapabilities saved;
};
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
// gl_BaseInstance to a Private global.
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
int instance = gl_InstanceID + mg_BaseInstanceLowered;
gl_Position = vec4(float(instance));
}
)";
} // namespace
// One block is all the indirect view needs, so the predicate is a >= 1 test.
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
}
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
// what produces the unlinkable program. Unusable, not clamped up to one.
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
}
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
<< rewritten;
}
// The bug this gate exists for. The block must not appear at all - not at a different binding,
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
// the whole program unlinkable, and the frontend never surfaces that failure.
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "buffer"));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
// Nothing reads the word index any more, so nothing may declare it either - its presence is
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
}
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
// what the non-indirect draw entry points already write.
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
// The global declaration must be gone; leaving it would shadow the define.
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
}
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
// twice from every non-indirect draw, which is worse than not rebasing at all.
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
}
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
// uniforms with no storage block behind them and must still be promoted on such a driver.
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String source = R"(#version 310 es
highp int mg_DrawID;
highp int mg_BaseVertex;
void main() {
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
}
)";
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
}
@@ -16,22 +16,5 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_executable(
BaseInstanceInjectionTest
BaseInstanceInjectionTest.cpp
)
target_include_directories(BaseInstanceInjectionTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
BaseInstanceInjectionTest 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)
@@ -21,7 +21,6 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
namespace {
@@ -551,55 +550,3 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
}
// --- GL_OES_viewport_array directive -------------------------------------------------------------
// SPIRV-Cross prints gl_ViewportIndex bare and requests nothing for it, and ESSL has no core
// spelling at any version - so without this directive the stage fails to compile, the program is
// marked unusable and every draw made with it silently renders nothing.
TEST(RequestViewportArrayExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
const String out = RequestViewportArrayExtension(source, true);
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_OES_viewport_array : require\n")) << out;
}
// Never speculatively: ARM's compiler hard-errors on an `#extension` naming a string the driver
// does not advertise, so the caller's "not needed" answer has to be honoured exactly. A driver
// without the extension gets the LowerViewportIndexPass fallback instead.
TEST(RequestViewportArrayExtensionTest, NotNeededMeansNotEmitted) {
const String source = R"(#version 320 es
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
EXPECT_EQ(RequestViewportArrayExtension(source, false), source);
}
TEST(RequestViewportArrayExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_OES_viewport_array : require
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
const String out = RequestViewportArrayExtension(source, true);
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_OES_viewport_array"), 1u) << out;
}
// The two image directives and this one share the insertion point, so a shader that needs both
// must end up with both - and with #version still first.
TEST(RequestViewportArrayExtensionTest, CoexistsWithTheImageFormatDirective) {
const String source = R"(#version 320 es
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestViewportArrayExtension(RequestExtendedImageFormats(source, true), true);
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
}
@@ -34,17 +34,6 @@ namespace {
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. The vertex and fragment pnames are ES 3.1,
// but the tessellation and geometry ones only exist from ES 3.2 on, so asking for them
// on an older context raises GL_INVALID_ENUM - the same shape as the buffer-texture and
// anisotropy probes. The "queried" flags are what pin that gating; the "raises error"
// knob is what pins the drain.
GLint maxTessControlSsboBlocks = 6;
GLint maxTessEvaluationSsboBlocks = 7;
GLint maxGeometrySsboBlocks = 8;
GLint maxFragmentSsboBlocks = 9;
bool tessAndGeometrySsboBlocksQueried = false;
bool perStageSsboBlockQueryRaisesError = false;
GLfloat minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6;
@@ -122,30 +111,7 @@ namespace {
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
switch (pname) {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxVertexSsboBlocks;
}
break;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxFragmentSsboBlocks;
}
break;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessControlSsboBlocks;
break;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessEvaluationSsboBlocks;
break;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxGeometrySsboBlocks;
*data = g_fake.maxVertexSsboBlocks;
break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms;
@@ -434,10 +400,6 @@ namespace {
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
MobileGL::MG_External::GLESCapabilities caps;
caps.GLESVersion = {3, 1, 0};
// The probe reads its vertex storage-block gate from caps rather than re-querying the
// driver (FillInGLESCapabilities resolves the per-stage limits before calling it), so a
// caps struct handed to the probe directly has to carry what the fake reports.
caps.MaxVertexShaderStorageBlocks = g_fake.maxVertexSsboBlocks;
return caps;
}
@@ -565,83 +527,6 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS probes. These decide whether an application is
// told it may declare a storage block in a graphics stage, and on a driver that cannot serve one
// a wrong answer is not a cosmetic mis-report: the program is built, the driver refuses it at
// link time, the frontend reports LINK_STATUS true anyway, and every draw with it renders
// nothing. A Mali-G925-Immortalis reports 0 for vertex, both tessellation stages and geometry.
TEST(PerStageStorageBlockCapabilities, TakesTheDriverValuesAndGatesTessAndGeometryOnEs32) {
const auto funcs = MakeFakeGLESFunctions();
// ES 3.1: the tessellation and geometry pnames do not exist, so they must not be asked for
// and the stages must report the spec minimum of 0 rather than a hopeful driver number.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_EQ(es31Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es31Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_EQ(es31Caps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_FALSE(g_fake.tessAndGeometrySsboBlocksQueried);
// ES 3.2: all five are real pnames and all five driver values must come through verbatim.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_EQ(es32Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es32Caps.MaxTessControlShaderStorageBlocks, g_fake.maxTessControlSsboBlocks);
EXPECT_EQ(es32Caps.MaxTessEvaluationShaderStorageBlocks, g_fake.maxTessEvaluationSsboBlocks);
EXPECT_EQ(es32Caps.MaxGeometryShaderStorageBlocks, g_fake.maxGeometrySsboBlocks);
EXPECT_EQ(es32Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_TRUE(g_fake.tessAndGeometrySsboBlocksQueried);
}
// Zero has to survive the round trip intact. It is the answer that matters most - it is what
// ARM's driver actually reports - so a probe that silently substituted a floor would put the
// bug straight back.
TEST(PerStageStorageBlockCapabilities, AZeroFromTheDriverIsReportedAsZero) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.glesMinorVersion = 2;
g_fake.maxVertexSsboBlocks = 0;
g_fake.maxTessControlSsboBlocks = 0;
g_fake.maxTessEvaluationSsboBlocks = 0;
g_fake.maxGeometrySsboBlocks = 0;
g_fake.maxFragmentSsboBlocks = 16;
MobileGL::MG_External::GLESCapabilities maliLikeCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(maliLikeCaps, funcs));
EXPECT_EQ(maliLikeCaps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxFragmentShaderStorageBlocks, 16);
}
// A rejected query must leave no error behind for the application's first glGetError to find,
// and must fall back to the spec minimums rather than to whatever the untouched out-param held.
TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheSpecMinimums) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.perStageSsboBlockQueryRaisesError = true;
g_fake.maxVertexSsboBlocks = 12;
g_fake.maxFragmentSsboBlocks = 12;
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_EQ(caps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(caps.MaxFragmentShaderStorageBlocks, 4);
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
}
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions();
-223
View File
@@ -19,7 +19,6 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
@@ -120,56 +119,6 @@ namespace {
g_stubResultObtainable = true;
g_stubResultNs = 0;
}
// Stub backend transform feedback primitive queries. g_stubXfbQuerySupported = false
// models a backend with no GPU counter at all (null handle), which is what leaves the
// frontend's CPU accounting as the only source; g_stubResultNs is what the "driver"
// would answer when its query IS read, deliberately set to a value the CPU accounting
// never produces so the two sources are told apart.
Int g_stubXfbBeginCount = 0;
Int g_stubXfbEndCount = 0;
Bool g_stubXfbQuerySupported = true;
MG_Backend::BackendQueryHandle StubBeginXfbPrimitivesQuery(Bool) {
if (!g_stubXfbQuerySupported) {
return nullptr;
}
++g_stubXfbBeginCount;
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x53));
}
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
void InstallStubBackendXfbQueries() {
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
backendGL.EndXfbPrimitivesQuery = StubEndXfbPrimitivesQuery;
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
backendGL.GetQueryResult64 = StubGetQueryResult64;
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
// Off by default: the tests that exercise the DirectGLES preference turn it on.
backendGL.PrefersCpuXfbPrimitiveAccounting = false;
g_stubXfbBeginCount = 0;
g_stubXfbEndCount = 0;
g_stubXfbQuerySupported = true;
g_stubDeleteCount = 0;
g_stubResultAvailable = true;
g_stubResultObtainable = true;
g_stubResultNs = 0;
}
// What AccountTransformFeedbackPrimitives (GL_Drawing.cpp) records for one captured
// draw, without needing a draw: `assembled` primitives came out of the vertex stage
// and `written` of them fitted in the capture buffers (they differ once the buffers
// overflow, which is the whole point of PRIMITIVES_WRITTEN).
void SimulateAccountedCaptureDraw(Uint64 assembled, Uint64 written, Bool throughGeometryStage = false) {
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(assembled);
if (throughGeometryStage) {
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(written);
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
}
} // namespace
class QueryTest : public ::testing::Test {
@@ -499,178 +448,6 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The two transform feedback targets count different things and must therefore read
// different counters: PRIMITIVES_WRITTEN what the capture buffers took, PRIMITIVES_GENERATED
// every primitive the capture stage assembled - including the ones a paused span threw away,
// which are generated but never written. Answering both from the written counter (as the
// fallback used to) reports the clamped number as the generated one.
TEST_F(QueryTest, TransformFeedbackQueryTargetsReadTheirOwnCounter) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
g_stubXfbQuerySupported = false; // no GPU counter: the CPU accounting is the only source
GLuint ids[2] = {0, 0};
MG_Impl::GLImpl::GenQueries(2, ids);
ASSERT_NE(ids[0], 0u);
ASSERT_NE(ids[1], 0u);
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, ids[0]);
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, ids[1]);
// Four points assembled into a buffer with room for three.
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
// ...and two more points assembled while the span was paused: generated, never written.
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(2);
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED);
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
GLuint written = 0;
GLuint generated = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &written);
MG_Impl::GLImpl::GetQueryObjectuiv(ids[1], GL_QUERY_RESULT, &generated);
EXPECT_EQ(written, 3u);
EXPECT_EQ(generated, 6u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(2, ids);
}
// A query span that captured nothing at all reads zero from the CPU accounting rather than
// the unsigned wrap-around a bare End-minus-Begin subtraction produces the moment the
// snapshot is not below the counter (GetQueryObjectuiv would hand the app 4294967295).
TEST_F(QueryTest, AnEmptyTransformFeedbackSpanReadsZero) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
g_stubXfbQuerySupported = false;
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
GLuint result = 123u;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 0u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
// The DirectGLES preference: for a capture the frontend counted exactly - every draw
// accounted, none of them amplified by a geometry stage - the CPU number is the
// desktop-exact one and the ES driver's PRIMITIVES_WRITTEN counter is not consulted, even
// though the backend query ran. The backend query object is released at EndQuery instead of
// being left to a result read that will never come.
TEST_F(QueryTest, VertexOnlyCaptureSpansPreferTheCpuPrimitiveAccounting) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
g_stubResultNs = 6; // what the driver's counter would have said - twice the truth
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
EXPECT_EQ(g_stubXfbBeginCount, 1);
EXPECT_EQ(g_stubXfbEndCount, 1);
EXPECT_EQ(g_stubDeleteCount, 1); // ended, then released - not leaked
GLint available = -1;
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
EXPECT_EQ(available, 1);
GLuint result = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 3u);
EXPECT_EQ(g_stubDeleteCount, 1); // the read had no handle left to release
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
EXPECT_EQ(g_stubDeleteCount, 1);
}
// The regression gate for that preference: a capture fed by a geometry stage writes whatever
// the shader emits, which the CPU accounting cannot model, so the backend's counter stays the
// answer and its handle survives EndQuery to be read later.
TEST_F(QueryTest, AGeometryStageCaptureKeepsTheBackendPrimitiveResult) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
g_stubResultNs = 9; // the amplified count only the driver knows
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
SimulateAccountedCaptureDraw(/*assembled=*/1, /*written=*/1, /*throughGeometryStage=*/true);
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
EXPECT_EQ(g_stubDeleteCount, 0); // still to be read
GLuint result = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 9u);
EXPECT_EQ(g_stubDeleteCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
// The other half of that gate: the instanced, indirect and multi-draw entry points never
// reach the CPU accounting, so a span made of those moves no counter at all. Its delta would
// be zero, which is not "nothing was written" - it is "nothing was counted" - and the
// backend's result has to stand.
TEST_F(QueryTest, ACaptureSpanTheAccountingNeverSawKeepsTheBackendResult) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
g_stubResultNs = 12;
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
GLuint result = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 12u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
// GL_PRIMITIVES_GENERATED counts primitives whether or not a capture is active, while the
// CPU accounting only ever sees capture draws - so the preference above deliberately does
// not extend to that target, whatever the backend asked for.
TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference) {
const ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendXfbQueries();
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
g_stubResultNs = 7;
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
ASSERT_NE(id, 0u);
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, id);
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED);
EXPECT_EQ(g_stubDeleteCount, 0);
GLuint result = 0;
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
EXPECT_EQ(result, 7u);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(1, &id);
}
// Environment-agnostic property test for the env -> ConfigLoader -> Features
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
// this test process, MG_ConfigLoader::Init must have parsed it with the
-16
View File
@@ -365,13 +365,6 @@ TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
// probed cap and falls back to the struct default would surface as "binding = 7".
caps.MaxShaderStorageBufferBindings = 13;
// The indirect lowering reads its baseInstance through a storage block declared in the
// VERTEX stage, which is optional in both APIs and which the GLESCapabilities default
// (0, the spec minimum) therefore denies. This suite is pinning the shape of that
// lowering, so it has to describe a driver that can actually have it - see
// VertexStageStorageBlockUsable and the BaseInstanceInjectionGate suite for the
// zero case.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -410,9 +403,6 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
// See RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance: without a vertex-stage
// storage block there is no word index to be one-based about.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -438,10 +428,6 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
// Set explicitly even though the assertions below would also hold on the degraded path:
// this case is about a CONFORMING driver leaving gl_InstanceID alone, and it would be a
// silent weakening for it to be exercising the no-storage-block fallback instead.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -456,8 +442,6 @@ void main() {
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
// The indirect view is present on this driver, so the fallback must NOT have fired.
EXPECT_NE(rewritten.find("buffer mg_IndirectParams"), MobileGL::String::npos);
}
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
@@ -4,13 +4,10 @@ add_executable(
SpirvPassTest
SpirvPassTest.cpp
DeriveNumSubgroupsTest.cpp
FixIterationRPBarrierTest.cpp
FixIterationRPSubgroupScratchTest.cpp
EmulateSubgroupsTest.cpp
DemoteFloat64Test.cpp
FlattenXfbInterfaceBlocksTest.cpp
LowerViewportIndexTest.cpp
ClampMultisampleFetchTest.cpp
)
target_include_directories(SpirvPassTest PRIVATE
@@ -1,383 +0,0 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.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
//
// ClampMultisampleFetchPass exists because MobileGL advertises one multisample ceiling and the ES
// driver underneath delivers another. GL 4.6 core table 23.53 forces GL_MAX_SAMPLES and
// GL_MAX_INTEGER_SAMPLES up to 4; Adreno and Mali back an integer multisample texture with ONE
// sample, and DirectGLES quietly allocates that (ClampSamplesToBackendSupport). A CTS shader that
// bakes in `texelFetch(usampler2DMS, coord, 3)` - which is what
// KHR-GL33/40/41.texture_swizzle.functional_* and KHR-GLxx.texture_size_promotion.functional do -
// then reads a sample the storage does not have.
//
// So what has to hold is per-fetch and per-category at once: the squeezed category's Sample
// operand must come back in range, a category that is not squeezed must be untouched, a module
// with no multisampled image at all must come out byte for byte as it went in, and every result
// must still be a valid module. Real GLSL through the same glslang path the backends use, for the
// same reason LowerViewportIndexTest.cpp does it: what matters is what glslang actually emits.
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <map>
#include <string>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// GLSL.std.450 instruction number (see 3rdparty/glslang/SPIRV/GLSL.std.450.h). The signed
// minimum, which is what a GLSL `int` sample index asks for.
constexpr Uint32 kGlslStd450SMin = 39u;
// What MobileGL tells the application GL_MAX_SAMPLES / GL_MAX_INTEGER_SAMPLES are, i.e.
// GL_Getter's kFrontendMaxSamples floor. Each test supplies its own backend-real ceilings
// against it; Adreno and Mali's Immortalis-G925 both really answer 1 for integer formats.
constexpr Int32 kAdvertisedMaxSamples = 4;
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> CompileFragment(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_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_FRAGMENT_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();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
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);
}
// OpImageFetch words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate,
// 5 the optional image-operands mask, 6.. the ids that mask asks for.
struct ImageFetch {
Uint32 resultId = 0u;
Uint32 imageId = 0u;
Uint32 mask = 0u;
Vector<Uint32> maskOperandIds;
};
Vector<ImageFetch> CollectImageFetches(const Vector<Uint32>& spirv) {
Vector<ImageFetch> fetches;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpImageFetch || wordCount < 5u) return;
ImageFetch fetch{};
fetch.resultId = words[2];
fetch.imageId = words[3];
if (wordCount > 5u) {
fetch.mask = words[5];
for (Uint32 word = 6u; word < wordCount; ++word) {
fetch.maskOperandIds.push_back(words[word]);
}
}
fetches.push_back(fetch);
});
return fetches;
}
// OpExtInst words: 0 opcode/count, 1 result type, 2 result id, 3 set, 4 instruction number,
// 5.. the operand ids.
struct ExtInst {
Uint32 resultId = 0u;
Uint32 instructionNumber = 0u;
Vector<Uint32> operandIds;
};
Vector<ExtInst> CollectExtInsts(const Vector<Uint32>& spirv) {
Vector<ExtInst> extInsts;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpExtInst || wordCount < 5u) return;
ExtInst extInst{};
extInst.resultId = words[2];
extInst.instructionNumber = words[4];
for (Uint32 word = 5u; word < wordCount; ++word) {
extInst.operandIds.push_back(words[word]);
}
extInsts.push_back(extInst);
});
return extInsts;
}
std::map<Uint32, Uint32> CollectScalarConstants(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> values;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount == 4u) values[words[2]] = words[3];
});
return values;
}
// The one fetch carrying an explicit Sample operand. glslang emits Sample on its own for a
// multisample texelFetch - there is no texelFetchOffset for a multisampled sampler - so the
// sample id is the mask's first and only operand.
const ImageFetch* FindSampleCarryingFetch(const Vector<ImageFetch>& fetches) {
for (const ImageFetch& fetch : fetches) {
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Sample)) != 0u) {
return &fetch;
}
}
return nullptr;
}
const ImageFetch* FindLodCarryingFetch(const Vector<ImageFetch>& fetches) {
for (const ImageFetch& fetch : fetches) {
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Lod)) != 0u) {
return &fetch;
}
}
return nullptr;
}
// KHR-GL4x.texture_swizzle.functional's integer multisample read in miniature: the sample
// index is the advertised GL_MAX_INTEGER_SAMPLES - 1, baked in as a literal, which is exactly
// the value the one-sample allocation underneath cannot answer. The plain sampler2D fetch is
// the negative control - a NON-multisampled image whose Lod operand this pass must not touch.
const char* const kIntegerMultisampleFetch = R"(#version 410 core
uniform usampler2DMS uintMs;
uniform sampler2D plain;
out vec4 fragColor;
void main() {
uvec4 texel = texelFetch(uintMs, ivec2(gl_FragCoord.xy), 3);
vec4 other = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
fragColor = vec4(texel) * 0.5 + other;
}
)";
// The colour class, which real devices squeeze to something above 1 rather than to 1.
const char* const kColorMultisampleFetch = R"(#version 410 core
uniform sampler2DMS colorMs;
out vec4 fragColor;
void main() {
fragColor = texelFetch(colorMs, ivec2(gl_FragCoord.xy), 3);
}
)";
// Every stage on a squeezed device goes through the probe, so the one that declares no
// multisampled image has to come back untouched.
const char* const kNoMultisampleFetch = R"(#version 410 core
uniform sampler2D plain;
out vec4 fragColor;
void main() {
fragColor = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
}
)";
} // namespace
class ClampMultisampleFetchTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the clamped module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
// answer no for a shader that never reads a multisample texture - and yes for the ones that do.
TEST_F(ClampMultisampleFetchTest, TheProbeAnswersOnlyForAMultisampledImage) {
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(plain.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage(plain));
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(integerMs.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(integerMs));
const Vector<Uint32> colorMs = CompileFragment(kColorMultisampleFetch);
ASSERT_FALSE(colorMs.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(colorMs));
// Runs on every stage of every program on a squeezed device, so it must survive a stage that
// produced no SPIR-V rather than pushing a parse diagnostic for it.
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage({}));
}
// The combined probe answers both gate questions from one parse; it must agree with the
// per-gate probes on the same modules and stay quiet for an empty stage.
TEST_F(ClampMultisampleFetchTest, TheCombinedProbeAgreesWithThePerGateOnes) {
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(integerMs.empty());
const auto msFeatures = ShaderCompiler::ProbeSpirvGateFeatures(integerMs);
EXPECT_TRUE(msFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(msFeatures.WritesViewportIndexOutput);
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(plain.empty());
const auto plainFeatures = ShaderCompiler::ProbeSpirvGateFeatures(plain);
EXPECT_FALSE(plainFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(plainFeatures.WritesViewportIndexOutput);
const auto emptyFeatures = ShaderCompiler::ProbeSpirvGateFeatures({});
EXPECT_FALSE(emptyFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(emptyFeatures.WritesViewportIndexOutput);
}
// The overwhelming majority of modules. Behind the probe they never reach the pass at all, but the
// pass has to be inert for them on its own, or a future caller that forgets the gate silently
// re-serialises every shader in the program.
TEST_F(ClampMultisampleFetchTest, LeavesAModuleWithoutAMultisampledImageUntouched) {
const Vector<Uint32> input = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
EXPECT_EQ(output, input) << Disassemble(output);
}
// The bug itself. GL_MAX_INTEGER_SAMPLES says 4, the texture has one sample, and the shader asks
// for sample 3.
TEST_F(ClampMultisampleFetchTest, ReplacesAnOutOfRangeIntegerSampleWithZero) {
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(input.empty());
const Vector<ImageFetch> before = CollectImageFetches(input);
ASSERT_EQ(before.size(), 2u) << Disassemble(input);
const ImageFetch* sampleBefore = FindSampleCarryingFetch(before);
const ImageFetch* lodBefore = FindLodCarryingFetch(before);
ASSERT_NE(sampleBefore, nullptr) << Disassemble(input);
ASSERT_NE(lodBefore, nullptr) << Disassemble(input);
ASSERT_EQ(sampleBefore->maskOperandIds.size(), 1u);
const std::map<Uint32, Uint32> constantsBefore = CollectScalarConstants(input);
ASSERT_EQ(constantsBefore.count(sampleBefore->maskOperandIds.front()), 1u);
EXPECT_EQ(constantsBefore.at(sampleBefore->maskOperandIds.front()), 3u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
ASSERT_TRUE(Validates(output)) << dis;
const Vector<ImageFetch> after = CollectImageFetches(output);
ASSERT_EQ(after.size(), 2u) << dis;
const ImageFetch* sampleAfter = FindSampleCarryingFetch(after);
ASSERT_NE(sampleAfter, nullptr) << dis;
ASSERT_EQ(sampleAfter->maskOperandIds.size(), 1u) << dis;
// Sample 0 is the only one a one-sample allocation has - and it is a CONSTANT, not a computed
// minimum: at K == 1 there is nothing to compare against. An id that resolves in the constant
// table cannot also be some OpExtInst's result.
const std::map<Uint32, Uint32> constantsAfter = CollectScalarConstants(output);
ASSERT_EQ(constantsAfter.count(sampleAfter->maskOperandIds.front()), 1u) << dis;
EXPECT_EQ(constantsAfter.at(sampleAfter->maskOperandIds.front()), 0u) << dis;
// The float sampler2D in the same module is not multisampled, so its Lod fetch has to come
// through with the same image, the same mask and the same operand.
const ImageFetch* lodAfter = FindLodCarryingFetch(after);
ASSERT_NE(lodAfter, nullptr) << dis;
EXPECT_EQ(lodAfter->imageId, lodBefore->imageId) << dis;
EXPECT_EQ(lodAfter->mask, lodBefore->mask) << dis;
EXPECT_EQ(lodAfter->maskOperandIds, lodBefore->maskOperandIds) << dis;
}
// The same shader on a device whose integer ceiling really is what MobileGL advertises. Nothing is
// out of range, so nothing may be rewritten - and the module must not even be re-serialised.
TEST_F(ClampMultisampleFetchTest, LeavesTheFetchAloneWhenTheCategoryReachesTheAdvertisedMaximum) {
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
EXPECT_EQ(output, input) << Disassemble(output);
}
// A category squeezed to something above 1 cannot be answered with a constant: an index the
// allocation does have must survive, so only the upper bound moves.
TEST_F(ClampMultisampleFetchTest, ClampsAColorSampleWithAMinimum) {
const Vector<Uint32> input = CompileFragment(kColorMultisampleFetch);
ASSERT_FALSE(input.empty());
const Vector<ImageFetch> before = CollectImageFetches(input);
ASSERT_EQ(before.size(), 1u) << Disassemble(input);
ASSERT_EQ(before.front().maskOperandIds.size(), 1u);
const Uint32 originalSampleId = before.front().maskOperandIds.front();
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/2, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
ASSERT_TRUE(Validates(output)) << dis;
const Vector<ImageFetch> after = CollectImageFetches(output);
ASSERT_EQ(after.size(), 1u) << dis;
ASSERT_EQ(after.front().maskOperandIds.size(), 1u) << dis;
const Uint32 clampedSampleId = after.front().maskOperandIds.front();
EXPECT_NE(clampedSampleId, originalSampleId) << dis;
const Vector<ExtInst> extInsts = CollectExtInsts(output);
const ExtInst* minimum = nullptr;
for (const ExtInst& extInst : extInsts) {
if (extInst.resultId == clampedSampleId) minimum = &extInst;
}
ASSERT_NE(minimum, nullptr) << dis;
EXPECT_EQ(minimum->instructionNumber, kGlslStd450SMin) << dis;
ASSERT_EQ(minimum->operandIds.size(), 2u) << dis;
EXPECT_EQ(minimum->operandIds[0], originalSampleId) << dis;
// min(sample, K - 1), i.e. the last sample a two-sample allocation has.
const std::map<Uint32, Uint32> constants = CollectScalarConstants(output);
ASSERT_EQ(constants.count(minimum->operandIds[1]), 1u) << dis;
EXPECT_EQ(constants.at(minimum->operandIds[1]), 1u) << dis;
}
@@ -8,7 +8,6 @@
#include <gtest/gtest.h>
#include <sstream>
#include <string>
#include <vector>
@@ -485,165 +484,52 @@ TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
}
// EliminateFloatEqualsZeroPass re-spells a comparison against 0.0 through GLSL.std.450 FAbs, so
// that no float-equality instruction reaches a driver that gets one wrong. Deciding WHICH
// constants are zero used to read every float constant as though it were 32 bits wide, and on a
// 64-bit constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf
// and every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
// a comparison against 1.0lf became a test against ZERO, and came out true for a uniform holding
// exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
//
// The replacement itself used to be an epsilon ball, `abs(x) < 1e-4`, which called any legitimately
// small value zero: KHR-GL3x.buffer_objects.triangles computes a specular term of ~6e-5 at a large
// render target and rendered black. It is exact now - `abs(x) <= 0.0` / `abs(x) > 0.0` against the
// module's own zero constant - and the tests below pin both halves of that: only a genuine 0.0 is
// matched, and what the compare tests against is the constant the source itself spelled.
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a
// workaround for drivers whose exact float compare misbehaves. Deciding WHICH constants are
// zero used to read every float constant as though it were 32 bits wide, and on a 64-bit
// constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf and
// every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
// a comparison against 1.0lf became an epsilon test against ZERO, and came out true for a
// uniform holding exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
//
// Asserted on the optimized module rather than through a driver, because that is where the
// rewrite happens and its fingerprint there is unambiguous: the rewrite introduces a
// rewrite happens and its fingerprint there is unambiguous: the epsilon form introduces a
// GLSL.std.450 FAbs, and nothing else in these shaders would.
namespace {
String OptimizedDisassembly(const String& source) {
Bool RewritesToAnEpsilonTest(const String& source) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
EXPECT_FALSE(input.empty());
if (input.empty()) return {};
if (input.empty()) return false;
Vector<Uint32> output;
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
return Disassemble(output);
return Disassemble(output).find("FAbs") != String::npos;
}
Bool RewritesToAnAbsoluteValueTest(const String& source) {
return OptimizedDisassembly(source).find("FAbs") != String::npos;
}
String CompareAgainstUsing(const String& type, const String& op, const String& literal) {
String CompareAgainst(const String& type, const String& literal) {
return "#version 430 core\n"
"layout(local_size_x = 1) in;\n"
"buffer Result { int g_result; };\n"
"uniform " + type + " g_0;\n"
"void main() {\n"
" g_result = 0;\n"
" if (g_0 " + op + " " + literal + ") g_result = 1;\n"
" if (g_0 != " + literal + ") g_result = 1;\n"
"}\n";
}
String CompareAgainst(const String& type, const String& literal) {
return CompareAgainstUsing(type, "!=", literal);
}
// Every instruction of a disassembly, split into whitespace-separated tokens, so an operand can
// be identified by position instead of by a substring another opcode might also contain -
// `OpFOrdLessThan` is a prefix of `OpFOrdLessThanEqual`, and those two are the whole difference
// between the epsilon rewrite and the exact one.
Vector<Vector<String>> TokenizedInstructions(const String& disassembly) {
Vector<Vector<String>> instructions;
StringStream lines(disassembly);
String line;
while (std::getline(lines, line)) {
Vector<String> tokens;
StringStream words(line);
String word;
while (words >> word) tokens.push_back(word);
instructions.push_back(tokens);
}
return instructions;
}
// The compare the rewrite leaves behind, e.g. `%22 = OpFOrdLessThanEqual %bool %21 %float_0`,
// or an empty vector if the module has none. These four opcodes are the only ones the pass
// emits and nothing else in these shaders produces one.
Vector<String> FindRewrittenCompare(const String& disassembly) {
for (const Vector<String>& tokens : TokenizedInstructions(disassembly)) {
if (tokens.size() < 6 || tokens[1] != "=") continue;
if (tokens[2] == "OpFOrdLessThanEqual" || tokens[2] == "OpFUnordLessThanEqual" ||
tokens[2] == "OpFOrdGreaterThan" || tokens[2] == "OpFUnordGreaterThan") {
return tokens;
}
}
return {};
}
// Result id of the module's 0.0 constant of the type FAbs produces - the constant the source
// itself spelled - found without assuming what the disassembler names it or how it prints the
// literal.
String FindZeroConstantId(const String& disassembly) {
const Vector<Vector<String>> instructions = TokenizedInstructions(disassembly);
String floatTypeId;
for (const Vector<String>& tokens : instructions) {
if (tokens.size() >= 7 && tokens[2] == "OpExtInst" && tokens[5] == "FAbs") {
floatTypeId = tokens[3];
break;
}
}
if (floatTypeId.empty()) return {};
for (const Vector<String>& tokens : instructions) {
if (tokens.size() < 5 || tokens[2] != "OpConstant" || tokens[3] != floatTypeId) continue;
char* end = nullptr;
const double value = std::strtod(tokens[4].c_str(), &end);
if (end != nullptr && *end == '\0' && value == 0.0) return tokens[0];
}
return {};
}
// The shape the pass promises: the given opcode (either NaN half of it), tested against the
// module's own zero constant rather than against anything this pass invented.
void ExpectComparedAgainstModuleZero(const String& source, const String& orderedOpcode,
const String& unorderedOpcode) {
const String disassembly = OptimizedDisassembly(source);
const Vector<String> compare = FindRewrittenCompare(disassembly);
ASSERT_FALSE(compare.empty()) << "no rewritten compare in the optimized module\n"
<< disassembly;
EXPECT_TRUE(compare[2] == orderedOpcode || compare[2] == unorderedOpcode)
<< "expected " << orderedOpcode << " (or its unordered twin), got " << compare[2] << "\n"
<< disassembly;
const String zeroId = FindZeroConstantId(disassembly);
ASSERT_FALSE(zeroId.empty()) << "the module has no 0.0 constant of the abs() type\n"
<< disassembly;
EXPECT_EQ(compare.back(), zeroId)
<< "the rewrite compares against " << compare.back()
<< " instead of the module's own zero; a synthesized threshold is the epsilon bug\n"
<< disassembly;
}
} // namespace
TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) {
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "1.0LF")))
<< "a double compared against 1.0lf was rewritten into a test against zero";
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("double", "1.0LF")))
<< "a double compared against 1.0lf was rewritten into an epsilon test against zero";
}
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0LF")))
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("double", "0.0LF")))
<< "the rewrite must still fire for a genuine comparison against zero";
}
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "1.0")))
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("float", "1.0")))
<< "a float compared against 1.0 must not be rewritten";
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0")))
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("float", "0.0")))
<< "the 32-bit behaviour this pass shipped with must be preserved exactly";
}
// The pass matches ZERO, not "small". The old constant-is-zero test was `fabs(v) <= 1e-4`, so a
// float compared against exactly 1e-4 was declared a comparison against zero and rewritten into
// `abs(x) >= 1e-4` - a different question from the one the shader asked, against a constant that
// was never zero to begin with.
TEST_F(DemoteFloat64Test, AComparisonAgainstASmallNonZeroLiteralIsLeftAlone) {
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0001")))
<< "a float compared against 1e-4 was treated as a comparison against zero";
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0001LF")))
<< "the 64-bit accessor must judge the constant just as exactly as the 32-bit one";
}
// What replaces the compare, not just that something did. Both properties here are what makes the
// rewrite exact rather than a tolerance, and neither is visible in the FAbs fingerprint above.
TEST_F(DemoteFloat64Test, TheRewriteComparesAbsAgainstTheModulesOwnZero) {
// `x == 0.0` -> `abs(x) <= 0.0`. The equality has to be INSIDE the replacement: with a strict
// `<` and no epsilon left to hide behind, +/-0 would stop comparing equal to zero.
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "==", "0.0"),
"OpFOrdLessThanEqual", "OpFUnordLessThanEqual");
// `x != 0.0` -> `abs(x) > 0.0`, the strict complement of the above.
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "!=", "0.0"), "OpFOrdGreaterThan",
"OpFUnordGreaterThan");
}
@@ -1,206 +0,0 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPBarrierTest.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 <map>
#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;
}
bool HasWorkgroupBarrierImmediatelyBeforeSecondScan(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> uintConstants;
spv::Op previous = spv::Op::OpNop;
Uint32 scanCount = 0u;
bool found = false;
const Uint32* previousWords = nullptr;
Uint32 previousWordCount = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount >= 4u) {
uintConstants[words[2]] = words[3];
}
if (opcode == spv::Op::OpGroupNonUniformFAdd && wordCount >= 6u &&
static_cast<spv::GroupOperation>(words[4]) == spv::GroupOperation::InclusiveScan && ++scanCount == 2u &&
previous == spv::Op::OpControlBarrier && previousWordCount == 4u) {
found =
uintConstants[previousWords[1]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[2]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[3]] == (static_cast<Uint32>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<Uint32>(spv::MemorySemanticsMask::WorkgroupMemory));
}
previous = opcode;
previousWords = words;
previousWordCount = wordCount;
});
return found;
}
constexpr const char* kProgram203RaceShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 sampleLuminance = subgroupInclusiveAdd(
vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
float weight = avg > 0.0 ? float(gl_LocalInvocationIndex + 1u) / avg : 0.0;
vec2 sampleExposure = subgroupInclusiveAdd(vec2(weight, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
if (gl_LocalInvocationIndex == 511u)
outputData.value = sampleExposure;
}
)";
constexpr const char* kAlreadySynchronizedShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 first = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = first;
barrier();
if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = first / 512.0;
barrier();
float avg = prefixSumCache[0].x;
barrier();
vec2 second = subgroupInclusiveAdd(vec2(avg, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = second;
barrier();
if (gl_LocalInvocationIndex == 511u) outputData.value = second;
}
)";
constexpr const char* kForeignSingleScanShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 value = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = value;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = prefixSumCache[0];
}
)";
} // namespace
TEST(FixIterationRPBarrierPass, InsertsWorkgroupBarrierBeforeSecondReduction) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
const Uint32 inputBarrierCount = CountOpcode(input, spv::Op::OpControlBarrier);
EXPECT_FALSE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(CountOpcode(output, spv::Op::OpControlBarrier), inputBarrierCount + 1u);
EXPECT_TRUE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(output));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPBarrierPass, LeavesOtherShapesByteIdentical) {
const Vector<Uint32> input = CompileCompute(kForeignSingleScanShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, LeavesAnAlreadySynchronizedShaderByteIdentical) {
const Vector<Uint32> input = CompileCompute(kAlreadySynchronizedShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, once, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -109,11 +109,10 @@ namespace {
return tools.Validate(spirv);
}
// iterationRP's exposure reduction, as the pack ships it: 32x16 (512
// invocations), subgroupInclusiveAdd on a vec2, and a 32-entry
// gl_SubgroupID-indexed scratch. A second, plainly indexed array rides along
// to prove the patch is surgical.
constexpr const char* kExposureShapedSource = R"(#version 450 core
// iterationRP's reduction fingerprint: 32x16x1, subgroupInclusiveAdd on a
// vec2, and the pack's own 32-entry gl_SubgroupID-indexed scratch. A second,
// plainly indexed array rides along to prove the patch is surgical.
constexpr const char* kIterationRPShapedSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
@@ -142,195 +141,85 @@ void main() {
}
)";
// The pack's OTHER instance of the same bug, which a fingerprint pinned to the
// exposure pass's dimensions walks straight past: the RTW importance warp
// scans a plain float across 1024 invocations into a 64-entry scratch.
constexpr const char* kRtwWarpShapedSource = R"(#version 450 core
// Same scratch idiom, different workgroup shape - NOT iterationRP, so the
// fingerprint must refuse it even though it would break identically.
constexpr const char* kWrongWorkgroupShapeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(local_size_x = 64, local_size_y = 8, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float prefixSumCache[64];
shared vec2 prefixSumCache[32];
void main() {
float importance = float(gl_LocalInvocationID.x) * 0.5;
float prefixSum = subgroupInclusiveAdd(importance);
vec2 v = subgroupInclusiveAdd(vec2(1.0, 0.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
prefixSumCache[gl_SubgroupID] = v;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) outputData.value = prefixSumCache[0];
if (gl_LocalInvocationIndex == 0u)
outputData.value = prefixSumCache[0].x;
}
)";
// A subgroup scan, but the scratch is indexed per invocation rather than per
// subgroup: its size is not a subgroup-count assumption, so it is not ours.
constexpr const char* kInvocationIndexedSource = R"(#version 450 core
// Right shape, but a float scan and a float[32] scratch - not the pack's
// vec2 accumulator signature.
constexpr const char* kWrongElementTypeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 perInvocation[32];
shared float cache[32];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
perInvocation[gl_LocalInvocationIndex & 31u] = v;
float v = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
cache[gl_SubgroupID] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = perInvocation[0].x;
}
)";
// gl_SubgroupID-indexed, but no subgroup scan feeds it and the element type is
// not the pack's float accumulator.
constexpr const char* kNonFloatScratchSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
shared uint tally[32];
void main() {
float scan = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
tally[gl_SubgroupID] = uint(scan);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = tally[0];
}
)";
// gl_SubgroupID-indexed, but masked into range: the declaration is bounded by
// construction, not a subgroup-count assumption, so it is not the pack's bug.
constexpr const char* kMaskedSubgroupIndexSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 bounded[8];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
bounded[gl_SubgroupID & 7u] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = bounded[0].x;
}
)";
// Neither of the pack's shapes: a small per-subgroup array in a 256-invocation
// workgroup, used to prove the width gate keeps EVERY module inert at >= 16 lanes.
constexpr const char* kForeignShapeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 256) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float partial[4];
void main() {
float v = subgroupInclusiveAdd(float(gl_LocalInvocationID.x));
if (gl_SubgroupID < 4u) partial[gl_SubgroupID] = v;
barrier();
if (gl_LocalInvocationID.x == 0u) outputData.value = partial[0];
}
)";
// No subgroup construct at all.
constexpr const char* kSubgroupFreeSource = R"(#version 450 core
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 scratch[32];
void main() {
scratch[gl_LocalInvocationIndex & 31u] = vec2(float(gl_LocalInvocationIndex), 0.0);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = scratch[0].x;
if (gl_LocalInvocationIndex == 0u)
outputData.value = cache[0];
}
)";
} // namespace
TEST(FixIterationRPSubgroupScratchPass, GrowsTheExposureScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kExposureShapedSource);
TEST(FixIterationRPSubgroupScratchPass, GrowsThePacksScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{4u, 32u}));
// lavapipe: 8-lane subgroups over 512 invocations need 64 entries; the
// plainly indexed neighbour must keep its 4.
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{4u, 64u}));
EXPECT_TRUE(Validates(output));
}
// The regression the CI retrace caught: patching only the exposure pass leaves
// this one writing 128 subgroups into 64 entries, and the frame stays wrong.
TEST(FixIterationRPSubgroupScratchPass, GrowsTheRtwWarpScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{64u}));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{128u}));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPSubgroupScratchPass, LeavesPackWidthAssumptionsAloneOnWideDevices) {
// Both shapes are sized for >= 16 lanes (512/16 = 32, 1024/16 = 64), so on
// every such device the modules must pass through byte-identical.
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheIdiom) {
for (const char* source : {kInvocationIndexedSource, kNonFloatScratchSource,
kSubgroupFreeSource, kMaskedSubgroupIndexSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(output, input);
}
}
// A grown array that would not fit the device's shared memory is left alone:
// a pipeline that cannot be created is worse than the pack's own overrun.
// The width gate is what keeps unrelated shaders untouched on the devices the pack
// was written for: at >= 16 lanes nothing is rewritten, whatever its shape.
TEST(FixIterationRPSubgroupScratchPass, LeavesEveryModuleAloneAtThePacksAssumedWidth) {
const Vector<Uint32> input = CompileCompute(kForeignShapeSource);
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u}) {
// >= 16 lanes means at most 32 subgroups: the pack's declared size holds and
// the module must pass through byte-identical.
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, nativeSize, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesGrowthThatWouldNotFitSharedMemory) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 256u, true));
EXPECT_EQ(output, input);
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheFingerprint) {
for (const char* source : {kWrongWorkgroupShapeSource, kWrongElementTypeSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
EXPECT_EQ(output, input);
}
}
TEST(FixIterationRPSubgroupScratchPass, IsIdempotent) {
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, 32768u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, 32768u, true));
EXPECT_EQ(twice, once);
}
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, true));
EXPECT_EQ(twice, once);
}
@@ -1,237 +0,0 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LowerViewportIndexTest.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
//
// LowerViewportIndexPass is the DirectGLES fallback for a driver with no GL_OES_viewport_array.
// The thing it prevents is not a wrong pixel but a missing program: ESSL has no core
// gl_ViewportIndex at any version, SPIRV-Cross prints the identifier bare, and the driver rejects
// the stage - after which DirectGLES binds program 0 and every draw renders nothing while
// GL_LINK_STATUS still answers TRUE. So what has to hold is textual and structural at once: the
// emitted ESSL must stop naming the builtin, the module must stay valid, and gl_Layer - which IS
// core in ESSL 3.20 geometry shaders - must come through untouched.
//
// Real GLSL through the same glslang path the backends use, rather than hand-assembled words, for
// the same reason MG_Test/Pipeline/ViewportIndexReflectionTest.cpp does it: what matters is what
// glslang actually emits for these shaders.
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
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();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
// ESSL 320, i.e. exactly what the DirectGLES transpile asks SPIRV-Cross for.
String Transpile(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
return essl ? essl.value() : String{};
}
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// KHR-GL4x.viewport_array.draw_to_single_layer_with_multiple_viewports' geometry stage in
// miniature: sixteen invocations, each routing its primitive to its own viewport. This is the
// shape that today loses the whole program on a driver without GL_OES_viewport_array.
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_ViewportIndex = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
// Layered rendering, not viewport routing. gl_Layer IS core in ESSL 3.20 geometry shaders, so
// demoting it would break a Minecraft-style cubemap pass that works today.
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
layout(points, invocations = 6) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_Layer = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
// Both at once, which is the case that separates "lowers the right builtin" from "lowers every
// builtin it can reach": KHR-GL4x.viewport_array.draw_multiple_layers writes both.
const char* const kGeometryWritesBoth = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_ViewportIndex = gl_InvocationID;
gl_Layer = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
const char* const kPlainGeometry = R"(#version 410 core
layout(points, invocations = 1) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
} // namespace
class LowerViewportIndexTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the lowered module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
// answer no for a shader that never routes a viewport - and yes for the one that does.
TEST_F(LowerViewportIndexTest, TheProbeAnswersOnlyForAViewportIndexWriter) {
const Vector<Uint32> plain = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
ASSERT_FALSE(plain.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(plain));
const Vector<Uint32> layerOnly = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly);
ASSERT_FALSE(layerOnly.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(layerOnly));
const Vector<Uint32> writer = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
ASSERT_FALSE(writer.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresViewportIndexBuiltin(writer));
// Runs on every stage of every program on a driver without the extension, so it must survive a
// stage that produced no SPIR-V rather than pushing a parse diagnostic for it.
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin({}));
}
// The whole point: the emitted ESSL must stop naming a builtin the language does not have.
TEST_F(LowerViewportIndexTest, DemotesTheBuiltinToAnOrdinaryGlobal) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
ASSERT_FALSE(input.empty());
// Negative control, and the bug itself: untouched, SPIRV-Cross prints gl_ViewportIndex into
// ESSL 320 and asks for no extension to go with it.
const String before = Transpile(input);
EXPECT_TRUE(Contains(before, "gl_ViewportIndex")) << before;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "mg_ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "Private")) << dis;
const String after = Transpile(output);
EXPECT_TRUE(Contains(after, "mg_ViewportIndex")) << after;
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
}
// gl_Layer is core in ESSL 3.20 geometry shaders and layered rendering works on this backend
// today. Lowering it too would trade one silent failure for another.
TEST_F(LowerViewportIndexTest, LeavesGlLayerAlone) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesBoth);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "BuiltIn Layer")) << dis;
const String after = Transpile(output);
EXPECT_TRUE(Contains(after, "gl_Layer")) << after;
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
}
// Every other stage on a driver without the extension goes through this pass too (behind the
// probe), so a module it has nothing to do with must come out saying exactly what it said.
TEST_F(LowerViewportIndexTest, LeavesAModuleWithoutTheBuiltinUntouched) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
ASSERT_FALSE(input.empty());
const String before = Transpile(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "mg_ViewportIndex")) << dis;
EXPECT_EQ(Transpile(output), before);
}
@@ -18,11 +18,6 @@
// errors that guard a parameter-buffer draw.
// * KHR-GL43.compute_shader.api-indirect / .api-program.
// * KHR-GLxx.texture_storage.compressed_data - compressed formats on TEXTURE_3D.
// * KHR-GL32.api.coverage - glFenceSync's condition/flags and glWaitSync's flags/timeout.
// * KHR-GL31.api.coverage - a draw's mode INVALID_ENUM has to outrank MobileGL's own
// no-current-program guard.
// * KHR-GL30.api.coverage - glBlitFramebuffer's mask bits, filter enum and the LINEAR-with-
// depth/stencil rule.
// Plus the indexed-getter parity RC-7b is about: glGetBooleani_v / glGetInteger64i_v /
// glGetFloati_v / glGetDoublei_v must answer every pname glGetIntegeri_v answers.
//
@@ -38,12 +33,10 @@
#include "Init.h"
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
#include <MG_State/GLState/Core.h>
@@ -477,114 +470,4 @@ void main() { g_color = vec4(1); }
EXPECT_EQ(offset, 2048);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// KHR-GL32.api.coverage: glFenceSync and glWaitSync took every argument they were handed and
// reported GL_NO_ERROR for the two calls GL 4.6 core 4.1.2 requires to fail. A rejected
// glFenceSync must also hand back 0 rather than a live handle.
TEST_F(NegativeApiErrorsTest, SyncEntryPointsRejectTheirIllegalArguments) {
DrainErrors();
RunRows({
{"glFenceSync with a condition other than GL_SYNC_GPU_COMMANDS_COMPLETE",
[] { EXPECT_EQ(FenceSync(GL_SYNC_FENCE, 0), nullptr); }, GL_INVALID_ENUM},
{"glFenceSync with nonzero flags", [] { EXPECT_EQ(FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 1), nullptr); },
GL_INVALID_VALUE},
});
// The legal fence still works, and with no backend function table it is the always-signaled
// fallback - which is all this GPU-free suite needs to reach glWaitSync's own checks.
const GLsync sync = FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
ASSERT_NE(sync, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(IsSync(sync), GL_TRUE);
RunRows({
{"glWaitSync with nonzero flags", [&] { WaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); },
GL_INVALID_VALUE},
{"glWaitSync with a finite timeout", [&] { WaitSync(sync, 0, 1000000000ull); }, GL_INVALID_VALUE},
{"glWaitSync with the only legal argument pair", [&] { WaitSync(sync, 0, GL_TIMEOUT_IGNORED); },
GL_NO_ERROR},
});
DeleteSync(sync);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// KHR-GL31.api.coverage's first two calls are glDrawArraysInstanced / glDrawElementsInstanced
// with mode GL_POINTS-1 against a context that has no program and no VAO bound, and they must
// answer GL_INVALID_ENUM. MobileGL's own "there is no current program" guard - which the spec
// does not list as a draw error at all - used to run first and shadowed the enum check with
// GL_INVALID_OPERATION. Nothing here reaches a backend: the mode is rejected before the guard.
TEST_F(NegativeApiErrorsTest, BadPrimitiveModeOutranksTheNoProgramGuard) {
DrainErrors();
// Exactly what the coverage test passes: GL_POINTS is 0, so this is 0xFFFFFFFF.
constexpr GLenum kBadMode = static_cast<GLenum>(GL_POINTS - 1);
RunRows({
{"glDrawArraysInstanced with an unaccepted mode", [] { DrawArraysInstanced(kBadMode, 0, 3, 4); },
GL_INVALID_ENUM},
{"glDrawElementsInstanced with an unaccepted mode",
[] { DrawElementsInstanced(kBadMode, 3, GL_UNSIGNED_INT, nullptr, 4); }, GL_INVALID_ENUM},
{"glDrawArrays with an unaccepted mode", [] { DrawArrays(kBadMode, 0, 3); }, GL_INVALID_ENUM},
{"glDrawElements with an unaccepted mode",
[] { DrawElements(kBadMode, 3, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
{"glMultiDrawArrays with an unaccepted mode",
[] { MultiDrawArrays(kBadMode, nullptr, nullptr, 0); }, GL_INVALID_ENUM},
{"glDrawRangeElements with an unaccepted mode",
[] { DrawRangeElements(kBadMode, 0, 2, 3, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
{"glDrawElementsIndirect with an unaccepted mode",
[] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
GL_INVALID_ENUM},
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION
// that used to win is still raised for the calls it is actually about.
{"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); },
GL_INVALID_OPERATION},
});
}
// KHR-GL30.api.coverage's glBlitFramebuffer sub-check. The frontend passed mask and filter
// straight through, and DirectGLES drains the driver's error queue around the blit so the ES
// rejection never surfaced either - both illegal calls reported GL_NO_ERROR. Every row here
// is rejected before the backend function pointer is reached, which is what lets this
// GPU-free suite run them at all.
TEST_F(NegativeApiErrorsTest, BlitFramebufferRejectsBadMasksAndFilters) {
DrainErrors();
// The bit the coverage test smuggles in: a legal glMapBufferRange flag, not a blit one.
constexpr GLbitfield kForeignBit = GL_MAP_INVALIDATE_BUFFER_BIT;
RunRows({
{"glBlitFramebuffer with a mask bit outside COLOR|DEPTH|STENCIL",
[] {
BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | kForeignBit, GL_NEAREST);
},
GL_INVALID_VALUE},
{"glBlitFramebuffer with a filter that is neither GL_NEAREST nor GL_LINEAR",
[] { BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT, GL_NONE); }, GL_INVALID_ENUM},
{"glBlitFramebuffer of colour+stencil with GL_LINEAR",
[] {
BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_LINEAR);
},
GL_INVALID_OPERATION},
{"glBlitFramebuffer of depth with GL_LINEAR",
[] { BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_DEPTH_BUFFER_BIT, GL_LINEAR); },
GL_INVALID_OPERATION},
// The DSA form has to answer identically.
{"glBlitNamedFramebuffer with a mask bit outside COLOR|DEPTH|STENCIL",
[] {
BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | kForeignBit,
GL_NEAREST);
},
GL_INVALID_VALUE},
{"glBlitNamedFramebuffer with a bad filter",
[] { BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT, GL_NONE); },
GL_INVALID_ENUM},
{"glBlitNamedFramebuffer of depth+stencil with GL_LINEAR",
[] {
BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16,
GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_LINEAR);
},
GL_INVALID_OPERATION},
});
}
} // namespace
+5 -478
View File
@@ -377,40 +377,6 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
}
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points.
// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them
// with uncompressed storage on purpose, so by the time the clear runs the level looks like any
// other RGBA8 image unless the REQUEST was recorded alongside it.
TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) {
GLuint genericTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &genericTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A specific compressed internalformat is refused through the tag the level already carried...
GLuint specificTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &specificTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE,
nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// ...and respecifying the level with an uncompressed format makes it clearable again, because
// AllocateStorage clears both tags.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
@@ -1057,32 +1023,6 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_STENCIL_INDEX is the unsized base format for stencil-only storage, and refusing it as an
// internal format killed the ARB_clear_texture stencil case in its own setup - before it could
// reach the calls it actually tests. The stencil-only transfer format stays paired with
// stencil-only storage in both directions, which is what keeps those clears erroring.
TEST_F(TextureTest, StencilIndexIsATextureInternalFormatPairedOnlyWithStencilStorage) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_STENCIL_INDEX, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE,
nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::StencilIndex8);
// A colour transfer format against stencil storage is still INVALID_OPERATION, so the clear
// the conformance case makes next fails the way it is supposed to.
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// ...and the other direction: GL_STENCIL_INDEX against colour storage stays illegal.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels
// rgba8_format_green/blue upload with them and verify the readback): the single input component
// feeds the named channel, the other color channels default to 0 and alpha to 1.
@@ -1377,56 +1317,6 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 8.11.4 asks a readback for cube completeness and nothing else, so a mip chain whose
// levels BELOW the requested one were never defined is still readable at that level - which is
// exactly the shape ARB_clear_texture's conformance cases build (they define only the level they
// clear). The whole-chain completeness gate used to answer INVALID_OPERATION here.
TEST_F(TextureTest, GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 pixels[] = {
61, 62, 63, 64,
71, 72, 73, 74,
};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 output[sizeof(pixels)] = {};
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 2, GL_RGBA, GL_UNSIGNED_BYTE, output);
EXPECT_EQ(std::memcmp(output, pixels, sizeof(pixels)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The other half of the same rule: loosening the chain-wide check must not let a level that holds
// no image at all through. Level 0 exists as a chain slot once level 2 is defined, but nothing ever
// gave it an image, so it stays INVALID_OPERATION - as does a level past the end of the chain and a
// texture that was never given any image whatsoever.
TEST_F(TextureTest, GetTexImageStillRejectsALevelThatHoldsNoImage) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
Uint8 output[4] = {};
// No image at all yet: the chain carries no levels.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Inside the chain, but never defined.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
// Past the end of the chain.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 3, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
GLuint texture = 0;
GLuint boundTexture = 0;
@@ -1761,56 +1651,6 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The same rule for the 3D entry points, which never recorded the tag at all. Besides the two
// level queries this decides the level's texel BLOCK SIZE, which glCopyImageSubData compares
// against the other endpoint's - an untagged GL_COMPRESSED_RG_RGTC2 array level measured as the
// RG8 storage it resolves to, 2 bytes instead of 16.
TEST_F(TextureTest, TexImage3DAndTexStorage3DTagASpecificCompressedInternalFormat) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 2, 0, GL_RG,
GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE);
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RG_RGTC2));
// 8x8 in 4x4 blocks of 16 bytes each is 64 bytes a layer, and both layers count.
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, 128);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The texel shadow behind the tag keeps the uncompressed storage the format resolves to.
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RG8);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
// glTexStorage3D has the same gap and the same fix; immutable storage plus
// glCompressedTexSubImage3D is the modern way to upload a compressed array texture.
GLuint storageTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &storageTexture);
MG_Impl::GLImpl::TextureStorage3D(storageTexture, 1, GL_COMPRESSED_RG_RGTC2, 8, 8, 2);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storageTexture);
compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE);
imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, 128);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
}
namespace {
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
@@ -3135,58 +2975,6 @@ TEST_F(TextureTest, CtsStyleStateResetOnDefaultTexturesLeavesNoError) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed";
}
// Clean is not enough: per GL 4.6 core 8.8 that zero-sized reset has to DEALLOCATE the image,
// not define an empty one. gluStateReset runs it on both default multisample textures on every
// texture unit of a 3.2+ context, and a default texture left 'defined' afterwards stops being
// skipped by IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
// every unit the reset touched and reaches an ES glTexStorage*Multisample(..., 0, 0), which ES
// 3.1 8.19 rejects on every driver.
TEST_F(TextureTest, ZeroSizedMultisampleTexImageDeallocatesTheImage) {
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
const auto& defaultMultisample = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2DMultisample)
.GetBoundObject();
MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 4, 4, GL_TRUE);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
ASSERT_FALSE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisample.get()));
MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisample.get()));
// The array target's reset also passes zero LAYERS, which deallocates just the same.
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0);
const auto& defaultMultisampleArray = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2DMultisampleArray)
.GetBoundObject();
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 4, 4, 2, GL_TRUE);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
ASSERT_FALSE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisampleArray.get()));
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 4, 4, 0, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisampleArray.get()));
// The immutable forms do NOT share that leniency: GL 4.6 core 8.19 makes a size below 1
// INVALID_VALUE, and freezing an imageless texture as immutable would be unrecoverable.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::TexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE);
ExpectSingleGlError(GL_INVALID_VALUE);
EXPECT_FALSE(MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2DMultisample)
.GetBoundObject()
->IsImmutable());
MG_Impl::GLImpl::DeleteTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions --------------------
TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
@@ -3480,29 +3268,6 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
TexturePixelDataType::UnsignedInt5999Rev));
}
TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) {
using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding;
// This is the predicate that decides whether the CPU shadow has to answer glGetTexImage
// instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent
// has several legal encodings of one value.
EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5));
// The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is
// either an integer or a unique float encoding), so a GPU readback still serves them - which
// matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can
// legitimately be stale.
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F));
// Nothing unpacked qualifies, and neither does an unknown format.
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown));
}
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
// words untouched, including the redundant encodings the CTS generates.
@@ -4283,25 +4048,24 @@ namespace {
GLint SrcZ = -1;
GLint DstZ = -1;
GLsizei Depth = -1;
Bool SrcIsRenderbuffer = false;
Bool DstIsRenderbuffer = false;
} g_copyImageSubDataCall;
void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTexture;
(void)srcLevel;
(void)srcX;
(void)srcY;
(void)dstTexture;
(void)dstLevel;
(void)dstX;
(void)dstY;
(void)srcWidth;
(void)srcHeight;
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ,
dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()};
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
}
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
@@ -4538,13 +4302,9 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
@@ -4576,236 +4336,3 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated)
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER as an endpoint target, and a renderbuffer name lives
// in its own namespace. Resolving BOTH names through the texture namespace answered a null object
// for every renderbuffer endpoint, so all 74 conformance cases that name one - the whole
// texture<->renderbuffer half of KHR-GL43.copy_image, plus its smoke test - reported
// GL_INVALID_VALUE. The endpoint is a sum type now; the target picks the namespace.
TEST_F(TextureTest, CopyImageSubDataResolvesARenderbufferEndpointInTheRenderbufferNamespace) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
GLuint renderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_FALSE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_RENDERBUFFER));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and back the other way, which is the second half of the conformance case's two-copy
// shape (texture -> renderbuffer -> texture).
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_FALSE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// Renderbuffer to renderbuffer, the shape neither endpoint could take before, plus the negative
// that pins which table was consulted: with GL_RENDERBUFFER named, a number that is not a live
// RENDERBUFFER is INVALID_VALUE - the texture table is never asked.
TEST_F(TextureTest, CopyImageSubDataKeepsTheTwoNameNamespacesApart) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcRenderbuffer = 0;
GLuint dstRenderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &srcRenderbuffer);
MG_Impl::GLImpl::CreateRenderbuffers(1, &dstRenderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(srcRenderbuffer, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::NamedRenderbufferStorage(dstRenderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, dstRenderbuffer,
GL_RENDERBUFFER, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, 4243, GL_RENDERBUFFER, 0, 0, 0,
0, 4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A renderbuffer has exactly one image, so any level above zero is the same INVALID_VALUE a
// texture gets for a level it does not have - and an unallocated one is an incomplete image,
// which 18.3.2 spells INVALID_OPERATION.
TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
GLuint renderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 1, 0, 0, 0,
4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
g_copyImageSubDataCall = {};
GLuint emptyRenderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &emptyRenderbuffer);
DrainPendingGlErrors();
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, emptyRenderbuffer, GL_RENDERBUFFER, 0, 0,
0, 0, 4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
// resolves to and the compatibility rule saw 2 against 16.
TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint compressedSource = 0;
MG_Impl::GLImpl::GenTextures(1, &compressedSource);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, compressedSource);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 1, 0, GL_RG,
GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
GLuint uncompressedDestination = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &uncompressedDestination);
MG_Impl::GLImpl::TextureStorage3D(uncompressedDestination, 1, GL_RGBA32UI, 8, 8, 1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(compressedSource, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, uncompressedDestination,
GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// 18.3.2 requires INVALID_OPERATION when either object is an INCOMPLETE TEXTURE, and completeness
// is GL 4.6 core 8.17's - which includes the mip chain whenever the minification filter reads it.
// A mutable texture with level 0 alone still carries the default NEAREST_MIPMAP_LINEAR filter, so
// it is mipmap incomplete; the storage-only IsComplete() this used to ask called it complete and
// let the copy through, which is the whole of KHR-GL43.copy_image.incomplete_tex.
TEST_F(TextureTest, CopyImageSubDataRejectsAMipmapIncompleteTexture) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint incomplete = 0;
MG_Impl::GLImpl::GenTextures(1, &incomplete);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
GLuint complete = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &complete);
MG_Impl::GLImpl::TextureStorage2D(complete, 1, GL_RGBA8, 16, 16);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
// The destination side is checked the same way.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(complete, GL_TEXTURE_2D, 0, 0, 0, 0, incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
// Capping TEXTURE_MAX_LEVEL at the one level that exists is what the conformance suite's
// makeTextureComplete does, and it is enough to make the same object complete.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
DrainPendingGlErrors();
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The targets that have no mip chain must not be dragged in: GL 4.6 core 8.17 makes q equal to
// level_base for them, so no filter can make them mipmap incomplete. A rectangle texture gets a
// non-mipmapping default filter from the object itself, so it would survive a predicate that
// trusted the sampler alone - it is here because the whole texture path is one branch and this is
// the cheap half of pinning it.
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToRectangleTextures) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcRectangle = 0;
GLuint dstRectangle = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcRectangle);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstRectangle);
MG_Impl::GLImpl::TextureStorage2D(srcRectangle, 1, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::TextureStorage2D(dstRectangle, 1, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcRectangle, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstRectangle,
GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The multisample half, which is the one the target guard actually exists for: a multisample
// texture keeps the shared NEAREST_MIPMAP_LINEAR default in its own sampler state (only the
// rectangle constructor overrides it), so asking the mipmap predicate about it without the target
// guard would report every 8x8 multisample image incomplete and refuse a legal copy.
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleTextures) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcMultisample = 0;
GLuint dstMultisample = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcMultisample);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstMultisample);
MG_Impl::GLImpl::TextureStorage2DMultisample(srcMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
MG_Impl::GLImpl::TextureStorage2DMultisample(dstMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
DrainPendingGlErrors();
// This unit-test binary has no backend behind the renderable-format and sample-count queries,
// so the storage may not have been created at all. Checked on the state objects rather than
// assumed, so the case can only skip or test the real rule.
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcMultisample);
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstMultisample);
ASSERT_NE(srcObject, nullptr);
ASSERT_NE(dstObject, nullptr);
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
GTEST_SKIP() << "this context could not give the multisample textures storage";
}
MG_Impl::GLImpl::CopyImageSubData(srcMultisample, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstMultisample,
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
@@ -692,12 +692,8 @@ namespace MobileGL::MG_Util::BackendLoader {
!f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
return false;
}
// Read from caps, not re-queried: the per-stage limits are resolved (and their query
// errors drained) before this probe runs, so asking the driver again would be a second
// round trip that can disagree with the number MobileGL actually advertises - and, on
// the early return below, would leave its own GL_INVALID_ENUM in the queue for the
// application's first glGetError to find.
const GLint maxVertexSsboBlocks = caps.MaxVertexShaderStorageBlocks;
GLint maxVertexSsboBlocks = 0;
f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
if (maxVertexSsboBlocks < 1) {
// The native indirect machinery cannot read the command buffer from the vertex
// stage on this driver anyway; assume conforming zero-based gl_InstanceID.
@@ -936,9 +932,6 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
caps.SupportsClipDistance = true;
}
if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
caps.SupportsViewportArray = true;
}
}
}
// The pointer check on top of the extension check makes each flag sufficient on its own
@@ -996,8 +989,6 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
caps.SupportsBaseInstance ? "yes" : "no");
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
caps.SupportsViewportArray ? "yes" : "no");
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
@@ -1040,15 +1031,6 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxVertexAttribs = 16;
GLint maxComputeShaderStorageBlocks = 8;
GLint maxCombinedShaderStorageBlocks = 32;
// ES 3.2 table 21.44 minimums. Zero for the four graphics stages below fragment is not a
// placeholder - it is what the spec permits and what ARM's GLES driver actually reports,
// so a probe that never runs (pre-ES 3.2, unsupported pname) leaves behind the truthful
// answer rather than an optimistic one.
GLint maxVertexShaderStorageBlocks = 0;
GLint maxTessControlShaderStorageBlocks = 0;
GLint maxTessEvaluationShaderStorageBlocks = 0;
GLint maxGeometryShaderStorageBlocks = 0;
GLint maxFragmentShaderStorageBlocks = 4;
GLint maxComputeUniformBlocks = 12;
GLint maxComputeWorkGroupInvocations = 128;
GLint maxShaderStorageBufferBindings = 8;
@@ -1140,59 +1122,6 @@ namespace MobileGL::MG_Util::BackendLoader {
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
}
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
// EXT_tessellation_shader / EXT_geometry_shader), so on an older context they raise
// GL_INVALID_ENUM, leave the local untouched, and - with nothing draining the queue until
// some later probe - let that error be misattributed to an unrelated query in between, or
// leak into the application's first glGetError.
//
// A stage whose probe does not run keeps the spec minimum, which for all four graphics
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
// where those stages do not exist at all.
{
const auto drainErrors = [&glesFuncs]() {
Bool hadError = false;
if (glesFuncs.glGetError) {
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
}
return hadError;
};
// Isolate from errors raised by the preceding probes so the drain below reports on
// these queries only.
drainErrors();
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentShaderStorageBlocks);
if (drainErrors()) {
MGLOG_W("Per-stage shader storage block query failed for the vertex/fragment "
"stages; assuming the ES minimums (vertex 0, fragment 4)");
maxVertexShaderStorageBlocks = 0;
maxFragmentShaderStorageBlocks = 4;
}
if (esAtLeast32) {
glesFuncs.glGetIntegerv(GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS,
&maxTessControlShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS,
&maxTessEvaluationShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &maxGeometryShaderStorageBlocks);
if (drainErrors()) {
MGLOG_W("Per-stage shader storage block query failed for the tessellation/"
"geometry stages; assuming the ES minimum of 0");
maxTessControlShaderStorageBlocks = 0;
maxTessEvaluationShaderStorageBlocks = 0;
maxGeometryShaderStorageBlocks = 0;
}
}
// A driver is free to report a negative or nonsensical count into an untouched
// out-param; clamp before anything downstream treats it as a capacity.
maxVertexShaderStorageBlocks = std::max(maxVertexShaderStorageBlocks, 0);
maxTessControlShaderStorageBlocks = std::max(maxTessControlShaderStorageBlocks, 0);
maxTessEvaluationShaderStorageBlocks = std::max(maxTessEvaluationShaderStorageBlocks, 0);
maxGeometryShaderStorageBlocks = std::max(maxGeometryShaderStorageBlocks, 0);
maxFragmentShaderStorageBlocks = std::max(maxFragmentShaderStorageBlocks, 0);
}
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
@@ -1337,11 +1266,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxVertexAttribs = maxVertexAttribs;
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks;
caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks;
caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks;
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
@@ -1419,14 +1343,6 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks);
// Worth a line each: a zero here is what stops an application's storage block from ever
// working in that stage, and reading it back from an artifact is the difference between
// "MobileGL dropped my draw" and "this driver has no SSBOs outside compute".
MGLOG_I(" GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", caps.MaxVertexShaderStorageBlocks);
MGLOG_I(" GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessControlShaderStorageBlocks);
MGLOG_I(" GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessEvaluationShaderStorageBlocks);
MGLOG_I(" GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: %d", caps.MaxGeometryShaderStorageBlocks);
MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
@@ -1182,21 +1182,6 @@ namespace MobileGL {
// compile and the per-distance enables have nowhere to go - clipping silently never
// happens, which is exactly what KHR-GLxx.clip_distance.functional catches.
Bool SupportsClipDistance = false;
// GL_OES_viewport_array is present: the driver knows gl_ViewportIndex in ESSL - and
// only then. ESSL has no core spelling for it at ANY version, while SPIRV-Cross prints
// the identifier bare and requests nothing for it (contrast gl_Layer, which it backs
// with GL_NV_viewport_array2 on ES), so the `#extension GL_OES_viewport_array :
// require` line has to be inserted into the emitted source - see
// RequestViewportArrayExtension. Without the extension the stage does not compile at
// all and the whole program becomes unusable, which on DirectGLES means every draw
// using it silently renders nothing; LowerViewportIndexPass is the fallback that
// demotes the builtin so the program still links and degrades to viewport 0.
//
// Extension string only, deliberately: DirectGLES does not call any of the indexed
// OES entry points yet, so there is no pointer to require. When that forwarding lands
// this must gain the pointer check as well - the rule everywhere else in this struct,
// because eglGetProcAddress can hand back a stub that silently drops every call.
Bool SupportsViewportArray = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
@@ -1248,17 +1233,6 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS as the host GLES driver reports them.
// The defaults are the ES 3.2 minimums (table 21.44): 0 for every graphics stage
// except fragment, which is 4. ES only gained the tessellation and geometry pnames
// in 3.2 (or with EXT_tessellation_shader / EXT_geometry_shader), so those two are
// queried behind a support check and left at the default otherwise - see
// FillInGLESCapabilities.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 4;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
@@ -253,12 +253,6 @@ namespace MobileGL {
return TextureInternalFormat::Depth32FStencil8;
case GL_STENCIL_INDEX8:
return TextureInternalFormat::StencilIndex8;
// The unsized stencil base format resolves to the only stencil storage there is, the
// same way the unsized colour and depth base formats below resolve to theirs. Returning
// Unknown made glTexImage2D(GL_STENCIL_INDEX) an error, which killed the negative
// clear-texture cases in their own setup before they could reach the call they test.
case GL_STENCIL_INDEX:
return TextureInternalFormat::StencilIndex8;
case GL_DEPTH_COMPONENT:
return TextureInternalFormat::DepthComponent;
case GL_DEPTH_STENCIL:
@@ -124,9 +124,6 @@ namespace MobileGL {
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
// Already sized: both GL_STENCIL_INDEX8 and the unsized GL_STENCIL_INDEX resolve here,
// and there is only one stencil storage to infer.
case TextureInternalFormat::StencilIndex8:
return internalformat;
// probably we should assume unorm here?
case TextureInternalFormat::RGBA: {
@@ -20,7 +20,6 @@
#include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/DemoteFloat64Pass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/LowerViewportIndexPass.h"
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
@@ -28,12 +27,10 @@
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
#include "SpirvPasses/EmulateSubgroupsPass.h"
#include "SpirvPasses/FixIterationRPBarrierPass.h"
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/BakeImageFormatsPass.h"
#include "SpirvPasses/ClampMultisampleFetchPass.h"
#include "SpirvPasses/PrivateToEntryLocalPass.h"
#include "SpirvPasses/StripUniformLocationsPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -637,63 +634,6 @@ namespace MobileGL {
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerViewportIndexPass::CreateLowerViewportIndexPass());
return RunOptimizerChecked("LowerViewportIndexForEssl", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
}
bool ShaderCompiler::ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const Int32 maxColorSamples,
const Int32 maxIntegerSamples,
const Int32 maxDepthSamples,
const Int32 advertisedMaxSamples,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
return RunOptimizerChecked("ClampMultisampleFetchesForEssl", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary);
}
ShaderCompiler::SpirvGateFeatures ShaderCompiler::ProbeSpirvGateFeatures(
const Vector<Uint32>& binary) {
SpirvGateFeatures features;
if (binary.empty()) {
return features;
}
std::unique_ptr<spvtools::opt::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) {
// Unparseable here means unusable downstream too; let the ordinary transpile
// path produce the error rather than inventing a verdict from it.
return features;
}
features.WritesViewportIndexOutput =
LowerViewportIndexPass::DeclaresViewportIndexBuiltin(context.get());
features.DeclaresMultisampledImage =
ClampMultisampleFetchPass::DeclaresMultisampledImage(context.get());
return features;
}
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
@@ -972,29 +912,17 @@ namespace MobileGL {
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes,
const bool enableSpirvValidation) {
const Uint32 nativeSubgroupSize, const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
nativeSubgroupSize, maxWorkgroupScratchBytes));
nativeSubgroupSize));
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
@@ -32,49 +32,6 @@ namespace MobileGL {
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Demotes the gl_ViewportIndex OUTPUT builtin to a plain Private global named
// mg_ViewportIndex, so SPIRV-Cross emits an ordinary declaration instead of a bare
// gl_ViewportIndex that ESSL has no core spelling for. Multi-viewport routing is
// lost (everything lands in viewport 0) but the stage compiles and the program
// runs, instead of every draw made with it becoming a silent no-op. Only for the
// DirectGLES transpile path on a driver WITHOUT GL_OES_viewport_array; gl_Layer is
// deliberately left alone, being core in ESSL 3.20 geometry shaders.
static bool LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
// whether the pass above has anything to do. The gate that keeps every other
// stage off an optimizer round trip it does not need.
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
// Clamps the Sample image-operand of every multisample fetch to the sample count
// the BACKEND can really deliver for that image's category, which on Adreno and
// Mali is 1 for integer formats while the frontend advertises the GL-mandated
// floor of 4. Without it a `texelFetch(usampler2DMS, coord, 3)` reads past the
// end of a one-sample allocation. Pass the backend-real per-category ceilings and
// the advertised maximum (GL_Getter's GetAdvertisedMaxSamples); a category that
// already reaches the advertised value is left alone. DirectGLES transpile path
// only. See ClampMultisampleFetchPass.
static bool ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
Int32 maxColorSamples,
Int32 maxIntegerSamples,
Int32 maxDepthSamples,
Int32 advertisedMaxSamples,
bool enableSpirvValidation = false);
// Whether the module declares any multisampled image type, i.e. whether the pass
// above has anything to do. The gate that keeps every other stage off an
// optimizer round trip it does not need.
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
// Both gate questions above answered from ONE parse. Every armed gate costs a
// BuildModule per shader stage, and on a driver where both are armed (Mali: no
// 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.
struct SpirvGateFeatures {
Bool WritesViewportIndexOutput = false;
Bool DeclaresMultisampledImage = false;
};
static SpirvGateFeatures ProbeSpirvGateFeatures(const Vector<Uint32>& binary);
// Replaces an ARRAY vertex input with one input per element at consecutive
// locations, seeding a Private copy of the array so indexed reads still work.
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
@@ -210,24 +167,13 @@ namespace MobileGL {
Vector<uint32_t>& outputBinary,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Grows iterationRP's under-declared gl_SubgroupID-indexed scratch to the
// subgroup count the device actually partitions into, fingerprint-gated to
// that pack's reduction idiom; every other module - and every device whose
// width the pack already assumed - passes through byte-identical.
// maxWorkgroupScratchBytes bounds the growth (pass the device's
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
// minimum). See FixIterationRPSubgroupScratchPass.
// Patches iterationRP's under-declared prefixSumCache[32] on sub-16-lane
// devices, fingerprint-gated to that pack's reduction; every other module
// passes through byte-identical. See FixIterationRPSubgroupScratchPass.
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Inserts the missing workgroup rendezvous between Program 203's two
// prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
// unrelated and already-repaired modules pass through byte-identical.
static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
@@ -1,378 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.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 "ClampMultisampleFetchPass.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 "source/util/string_utils.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;
// GLSL.std.450 instruction numbers (see 3rdparty/glslang/SPIRV/GLSL.std.450.h).
// UMin is not interchangeable with SMin here: an unsigned operand large enough to
// read as negative would come back OUT of range from a signed minimum, which is
// the very thing this pass exists to prevent.
constexpr uint32_t kGlslUMin = 38u;
constexpr uint32_t kGlslSMin = 39u;
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
// 5 Sampled, 6 Format.
constexpr uint32_t kSampledTypeOperand = 0;
constexpr uint32_t kDepthOperand = 2;
constexpr uint32_t kMultisampledOperand = 4;
// OpImageFetch / OpImageRead in-operands: 0 image, 1 coordinate, 2 the optional
// image-operands mask, 3.. the ids that mask asks for.
constexpr uint32_t kImageOperand = 0;
constexpr uint32_t kImageOperandsMaskOperand = 2;
// The categories GL keeps a separate GL_MAX_*_SAMPLES ceiling for.
enum class SampleCategory { Color, Depth, Integer };
// The two operations that can carry a Sample image-operand and take their
// coordinate in in-operand 1. OpImageWrite can carry one too, but its operand
// layout differs (image, coordinate, TEXEL, mask) and writing a multisample
// texel is not expressible in the ESSL this backend emits, so it is left out
// rather than given an untested second index arithmetic. The sparse forms are
// out of scope for the same reason: ESSL has no sparse texturing at all, so a
// module containing one cannot reach a driver through this path anyway.
bool CarriesSampleImageOperand(spv::Op opcode) {
return opcode == spv::Op::OpImageFetch || opcode == spv::Op::OpImageRead;
}
bool IsMultisampledImageType(const Instruction* imageType) {
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
imageType->NumInOperands() > kMultisampledOperand &&
imageType->GetSingleWordInOperand(kMultisampledOperand) == 1u;
}
// The OpTypeImage behind whatever an image operation was handed - a sampled
// image, a bare image, or a pointer to (or array of) either. Same unwrapping as
// Lower1DArrayImagesPass.
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.
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;
}
SampleCategory CategoryOf(IRContext* context, const Instruction* imageType) {
const Instruction* sampledType =
context->get_def_use_mgr()->GetDef(imageType->GetSingleWordInOperand(kSampledTypeOperand));
if (sampledType != nullptr && sampledType->opcode() == spv::Op::OpTypeInt) {
return SampleCategory::Integer;
}
// Depth == 1 is the ONLY spelling that positively means a depth image.
// glslang writes 0 for a plain sampler and 2 ("no indication") wherever it
// cannot tell, and GLSL has no multisampled shadow sampler for it to write 1
// for, so everything but an explicit 1 falls to the colour ceiling - which is
// also the safer of the two to guess at, being the one GL_MAX_SAMPLES itself
// describes. Guarded because Depth is only readable on a well-formed type.
if (imageType->NumInOperands() > kDepthOperand &&
imageType->GetSingleWordInOperand(kDepthOperand) == 1u) {
return SampleCategory::Depth;
}
return SampleCategory::Color;
}
// Where the Sample id sits among an image operation's in-operands, or false when
// the operation carries no Sample at all.
//
// The position is NOT fixed. The mask's ids follow it in ASCENDING BIT ORDER, so
// every lower bit that is set pushes Sample along by the number of ids that bit
// asks for: Bias/Lod/ConstOffset/Offset/ConstOffsets one each, Grad two (dx and
// dy). Bits at or above Sample cannot move it and are irrelevant here. glslang
// only ever emits Sample on its own for a GLSL texelFetch - there is no
// texelFetchOffset for a multisampled sampler - so in practice this always
// answers 3; the walk is what keeps that from being an assumption.
bool TryGetSampleOperandIndex(const Instruction& instruction, uint32_t* sampleOperandIndex) {
if (instruction.NumInOperands() <= kImageOperandsMaskOperand) {
// No image-operands mask at all, so no explicit sample: SPIR-V reads
// sample 0, which is in range of any allocation. Nothing to clamp.
return false;
}
const uint32_t mask = instruction.GetSingleWordInOperand(kImageOperandsMaskOperand);
const auto has = [mask](spv::ImageOperandsMask bit) {
return (mask & static_cast<uint32_t>(bit)) != 0u;
};
if (!has(spv::ImageOperandsMask::Sample)) {
return false;
}
uint32_t index = kImageOperandsMaskOperand + 1;
if (has(spv::ImageOperandsMask::Bias)) ++index;
if (has(spv::ImageOperandsMask::Lod)) ++index;
if (has(spv::ImageOperandsMask::Grad)) index += 2;
if (has(spv::ImageOperandsMask::ConstOffset)) ++index;
if (has(spv::ImageOperandsMask::Offset)) ++index;
if (has(spv::ImageOperandsMask::ConstOffsets)) ++index;
if (instruction.NumInOperands() <= index) {
// A mask promising more operands than the instruction carries is a
// malformed module; leave it to the validator rather than indexing past
// the end of it.
return false;
}
*sampleOperandIndex = index;
return true;
}
// The module's GLSL.std.450 import, creating it when the module has none.
// glslang emits one for all but the most trivial shaders, but a module that
// reached here without one must still be clampable. 0 means no id was available,
// and in that case NOTHING was added - the caller can still leave the module
// untouched. IRContext::AddExtInstImport rather than Module's: it is the one that
// keeps the def-use and feature managers in step with the new import.
uint32_t EnsureGlslStd450Import(IRContext* context) {
for (const Instruction& import : context->module()->ext_inst_imports()) {
if (spvtools::utils::MakeString(import.GetInOperand(0).words) == "GLSL.std.450") {
return import.result_id();
}
}
const uint32_t importId = context->TakeNextId();
if (importId == 0u) return 0u;
context->AddExtInstImport(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpExtInstImport, 0, importId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector("GLSL.std.450")}}));
return importId;
}
} // namespace
bool ClampMultisampleFetchPass::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
if (binary.empty()) {
// An empty module is a stage that produced no SPIR-V, which is not a verdict
// about multisample fetches; letting BuildModule reject it would push a
// spurious diagnostic through the message consumer first.
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) {
// Unparseable here means unusable downstream too; let the ordinary transpile
// path produce the error rather than inventing a verdict from it.
return false;
}
return DeclaresMultisampledImage(context.get());
}
bool ClampMultisampleFetchPass::DeclaresMultisampledImage(IRContext* context) {
for (const Instruction& type : context->module()->types_values()) {
if (IsMultisampledImageType(&type)) {
return true;
}
}
return false;
}
spvtools::opt::Pass::Status ClampMultisampleFetchPass::Process() {
// No category is squeezed, so no fetch can be out of range. This is the whole
// answer on a driver whose per-format ceilings all reach what MobileGL
// advertises, and it costs nothing.
if (m_maxColorSamples >= m_advertisedMaxSamples &&
m_maxIntegerSamples >= m_advertisedMaxSamples &&
m_maxDepthSamples >= m_advertisedMaxSamples) {
return Status::SuccessWithoutChange;
}
auto* irContext = context();
// The type table settles it for almost every shader: no multisampled image
// declared, nothing any fetch in the body could be reading.
bool hasMultisampledImageType = false;
for (const Instruction& type : irContext->types_values()) {
if (IsMultisampledImageType(&type)) {
hasMultisampledImageType = true;
break;
}
}
if (!hasMultisampledImageType) {
return Status::SuccessWithoutChange;
}
auto* defUseMgr = irContext->get_def_use_mgr();
auto* typeMgr = irContext->get_type_mgr();
auto* constantMgr = irContext->get_constant_mgr();
bool clampedAnything = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& instruction : block) {
if (!CarriesSampleImageOperand(instruction.opcode()) ||
instruction.NumInOperands() <= kImageOperandsMaskOperand) {
continue;
}
const Instruction* imageType =
ResolveImageType(irContext, instruction.GetSingleWordInOperand(kImageOperand));
if (!IsMultisampledImageType(imageType)) {
continue;
}
uint32_t sampleOperandIndex = 0;
if (!TryGetSampleOperandIndex(instruction, &sampleOperandIndex)) {
continue;
}
Int32 categoryMaxSamples = m_maxColorSamples;
switch (CategoryOf(irContext, imageType)) {
case SampleCategory::Integer:
categoryMaxSamples = m_maxIntegerSamples;
break;
case SampleCategory::Depth:
categoryMaxSamples = m_maxDepthSamples;
break;
case SampleCategory::Color:
break;
}
if (categoryMaxSamples >= m_advertisedMaxSamples) {
continue;
}
// The replacement has to carry the ORIGINAL operand's type: SPIR-V
// permits either signedness for Sample, and handing OpImageFetch an
// int where it had a uint is an invalid module rather than a wrong
// answer - the kind of defect that reaches a driver as "compiles
// here, not there".
const uint32_t sampleOperandId = instruction.GetSingleWordInOperand(sampleOperandIndex);
const Instruction* sampleOperandDef = defUseMgr->GetDef(sampleOperandId);
if (sampleOperandDef == nullptr) {
continue;
}
const uint32_t sampleTypeId = sampleOperandDef->type_id();
const analysis::Type* sampleType =
sampleTypeId != 0u ? typeMgr->GetType(sampleTypeId) : nullptr;
const analysis::Integer* sampleInteger =
sampleType != nullptr ? sampleType->AsInteger() : nullptr;
if (sampleInteger == nullptr || sampleInteger->width() != 32u) {
// GLSL spells the sample index `int` and SPIR-V requires an
// integer scalar, so this is unreachable from any shader this
// backend compiles. Declining beats minting a constant of a
// width the operand never had.
MGLOG_D("ClampMultisampleFetchPass: sample operand %%%u of a "
"multisample fetch is not a 32-bit integer scalar; left "
"unclamped.",
sampleOperandId);
continue;
}
if (categoryMaxSamples <= 1) {
// One sample exists, and its index is 0.
const analysis::Constant* zero = constantMgr->GetConstant(sampleType, {0u});
const Instruction* zeroInst =
zero != nullptr ? constantMgr->GetDefiningInstruction(zero, sampleTypeId)
: nullptr;
if (zeroInst == nullptr) {
return Status::Failure;
}
instruction.SetInOperand(sampleOperandIndex, {zeroInst->result_id()});
irContext->UpdateDefUse(&instruction);
clampedAnything = true;
continue;
}
// min(operand, K-1). Only the upper bound: an index already inside
// the allocation comes through untouched, which is what makes this
// safe to apply to a shader that was already correct.
//
// Everything from here on either completes or fails the module.
// Anything that gives up half way - after the import or the bound
// constant has been added - would leave a MUTATED module reported as
// SuccessWithoutChange, which spvtools::Optimizer asserts against
// (it re-serialises and compares byte for byte in that case).
const uint32_t glslStd450Id = EnsureGlslStd450Import(irContext);
if (glslStd450Id == 0u) {
// Id space exhausted, and the import was NOT added. Nothing has
// changed yet, but nothing further can be built either.
return Status::Failure;
}
const uint32_t resultId = irContext->TakeNextId();
const analysis::Constant* bound = constantMgr->GetConstant(
sampleType, {static_cast<uint32_t>(categoryMaxSamples - 1)});
const Instruction* boundInst =
bound != nullptr ? constantMgr->GetDefiningInstruction(bound, sampleTypeId)
: nullptr;
if (resultId == 0u || boundInst == nullptr) {
return Status::Failure;
}
InstructionBuilder builder(
irContext, &instruction,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
Instruction* clamped = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpExtInst, sampleTypeId, resultId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {glslStd450Id}},
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER,
{sampleInteger->IsSigned() ? kGlslSMin : kGlslUMin}},
{SPV_OPERAND_TYPE_ID, {sampleOperandId}},
{SPV_OPERAND_TYPE_ID, {boundInst->result_id()}}}));
if (clamped == nullptr) {
return Status::Failure;
}
instruction.SetInOperand(sampleOperandIndex, {clamped->result_id()});
irContext->UpdateDefUse(&instruction);
clampedAnything = true;
}
}
}
if (!clampedAnything) {
return Status::SuccessWithoutChange;
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
const Int32 maxColorSamples, const Int32 maxIntegerSamples, const Int32 maxDepthSamples,
const Int32 advertisedMaxSamples) {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<ClampMultisampleFetchPass>(
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,93 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.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>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so MobileGL floors every
// multisample ceiling it advertises to 4 (GL_Getter's kFrontendMaxSamples) no matter
// what the ES driver reports. The realised allocation cannot be floored the same way -
// the driver would simply reject it - so DirectGLES clamps the count it passes to
// glTexStorage*Multisample down to what the format really supports
// (ClampSamplesToBackendSupport). On Adreno and on Mali's Immortalis-G925 that is ONE
// sample for every integer format, while the frontend keeps telling the application
// GL_MAX_INTEGER_SAMPLES is 4.
//
// A shader written against the advertised ceiling therefore fetches a sample the
// backing storage does not have. KHR-GL33/40/41.texture_swizzle.functional_* and
// KHR-GLxx.texture_size_promotion.functional bake `texelFetch(usampler2DMS, coord, 3)`
// in as a literal, and the fetch comes back as 0/garbage ("Found pixel with wrong
// value", "read value = 0") on a texture the backend quietly allocated with one
// sample.
//
// This pass closes that gap from the shader side: for every fetch of a multisampled
// image it clamps the Sample image-operand to the backend's REAL maximum for that
// image's category, so the lookup lands inside the allocation the backend made.
// - K >= advertisedMaxSamples: the category is not squeezed, nothing is rewritten.
// - K <= 1: the Sample operand becomes a constant 0 of its own type - the only
// sample that exists.
// - 1 < K < advertisedMaxSamples: the operand is wrapped in min(operand, K-1),
// which leaves an in-range index exactly as it was.
// Only the UPPER bound is clamped. A negative index is out of range in GL before this
// pass and after it alike, and MobileGL is not the component that should be inventing
// a value for it.
//
// Category comes from the OpTypeImage: an OpTypeInt sampled type is the integer
// class (GL_MAX_INTEGER_SAMPLES), a float one is depth when the image's Depth operand
// is exactly 1 and colour otherwise. That last clause is deliberate: glslang writes
// Depth 0 for a plain sampler and 2 ("unknown") wherever it cannot tell, and GLSL has
// no multisampled shadow sampler at all, so only an explicit 1 is treated as a depth
// image and everything else falls to the colour limit - which is the one a
// mis-classified image would want anyway.
//
// DirectGLES transpile path only. DirectVulkan allocates the sample count it was
// asked for and must see the module unchanged.
class ClampMultisampleFetchPass : public spvtools::opt::Pass {
public:
// The three backend-REAL per-category ceilings, plus the count the GL frontend
// advertises (GL_Getter's GetAdvertisedMaxSamples). A category whose real ceiling
// already reaches the advertised one is left completely alone.
ClampMultisampleFetchPass(Int32 maxColorSamples, Int32 maxIntegerSamples,
Int32 maxDepthSamples, Int32 advertisedMaxSamples)
: m_maxColorSamples(maxColorSamples),
m_maxIntegerSamples(maxIntegerSamples),
m_maxDepthSamples(maxDepthSamples),
m_advertisedMaxSamples(advertisedMaxSamples) {}
const char* name() const override { return "clamp-multisample-fetch"; }
Status Process() override;
// Whether the module declares any multisampled image type, i.e. whether running
// this pass could change anything. Answered from a single parse so the caller can
// skip the optimizer round trip entirely - which is every shader but the handful
// that read a multisample texture directly.
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
// Same question answered from an already-built module, so one parse can feed
// several gates (ShaderCompiler::ProbeSpirvGateFeatures).
static bool DeclaresMultisampledImage(spvtools::opt::IRContext* context);
static spvtools::Optimizer::PassToken CreateClampMultisampleFetchPass(
Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples,
Int32 advertisedMaxSamples);
private:
Int32 m_maxColorSamples;
Int32 m_maxIntegerSamples;
Int32 m_maxDepthSamples;
Int32 m_advertisedMaxSamples;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -15,6 +15,8 @@
#include "source/opt/ir_builder.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/opt/type_manager.h"
#include <cmath>
#include <vector>
namespace MobileGL {
@@ -27,6 +29,7 @@ namespace MobileGL {
analysis::ConstantManager* const_mgr = context()->get_constant_mgr();
analysis::DefUseManager* def_use_mgr = context()->get_def_use_mgr();
analysis::TypeManager* type_mgr = context()->get_type_mgr();
// 2. Import `GLSL.std.450` extension ID (for abs() func)
uint32_t glsl_std_450_id = context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
@@ -67,10 +70,6 @@ namespace MobileGL {
uint32_t op2_id = inst.GetSingleWordInOperand(1);
uint32_t var_id = 0;
// The zero the source spelled, reused verbatim as the right-hand side
// of the rewritten compare - so nothing has to be synthesized for a
// width this pass would have to encode by hand.
uint32_t zero_id = 0;
// The constant's WIDTH decides which accessor may read it, and asking
// the wrong one does not fail - it answers.
@@ -79,8 +78,8 @@ namespace MobileGL {
// bits. On a 64-bit constant words()[0] is the LOW half of the
// mantissa, and that half is zero for every round double a shader
// actually spells: 1.0lf, 2.0lf, 0.5lf, 100.0lf. Each of those
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into a
// test of `d` against ZERO - which is TRUE for d == 1.0. That is the whole
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into
// `abs(d) >= epsilon` - which is TRUE for d == 1.0. That is the whole
// of KHR-GL43.compute_shader.fp64-case2: twelve uniforms compared
// against vector and matrix constructors were untouched (a composite
// is not a FloatConstant) and the one scalar comparison in the shader
@@ -98,24 +97,17 @@ namespace MobileGL {
const analysis::Float* floatType =
floatConstant->type() != nullptr ? floatConstant->type()->AsFloat() : nullptr;
if (floatType == nullptr) return false;
// Exactly zero - a near-zero constant is not a zero constant.
// `x == 1e-5` asks a different question than `x == 0.0` and must
// keep its own right-hand side. -0.0 compares equal to 0.0 here,
// which is correct: `x == -0.0` and `x == 0.0` are the same
// predicate in IEEE, and abs() maps both zeroes onto +0.
switch (floatType->width()) {
case 32: return floatConstant->GetFloatValue() == 0.0f;
case 64: return floatConstant->GetDoubleValue() == 0.0;
case 32: return std::fabs(floatConstant->GetFloatValue()) <= K_EPSILON;
case 64: return std::fabs(floatConstant->GetDoubleValue()) <= K_EPSILON;
default: return false;
}
};
if (is_float_zero(op2_id)) {
var_id = op1_id; // x == 0.0
zero_id = op2_id;
} else if (is_float_zero(op1_id)) {
var_id = op2_id; // 0.0 == x
zero_id = op1_id;
} else {
++itInst;
continue;
@@ -128,7 +120,12 @@ namespace MobileGL {
uint32_t float_type_id = def_use_mgr->GetDef(var_id)->type_id();
uint32_t bool_type_id = inst.type_id();
// 2. Build Abs(x) inst
// 2. Create constant ID for `Epsilon`
const analysis::Constant* eps_const = const_mgr->GetConstant(
type_mgr->GetType(float_type_id), {*(reinterpret_cast<const uint32_t*>(&K_EPSILON))});
uint32_t eps_id = const_mgr->GetDefiningInstruction(eps_const)->result_id();
// 3. Build Abs(x) inst
// OpExtInst %float_type %glsl_import Abs %x
InstructionBuilder builder(
context(), &inst, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
@@ -143,42 +140,38 @@ namespace MobileGL {
Instruction* abs_inst = builder.AddInstruction(MakeUnique<Instruction>(
context(), spv::Op::OpExtInst, float_type_id, context()->TakeNextId(), abs_operands));
// 3. build Abs(x) <= 0.0, or Abs(x) > 0.0 for the NotEqual forms
// OpFOrdLessThanEqual %bool_type %abs_val %zero
std::vector<Operand> cmp_operands;
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {zero_id}});
// 4. build Abs(x) < Epsilon
// OpFOrdLessThan %bool_type %abs_val %eps
std::vector<Operand> less_operands;
less_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
less_operands.push_back({SPV_OPERAND_TYPE_ID, {eps_id}});
// Equality is INCLUDED in the replacement, which is what makes the
// rewrite exact: |x| <= 0 is true for +0 and -0 and false for every
// other finite value, |x| > 0 is its complement. The ordered/unordered
// half of the opcode is preserved, so NaN keeps answering as it did.
spv::Op replacementOp = spv::Op::OpNop;
switch (inst.opcode()) {
case spv::Op::OpFOrdEqual:
replacementOp = spv::Op::OpFOrdLessThanEqual;
replacementOp = spv::Op::OpFOrdLessThan;
break;
case spv::Op::OpFUnordEqual:
replacementOp = spv::Op::OpFUnordLessThanEqual;
replacementOp = spv::Op::OpFUnordLessThan;
break;
case spv::Op::OpFOrdNotEqual:
replacementOp = spv::Op::OpFOrdGreaterThan;
replacementOp = spv::Op::OpFOrdGreaterThanEqual;
break;
case spv::Op::OpFUnordNotEqual:
replacementOp = spv::Op::OpFUnordGreaterThan;
replacementOp = spv::Op::OpFUnordGreaterThanEqual;
break;
default:
MOBILEGL_ASSERT(false, "Unexpected float compare opcode: %d",
static_cast<int>(inst.opcode()));
break;
}
Instruction* cmp_inst = builder.AddInstruction(MakeUnique<Instruction>(
context(), replacementOp, bool_type_id, context()->TakeNextId(), cmp_operands));
Instruction* less_than_inst = builder.AddInstruction(MakeUnique<Instruction>(
context(), replacementOp, bool_type_id, context()->TakeNextId(), less_operands));
// 4. Replaces all uses of old insn with new one
context()->ReplaceAllUsesWith(inst.result_id(), cmp_inst->result_id());
// 5. Replaces all uses of old insn with new one
context()->ReplaceAllUsesWith(inst.result_id(), less_than_inst->result_id());
// 5. Kill old instruction (will be cleaned up by DCE later)
// 6. Kill old instruction (will be cleaned up by DCE later)
auto nextInstIt = context()->KillInst(&inst);
if (nextInstIt) {
itInst = nextInstIt;
@@ -15,28 +15,15 @@
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Keeps the driver's float-EQUALITY instruction out of the module: every scalar
// comparison against a constant 0.0 is re-spelled through GLSL.std.450 FAbs, so no
// OpFOrdEqual / OpFUnordEqual / OpFOrdNotEqual / OpFUnordNotEqual against zero ever
// reaches a shader compiler that gets exact float compare wrong.
//
// The rewrite is EXACT, not a tolerance. `x == 0.0` becomes `abs(x) <= 0.0` and
// `x != 0.0` becomes `abs(x) > 0.0`, both against the module's own zero constant:
// |x| <= 0 holds for +0 and -0 and for nothing else, so the two forms agree on every
// input, at any float width, with or without denormal flushing. The ordered/unordered
// half of the opcode is carried across unchanged, which is what keeps NaN answering
// the way it did before.
//
// It used to be an epsilon ball (abs(x) < 1e-4). That silently classified any
// legitimately small value as zero - KHR-GL3x.buffer_objects.triangles renders a
// specular term of ~6e-5 at a large render target and came out black - so the fuzz is
// gone; the reason the pass exists never needed it.
class EliminateFloatEqualsZeroPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "float-equals-zero-elimination"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateEliminateFloatEqualsZeroPass();
private:
const float K_EPSILON = 0.0001f;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -1,232 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.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 "FixIterationRPBarrierPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL::MG_Util::ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
while (def != nullptr) {
switch (def->opcode()) {
case spv::Op::OpVariable:
return def;
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpCopyObject:
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
break;
default:
return nullptr;
}
}
return nullptr;
}
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
def->GetSingleWordInOperand(0) == wanted;
}
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr ||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
pointer->NumInOperands() < 2u) {
return false;
}
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
}
*root = RootVariable(context, pointerId);
return *root != nullptr;
}
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
return false;
}
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
pointerType->NumInOperands() < 2u) {
return false;
}
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
arrayType->NumInOperands() < 2u) {
return false;
}
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
return false;
}
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
if (arrayLength < 32u || arrayLength > 512u) return false;
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
spv::GroupOperation::InclusiveScan) {
return false;
}
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool HasProgram203LocalSize(IRContext* context) {
for (const Instruction& entryPoint : context->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
spv::ExecutionModel::GLCompute) {
return false;
}
}
for (const Instruction& mode : context->module()->execution_modes()) {
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
mode.GetSingleWordInOperand(4) == 1u;
}
}
return false;
}
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
}
} // namespace
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
auto* irContext = context();
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
for (auto& function : *irContext->module()) {
std::vector<Instruction*> instructions;
std::vector<size_t> scans;
for (auto& block : function) {
for (auto& inst : block) {
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
instructions.push_back(&inst);
}
}
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
if (scans.size() != 2u) continue;
const size_t firstScan = scans[0];
const size_t secondScan = scans[1];
const Instruction* scratch = nullptr;
size_t averageLoad = instructions.size();
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
Instruction* inst = instructions[i];
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
const Instruction* root = nullptr;
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
!IsWorkgroupVec2Array(irContext, root)) {
continue;
}
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
type->GetSingleWordInOperand(0) != 32u) {
continue;
}
scratch = root;
averageLoad = i;
break;
}
if (scratch == nullptr) continue;
bool sawZeroBroadcastStore = false;
bool sawPublishBarrier = false;
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
const Instruction* root = nullptr;
if (instructions[i]->opcode() == spv::Op::OpStore &&
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
root == scratch) {
sawZeroBroadcastStore = true;
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
sawPublishBarrier = true;
}
}
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
bool alreadySynchronized = false;
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
alreadySynchronized = true;
break;
}
}
if (alreadySynchronized) return Status::SuccessWithoutChange;
bool secondPhaseReusesScratch = false;
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
secondPhaseReusesScratch = true;
break;
}
}
if (!secondPhaseReusesScratch) continue;
auto* constantMgr = irContext->get_constant_mgr();
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
const uint32_t semanticsId =
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpControlBarrier, 0u, 0u,
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
return Status::SuccessWithoutChange;
}
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
}
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -1,28 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.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"
namespace MobileGL::MG_Util::ShaderTranspiler {
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
// reductions that reuse prefixSumCache. The first phase broadcasts its result
// through prefixSumCache[0], but the second phase may overwrite that element before
// every invocation has read it. The pass fingerprints that exact two-scan,
// 512-invocation shape and inserts one Workgroup control barrier immediately before
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "fix-iterationrp-barrier"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
};
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -26,13 +26,13 @@ namespace MobileGL {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// The Vulkan minimum for maxComputeSharedMemorySize, used when the caller
// could not tell us the device's real limit.
constexpr uint32_t kMinimumSharedMemoryBytes = 16384u;
// The narrowest subgroup width iterationRP's declarations are sized for.
// At or above it both shipped shapes fit and nothing may be rewritten.
constexpr uint32_t kPackAssumedSubgroupWidth = 16u;
// iterationRP's reduction fingerprint, spelled out.
constexpr uint32_t kIterationRPLocalSizeX = 32u;
constexpr uint32_t kIterationRPLocalSizeY = 16u;
constexpr uint32_t kIterationRPLocalSizeZ = 1u;
constexpr uint32_t kIterationRPInvocations =
kIterationRPLocalSizeX * kIterationRPLocalSizeY * kIterationRPLocalSizeZ;
constexpr uint32_t kIterationRPScratchLength = 32u;
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
auto* defUseMgr = context->get_def_use_mgr();
@@ -73,121 +73,18 @@ namespace MobileGL {
return nullptr;
}
// A 32-bit float scalar or vector - the shape of every accumulator the
// pack runs through its scans (float, vec2 and vec4 all appear). Returns
// the component count, or 0 for anything else.
uint32_t Float32ComponentCount(IRContext* context, uint32_t typeId) {
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return 0u;
uint32_t components = 1u;
if (type->opcode() == spv::Op::OpTypeVector) {
components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return 0u;
type = defUseMgr->GetDef(type->GetSingleWordInOperand(0));
if (type == nullptr) return 0u;
}
if (type->opcode() != spv::Op::OpTypeFloat ||
type->GetSingleWordInOperand(0) != 32u) {
return 0u;
}
return components;
}
uint32_t RoundUp(uint32_t value, uint32_t alignment) {
return alignment == 0u ? value : ((value + alignment - 1u) / alignment) * alignment;
}
// Size AND alignment of a workgroup-storage type. Drivers lay shared
// memory out at natural alignment and the limit
// (VUID-RuntimeSpirv-Workgroup-06530) counts the padding that produces,
// so a model that sums unpadded sizes would under-count exactly where the
// budget check matters. Returns false for anything not modelled here,
// which the caller answers by declining to grow at all rather than by
// certifying growth against a total it knows is an underestimate.
bool WorkgroupTypeLayout(IRContext* context, uint32_t typeId, uint32_t* size,
uint32_t* alignment, uint32_t depth = 0u) {
if (depth > 8u) return false;
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return false;
switch (type->opcode()) {
case spv::Op::OpTypeBool:
*size = 4u;
*alignment = 4u;
return true;
case spv::Op::OpTypeInt:
case spv::Op::OpTypeFloat: {
const uint32_t width = type->GetSingleWordInOperand(0) / 8u;
if (width == 0u) return false;
*size = width;
*alignment = width;
return true;
}
case spv::Op::OpTypeVector: {
uint32_t componentSize = 0u;
uint32_t componentAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0),
&componentSize, &componentAlignment, depth + 1u)) {
return false;
}
const uint32_t components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return false;
*size = componentSize * components;
// A three-component vector aligns like a four-component one.
*alignment = componentSize * (components == 3u ? 4u : components);
return true;
}
case spv::Op::OpTypeMatrix:
case spv::Op::OpTypeArray: {
uint32_t elementSize = 0u;
uint32_t elementAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0), &elementSize,
&elementAlignment, depth + 1u)) {
return false;
}
uint32_t count = 0u;
if (type->opcode() == spv::Op::OpTypeMatrix) {
count = type->GetSingleWordInOperand(1);
} else {
const Instruction* length =
defUseMgr->GetDef(type->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
return false; // spec-constant length: not sizeable here
}
count = length->GetSingleWordInOperand(0);
}
*size = RoundUp(elementSize, elementAlignment) * count;
*alignment = elementAlignment;
return true;
}
case spv::Op::OpTypeStruct: {
uint32_t offset = 0u;
uint32_t structAlignment = 1u;
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
uint32_t memberSize = 0u;
uint32_t memberAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(i),
&memberSize, &memberAlignment, depth + 1u)) {
return false;
}
offset = RoundUp(offset, memberAlignment) + memberSize;
if (memberAlignment > structAlignment) structAlignment = memberAlignment;
}
*size = RoundUp(offset, structAlignment);
*alignment = structAlignment;
return true;
}
default:
// vec2 of 32-bit float - the type of iterationRP's luminance/exposure
// accumulator and of its prefixSumCache entries.
bool IsVec2Float32(IRContext* context, uint32_t typeId) {
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
if (type == nullptr || type->opcode() != spv::Op::OpTypeVector ||
type->GetSingleWordInOperand(1) != 2u) {
return false;
}
}
// The group operations the pack's prefix scans use.
bool IsScanOrReduce(spv::GroupOperation operation) {
return operation == spv::GroupOperation::Reduce ||
operation == spv::GroupOperation::InclusiveScan ||
operation == spv::GroupOperation::ExclusiveScan;
const Instruction* component =
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
return component != nullptr && component->opcode() == spv::Op::OpTypeFloat &&
component->GetSingleWordInOperand(0) == 32u;
}
} // namespace
@@ -195,15 +92,13 @@ namespace MobileGL {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Without a known device width there is no topology to compare against;
// and a width the pack already assumed needs no patch at all. Both of
// iterationRP's shapes are sized for >= 16 lanes (512/16 = 32 entries,
// 1024/16 = 64), so every module on such a device - the pack's or anyone
// else's - must pass through byte-identical. The per-array length test
// further down is the second gate, not a replacement for this one.
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= kPackAssumedSubgroupWidth) {
// A device whose native width already satisfies the pack's assumption
// (>= 16 lanes -> at most 32 subgroups) needs no patch at all.
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= 16u) {
return Status::SuccessWithoutChange;
}
const uint32_t requiredLength =
(kIterationRPInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize;
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
@@ -212,8 +107,7 @@ namespace MobileGL {
}
}
// Fingerprint 1: a literal workgroup size, so the subgroup count the
// dispatch actually partitions into is known here.
// Fingerprint 1: the pack's exposure-pass workgroup shape, 32x16x1.
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
const Instruction* def = defUseMgr->GetDef(id);
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
@@ -245,40 +139,26 @@ namespace MobileGL {
}
}
}
if (!haveLocalSize || localSize[0] == 0u || localSize[1] == 0u || localSize[2] == 0u) {
if (!haveLocalSize || localSize[0] != kIterationRPLocalSizeX ||
localSize[1] != kIterationRPLocalSizeY || localSize[2] != kIterationRPLocalSizeZ) {
return Status::SuccessWithoutChange;
}
const uint64_t totalInvocations =
static_cast<uint64_t>(localSize[0]) * localSize[1] * localSize[2];
if (totalInvocations == 0u || totalInvocations > (1u << 20)) {
return Status::SuccessWithoutChange;
}
const uint32_t requiredLength = static_cast<uint32_t>(
(totalInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize);
// Fingerprint 2: a subgroup scan over a 32-bit float value - the pack's
// prefix-sum reduction, and the reason its scratch is indexed per subgroup.
bool sawFloatSubgroupScan = false;
// Fingerprint 2: the reduction's subgroupInclusiveAdd on a vec2.
bool sawVec2InclusiveAdd = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpGroupNonUniformFAdd &&
inst.opcode() != spv::Op::OpGroupNonUniformFMin &&
inst.opcode() != spv::Op::OpGroupNonUniformFMax) {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!IsScanOrReduce(static_cast<spv::GroupOperation>(
inst.GetSingleWordInOperand(1)))) {
continue;
}
if (Float32ComponentCount(irContext, inst.type_id()) != 0u) {
sawFloatSubgroupScan = true;
if (inst.opcode() == spv::Op::OpGroupNonUniformFAdd &&
static_cast<spv::GroupOperation>(inst.GetSingleWordInOperand(1)) ==
spv::GroupOperation::InclusiveScan &&
IsVec2Float32(irContext, inst.type_id())) {
sawVec2InclusiveAdd = true;
}
}
}
}
if (!sawFloatSubgroupScan) {
if (!sawVec2InclusiveAdd) {
return Status::SuccessWithoutChange;
}
@@ -291,89 +171,60 @@ namespace MobileGL {
}
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
// The pack indexes its scratch with gl_SubgroupID ITSELF, so only values
// that ARE that id qualify - not everything computed from it. An index
// that is masked or clamped (cache[gl_SubgroupID & 3u]) is bounded by
// construction and is none of this pass's business; accepting it would
// turn a targeted repair into a general array resizer. Identity survives
// OpCopyObject, a signedness OpBitcast, and the Function/Private spill
// glslang emits for a builtin load - and nothing else. A spill variable
// counts only when EVERY store into it is the id.
std::unordered_map<uint32_t, bool> subgroupIdValues; // result id IS the id
std::unordered_map<uint32_t, bool> subgroupIdVariables; // spill holding only it
bool changedIdentity = true;
while (changedIdentity) {
changedIdentity = false;
std::unordered_map<uint32_t, uint32_t> totalStores;
std::unordered_map<uint32_t, uint32_t> idStores;
// Conservative taint walk over values, and through Function/Private
// temporaries by variable (glslang routinely spills builtin loads into
// locals before they reach an index expression). Over-tainting is safe:
// the candidate filter below still demands the exact vec2[32] shape.
std::unordered_map<uint32_t, bool> valueTainted; // result id -> tainted
std::unordered_map<uint32_t, bool> variableTainted; // variable id -> tainted
bool changedTaint = true;
while (changedTaint) {
changedTaint = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpStore) continue;
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
const Instruction* target = defUseMgr->GetDef(pointerId);
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
const spv::Op opcode = inst.opcode();
if (opcode == spv::Op::OpStore) {
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
const Instruction* root =
RootVariable(irContext, inst.GetSingleWordInOperand(0));
if (root == nullptr) continue;
if (!variableTainted.count(root->result_id())) {
variableTainted[root->result_id()] = true;
changedTaint = true;
}
continue;
}
const auto storageClass = static_cast<spv::StorageClass>(
target->GetSingleWordInOperand(0));
if (storageClass != spv::StorageClass::Function &&
storageClass != spv::StorageClass::Private) {
if (inst.result_id() == 0 || valueTainted.count(inst.result_id())) {
continue;
}
totalStores[pointerId] += 1u;
if (subgroupIdValues.count(inst.GetSingleWordInOperand(1))) {
idStores[pointerId] += 1u;
}
}
}
}
for (const auto& entry : totalStores) {
if (entry.second != 0u && idStores[entry.first] == entry.second &&
!subgroupIdVariables.count(entry.first)) {
subgroupIdVariables[entry.first] = true;
changedIdentity = true;
}
}
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.result_id() == 0 ||
subgroupIdValues.count(inst.result_id())) {
continue;
}
bool isSubgroupId = false;
switch (inst.opcode()) {
case spv::Op::OpLoad: {
bool tainted = false;
if (opcode == spv::Op::OpLoad) {
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
isSubgroupId = pointerId == subgroupIdVariableId ||
subgroupIdVariables.count(pointerId) != 0u;
break;
if (pointerId == subgroupIdVariableId) tainted = true;
const Instruction* root = RootVariable(irContext, pointerId);
if (root != nullptr && variableTainted.count(root->result_id())) {
tainted = true;
}
} else {
inst.ForEachInId([&](const uint32_t* operandId) {
if (valueTainted.count(*operandId)) tainted = true;
});
}
case spv::Op::OpCopyObject:
case spv::Op::OpBitcast:
isSubgroupId =
subgroupIdValues.count(inst.GetSingleWordInOperand(0)) != 0u;
break;
default:
break;
}
if (isSubgroupId) {
subgroupIdValues[inst.result_id()] = true;
changedIdentity = true;
if (tainted) {
valueTainted[inst.result_id()] = true;
changedTaint = true;
}
}
}
}
}
if (subgroupIdValues.empty()) {
if (valueTainted.empty()) {
return Status::SuccessWithoutChange;
}
// Fingerprint 3: workgroup-shared float arrays indexed by gl_SubgroupID
// itself - the under-declared prefixSumCache.
// Fingerprint 3: workgroup-shared vec2[32] arrays whose access-chain
// index depends on gl_SubgroupID - the under-declared prefixSumCache.
std::map<uint32_t, Instruction*> candidates;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
@@ -383,7 +234,7 @@ namespace MobileGL {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!subgroupIdValues.count(inst.GetSingleWordInOperand(1))) continue;
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
Instruction* baseVariable =
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
if (baseVariable == nullptr ||
@@ -401,16 +252,7 @@ namespace MobileGL {
return Status::SuccessWithoutChange;
}
// Everything that survives the filter, with the bytes each grown array
// will need. Nothing is mutated until the whole set fits the device's
// shared-memory budget, so a module is never left half-grown.
struct Growth {
Instruction* variable = nullptr;
uint32_t elementTypeId = 0;
uint32_t lengthTypeId = 0;
uint32_t addedBytes = 0;
};
std::vector<Growth> growths;
bool changedModule = false;
for (auto& entry : candidates) {
Instruction* variable = entry.second;
@@ -448,70 +290,18 @@ namespace MobileGL {
continue;
}
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
const uint32_t components = Float32ComponentCount(irContext, elementTypeId);
if (components == 0u) continue;
if (!IsVec2Float32(irContext, elementTypeId)) continue;
const Instruction* lengthConstant =
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (lengthConstant == nullptr || lengthConstant->opcode() != spv::Op::OpConstant) {
uint32_t currentLength = 0;
if (lengthConstant == nullptr ||
lengthConstant->opcode() != spv::Op::OpConstant ||
!((currentLength = lengthConstant->GetSingleWordInOperand(0),
currentLength == kIterationRPScratchLength))) {
continue;
}
const uint32_t currentLength = lengthConstant->GetSingleWordInOperand(0);
// The pack's own assumption holds on this device: the declared array
// already covers every subgroup the workgroup partitions into. That is
// every >= 16-lane device for the shapes iterationRP ships, and those
// modules must pass through byte-identical.
if (currentLength >= requiredLength) continue;
// vec3 strides at its 16-byte alignment, so charge the padded stride.
const uint32_t elementStride = (components == 3u ? 4u : components) * 4u;
growths.push_back(Growth{variable, elementTypeId, lengthConstant->type_id(),
(requiredLength - currentLength) * elementStride});
}
if (growths.empty()) {
return Status::SuccessWithoutChange;
}
// Growing must not push the module past what the device can launch: a
// pipeline that fails to create is worse than the pack's own overrun.
{
uint64_t declaredBytes = 0;
bool sawUnsizeable = false;
for (auto& global : irContext->module()->types_values()) {
if (global.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
spv::StorageClass::Workgroup) {
continue;
}
const Instruction* pointerType = defUseMgr->GetDef(global.type_id());
uint32_t bytes = 0u;
uint32_t alignment = 0u;
if (pointerType == nullptr ||
pointerType->opcode() != spv::Op::OpTypePointer ||
!WorkgroupTypeLayout(irContext, pointerType->GetSingleWordInOperand(1),
&bytes, &alignment)) {
sawUnsizeable = true;
break;
}
declaredBytes = RoundUp(static_cast<uint32_t>(declaredBytes), alignment) + bytes;
}
// A declaration this pass cannot size leaves the total an
// underestimate, so the growth cannot be certified against the device
// limit at all - decline rather than guess.
if (sawUnsizeable) {
return Status::SuccessWithoutChange;
}
for (const Growth& growth : growths) declaredBytes += growth.addedBytes;
const uint32_t deviceBudget = m_maxWorkgroupScratchBytes != 0u
? m_maxWorkgroupScratchBytes
: kMinimumSharedMemoryBytes;
if (declaredBytes > deviceBudget) {
return Status::SuccessWithoutChange;
}
}
for (const Growth& growth : growths) {
// Build the grown array type. All three new instructions are inserted
// immediately BEFORE the variable so definition-before-use holds in the
// module's global section (manager-created instructions append to its
@@ -520,17 +310,17 @@ namespace MobileGL {
// scalar constant is legal SPIR-V); the fresh array type makes the
// pointer type unique by construction, so neither collides with an
// existing declaration.
Instruction* variable = growth.variable;
const uint32_t lengthTypeId = lengthConstant->type_id();
const uint32_t newLengthId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpConstant, growth.lengthTypeId, newLengthId,
irContext, spv::Op::OpConstant, lengthTypeId, newLengthId,
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
{requiredLength}}}));
const uint32_t newArrayTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {growth.elementTypeId}},
{SPV_OPERAND_TYPE_ID, {elementTypeId}},
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
const uint32_t newPointerTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
@@ -541,17 +331,21 @@ namespace MobileGL {
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
variable->SetResultType(newPointerTypeId);
changedModule = true;
}
if (!changedModule) {
return Status::SuccessWithoutChange;
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes) {
return spvtools::Optimizer::PassToken(MakeUnique<FixIterationRPSubgroupScratchPass>(
nativeSubgroupSize, maxWorkgroupScratchBytes));
const Uint32 nativeSubgroupSize) {
return spvtools::Optimizer::PassToken(
MakeUnique<FixIterationRPSubgroupScratchPass>(nativeSubgroupSize));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -16,57 +16,48 @@
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Patches ONE known shader-pack defect: iterationRP hard-sizes the scratch
// its subgroup prefix scans write through prefixSumCache[gl_SubgroupID].
// The pack ships that idiom twice, sized for the >= 16-lane subgroups
// desktop GL drivers give it:
// - the auto-exposure reduction: 32x16 (512 invocations), vec2[32];
// - the RTW importance warp: 1024 invocations, float[64].
// On a narrower Vulkan device (lavapipe's 8 lanes -> 64 and 128 subgroups)
// every subgroup past the last declared entry indexes shared memory out of
// bounds - on a CPU rasterizer that is literal heap corruption. Both
// reduction ALGORITHMS are width-agnostic (their combine loops are sized by
// gl_NumSubgroups), so the faithful repair is to grow the under-declared
// arrays to ceil(invocations / native width) and change nothing else.
// Patches ONE known shader-pack defect: iterationRP's auto-exposure reduction
// declares `shared vec2 prefixSumCache[32]` for its 512-invocation workgroup
// and stores per-subgroup subtotals through prefixSumCache[gl_SubgroupID].
// The pack hard-sized that scratch for the >=16-lane subgroups desktop GL
// drivers ship; on a narrower Vulkan device (lavapipe's 8 lanes -> 64
// subgroups) every subgroup past entry 31 indexes shared memory out of
// bounds - on a CPU rasterizer that is literal heap corruption. The
// reduction ALGORITHM is width-agnostic (its combine loop is sized by
// gl_NumSubgroups), so the faithful repair is to grow the one under-declared
// array to ceil(512 / native width) and change nothing else. This is the
// pack author's bug, not MobileGL's; the patch is therefore deliberately
// NOT a general mechanism - it only rewrites modules that positively match
// iterationRP's reduction fingerprint:
// - GLCompute entry point with local size exactly 32x16x1;
// - a subgroupInclusiveAdd on a vec2 (OpGroupNonUniformFAdd InclusiveScan,
// the pack's luminance/exposure accumulator signature);
// - a workgroup-shared array of exactly vec2[32] whose access-chain index
// is data-dependent on gl_SubgroupID.
// Matching at the SPIR-V level keeps the recognition robust against
// whitespace/identifier-level drift that made the old source-text template
// rewrite (removed in 7769156) so brittle, while still refusing to touch
// anything that is not this pack's reduction. On devices whose native width
// already satisfies the pack's assumption (>= 16 lanes: desktop GL, Adreno),
// the grown length equals or undershoots the declared 32 and every module
// passes through byte-identical.
//
// This is the pack author's bug, not MobileGL's, so the patch is
// deliberately NOT a general "resize shared arrays" mechanism. It rewrites
// an array only when the module positively matches the pack's reduction
// idiom AND the device's own topology proves the declaration too small:
// - GLCompute entry point with a literal workgroup size;
// - a subgroup scan/reduce over a 32-bit float scalar or vector
// (OpGroupNonUniformF{Add,Min,Max}), the pack's accumulator signature;
// - a workgroup-shared array of 32-bit float scalars/vectors whose
// access-chain index is data-dependent on gl_SubgroupID;
// - a declared length strictly below ceil(invocations / native width).
// That last clause is what keeps the patch inert wherever the pack is
// correct: on any device whose width satisfies the pack's assumption
// (>= 16 lanes: desktop GL, Adreno) both shapes already fit and every
// module passes through byte-identical. Matching at the SPIR-V level keeps
// recognition robust against the whitespace/identifier drift that made the
// old source-text template rewrite (removed in 7769156) so brittle.
//
// The pass never fails a module: anything it cannot prove is this pattern -
// or cannot grow safely (a whole-array use, a spec-constant length, an
// initializer, or growth that would not fit maxWorkgroupScratchBytes) - is
// left exactly as it was. Pass the device's maxComputeSharedMemorySize as
// maxWorkgroupScratchBytes; 0 falls back to the 16384-byte Vulkan minimum.
// The pass never fails a module: anything it cannot prove is this exact
// pattern - or cannot grow safely (a whole-array use, a spec-constant
// length, an initializer) - is left exactly as it was.
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
public:
FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes)
: m_nativeSubgroupSize(nativeSubgroupSize),
m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
explicit FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize)
: m_nativeSubgroupSize(nativeSubgroupSize) {}
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
Uint32 nativeSubgroupSize, Uint32 maxWorkgroupScratchBytes);
Uint32 nativeSubgroupSize);
private:
Uint32 m_nativeSubgroupSize;
Uint32 m_maxWorkgroupScratchBytes;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -1,250 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.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 "LowerViewportIndexPass.h"
#include "spirv.hpp"
#include "source/opt/build_module.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.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::IRContext;
using spvtools::opt::Operand;
// The name the decompiled ESSL ends up declaring. Same mg_ prefix as the
// draw-parameter lowering, so a global that came from a demoted builtin is
// recognisable in a driver log.
constexpr const char* kLoweredName = "mg_ViewportIndex";
// The one decoration this pass lowers. OpDecorate only, never OpMemberDecorate:
// glslang emits gl_ViewportIndex as a standalone variable, and a member of a
// gl_PerVertex-shaped block could not be demoted on its own anyway. BuiltIn Layer
// is deliberately not matched - see the header.
Bool IsViewportIndexBuiltinDecoration(const Instruction& annotation) {
if (annotation.opcode() != spv::Op::OpDecorate ||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
return false;
}
return static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) ==
spv::BuiltIn::ViewportIndex;
}
// The OUTPUT variable that decoration names, or nullptr. Only an output is
// demotable: a fragment stage READS gl_ViewportIndex as an Input, and a Private
// global has no defined value to read, so lowering that one would answer the
// shader with garbage instead of the viewport it asked for. That case is left for
// the driver to reject.
Instruction* GetDecoratedViewportIndexOutput(IRContext* context,
const Instruction& annotation) {
Instruction* variable =
context->get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
spv::StorageClass::Output) {
return nullptr;
}
return variable;
}
// Decorations the validator accepts only on an Input/Output variable, so they have
// to go with the storage class or the demoted module stops validating. glslang
// puts none of these on gl_ViewportIndex today - the BuiltIn is all it writes -
// but a geometry `layout(stream = N)` qualifier decorates every output of the
// stage, and the pass must not be the thing that produces an invalid module.
Bool IsInterfaceOnlyDecoration(spv::Decoration decoration) {
switch (decoration) {
case spv::Decoration::Flat:
case spv::Decoration::NoPerspective:
case spv::Decoration::Centroid:
case spv::Decoration::Sample:
case spv::Decoration::Patch:
case spv::Decoration::Invariant:
case spv::Decoration::Location:
case spv::Decoration::Component:
case spv::Decoration::Stream:
case spv::Decoration::XfbBuffer:
case spv::Decoration::XfbStride:
return true;
default:
return false;
}
}
void ReplaceName(IRContext* context, uint32_t id, const char* name) {
for (auto& debugInst : context->debugs2()) {
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id) {
debugInst.SetInOperand(
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(name));
return;
}
}
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpName, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {id}},
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
}
void RemoveFromEntryPointInterfaces(IRContext* context, uint32_t id) {
for (Instruction& entryPoint : context->module()->entry_points()) {
std::vector<Operand> newOperands;
Bool changed = false;
for (uint32_t i = 0; i < entryPoint.NumInOperands(); ++i) {
const Operand& operand = entryPoint.GetInOperand(i);
// Interface ids start after execution model, entry-point id and name.
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
entryPoint.GetSingleWordInOperand(i) == id) {
changed = true;
continue;
}
newOperands.push_back(operand);
}
if (changed) {
entryPoint.SetInOperands(std::move(newOperands));
}
}
}
} // namespace
bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
if (binary.empty()) {
// An empty module is a stage that produced no SPIR-V, which is not a verdict
// about viewport routing; letting BuildModule reject it would push a spurious
// diagnostic through the message consumer first.
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) {
// Unparseable here means unusable downstream too; let the ordinary transpile
// path produce the error rather than inventing a verdict from it.
return false;
}
return DeclaresViewportIndexBuiltin(context.get());
}
bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(IRContext* context) {
for (const Instruction& annotation : context->annotations()) {
if (IsViewportIndexBuiltinDecoration(annotation) &&
GetDecoratedViewportIndexOutput(context, annotation) != nullptr) {
return true;
}
}
return false;
}
spvtools::opt::Pass::Status LowerViewportIndexPass::Process() {
auto* irContext = context();
// Collect the decorations to lower first; mutating while iterating annotations
// invalidates the range.
struct LoweredVariable {
Instruction* variable = nullptr;
Instruction* decoration = nullptr;
};
std::vector<LoweredVariable> targets;
for (auto& annotation : irContext->annotations()) {
if (!IsViewportIndexBuiltinDecoration(annotation)) {
continue;
}
Instruction* variable = GetDecoratedViewportIndexOutput(irContext, annotation);
if (variable == nullptr) {
continue;
}
targets.push_back({variable, &annotation});
}
if (targets.empty()) {
return Status::SuccessWithoutChange;
}
// Second collection pass, for the same reason as the first: the decorations that
// stop being legal once the variable leaves the Output storage class.
std::vector<Instruction*> deadDecorations;
for (auto& annotation : irContext->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate ||
!IsInterfaceOnlyDecoration(
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)))) {
continue;
}
const uint32_t decoratedId = annotation.GetSingleWordInOperand(0);
for (const auto& target : targets) {
if (target.variable->result_id() == decoratedId) {
deadDecorations.push_back(&annotation);
break;
}
}
}
auto* defUseMgr = irContext->get_def_use_mgr();
auto* typeMgr = irContext->get_type_mgr();
for (auto& target : targets) {
Instruction* variable = target.variable;
const uint32_t variableId = variable->result_id();
// Demote the Output builtin to a plain Private global. Every store the shader
// already makes stays exactly where it is - it simply no longer reaches the
// rasterizer, which is the whole of the degradation.
Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
const uint32_t pointeeTypeId = pointerType->GetSingleWordInOperand(1);
const uint32_t privatePointerTypeId =
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private);
variable->SetResultType(privatePointerTypeId);
variable->SetInOperand(0, {static_cast<uint32_t>(spv::StorageClass::Private)});
// FindPointerToType APPENDS a newly minted pointer type to the end of the
// globals section - after this variable - and SPIR-V requires def before use.
// Re-anchor the variable directly after its new type, which is equally correct
// when the type already existed further up.
Instruction* privatePointerType = defUseMgr->GetDef(privatePointerTypeId);
variable->RemoveFromList();
variable->InsertAfter(privatePointerType);
irContext->KillInst(target.decoration);
RemoveFromEntryPointInterfaces(irContext, variableId);
ReplaceName(irContext, variableId, kLoweredName);
}
for (auto* decoration : deadDecorations) {
irContext->KillInst(decoration);
}
// The MultiViewport / ShaderViewportIndexLayerEXT capabilities are deliberately
// left declared, unlike DrawParameters in the sibling pass. They are not exclusive
// to this builtin: ShaderViewportIndexLayerEXT also enables gl_Layer in the
// pre-geometry stages, and it DEPENDS on MultiViewport, so dropping either can
// invalidate a module that still writes Layer. A declared-but-unused capability is
// legal SPIR-V and SPIRV-Cross's GLSL backend reads neither of them, so leaving
// both costs nothing.
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken LowerViewportIndexPass::CreateLowerViewportIndexPass() {
return spvtools::Optimizer::PassToken(MakeUnique<LowerViewportIndexPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,55 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.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>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// ESSL has no core gl_ViewportIndex at any version - only GL_OES_viewport_array
// introduces it - and SPIRV-Cross prints the identifier bare, requesting no extension
// for it (contrast BuiltInLayer, which it backs with GL_NV_viewport_array2 on ES). On
// a driver WITHOUT that extension the stage therefore fails to compile, DirectGLES
// marks the program unusable and binds program 0 for it, and every draw silently
// renders nothing while GL_LINK_STATUS still answers TRUE - the failure signature
// KHR-GL4x.viewport_array reports as "expected N, got -1", i.e. the untouched upload.
//
// This pass demotes the ViewportIndex OUTPUT to a plain Private global named
// mg_ViewportIndex, so the decompiled ESSL declares an ordinary global the shader
// still writes and nothing reads. The program compiles and rendering degrades to
// viewport 0 - which is the single-viewport behaviour MG_IntegrationTest's
// ViewportArrayScenario already documents for this backend - instead of the whole
// program becoming a no-op. Only meant for the DirectGLES transpile path; the Vulkan
// backend keeps the native builtin and routes it for real.
//
// gl_Layer is deliberately NOT touched: BuiltIn Layer IS core in ESSL 3.20 geometry
// shaders, and demoting it would break layered rendering that works today.
class LowerViewportIndexPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "lower-viewport-index"; }
Status Process() override;
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
// whether running this pass could change anything. Answered from a single parse so
// the caller can skip the optimizer round trip entirely - which is every shader
// but the handful that route viewports from the shader.
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
// Same question answered from an already-built module, so one parse can feed
// several gates (ShaderCompiler::ProbeSpirvGateFeatures).
static bool DeclaresViewportIndexBuiltin(spvtools::opt::IRContext* context);
static spvtools::Optimizer::PassToken CreateLowerViewportIndexPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -138,11 +138,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInternalFormat::DepthComponent32F:
out = {1, ShadowComponent::Float32, false};
return true;
// Stencil is the one single-channel INTEGER shadow that is not a colour format: eight
// bits, held as an unsigned index rather than a normalized value.
case TextureInternalFormat::StencilIndex8:
out = {1, ShadowComponent::UInt8, true};
return true;
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
@@ -341,13 +336,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
// A depth value converts like a single normalized/float channel.
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
// A stencil index is a single INTEGER channel (GL 4.6 core 8.4.4.3). Without this the
// upload fell to the raw-memcpy branch, which copies the client element width into the
// one-byte STENCIL_INDEX8 shadow verbatim - right for GL_UNSIGNED_BYTE and wrong for
// every wider type. The state layer keeps this paired with stencil-only storage.
case TextureInputFormat::StencilIndex: out = {{0, -1, -1, -1}, 1, true}; return true;
default:
return false; // packed depth-stencil / unknown
return false; // stencil / packed depth-stencil / unknown
}
}
@@ -816,14 +806,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
}
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat) {
InternalPackedLayout packedInternal{};
if (!GetInternalPackedLayout(internalFormat, packedInternal)) {
return false;
}
return packedInternal.kind == PackedInternalKind::FloatRGB9E5;
}
// assume 8 bit per channel
// swizzle.size() == channel count
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
@@ -1008,11 +990,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
const void* inputPixel,
Vector<Uint8>& outputPixel) {
outputPixel.clear();
// A stencil index became a transferable format when STENCIL_INDEX8 texture storage did (see
// GetUnpackChannelMapping), but this helper serves glClearBufferData, whose internal formats
// are all colour (GL 4.6 core table 8.20): a stencil pattern would otherwise pass the size
// check and land silently in an equally-sized colour store.
if (textureInputFormat == TextureInputFormat::StencilIndex) return false;
if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
PixelStoreParameters params{};
@@ -44,17 +44,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType);
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
// canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32
// bit-exactly, so a GPU readback can answer for them.
//
// This is what decides whether the CPU shadow has to stay authoritative for a format: a
// readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no
// matter how well behaved the driver is.
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set
@@ -164,21 +164,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
} else {
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
}
if (request.fixIterationRPSubgroupScratch) {
setenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
} else {
unsetenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
}
if (request.deriveNumSubgroups) {
setenv("MOBILEGL_DERIVE_NUM_SUBGROUPS", "1", 1);
} else {
unsetenv("MOBILEGL_DERIVE_NUM_SUBGROUPS");
}
if (request.iterationRPFixBarrier) {
setenv("MOBILEGL_ITERATIONRP_FIX_BARRIER", "1", 1);
} else {
unsetenv("MOBILEGL_ITERATIONRP_FIX_BARRIER");
}
if (request.fboAttachmentDumps.empty()) {
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
} else {
@@ -833,10 +818,6 @@ bool WriteResultJson(const Request& request, const Result& result) {
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
file << " \"fixIterationRPSubgroupScratch\": " << (request.fixIterationRPSubgroupScratch ? "true" : "false")
<< ",\n";
file << " \"deriveNumSubgroups\": " << (request.deriveNumSubgroups ? "true" : "false") << ",\n";
file << " \"iterationRPFixBarrier\": " << (request.iterationRPFixBarrier ? "true" : "false") << ",\n";
file << " \"holdMs\": " << request.holdMs << ",\n";
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
file << "}\n";
@@ -44,9 +44,6 @@ struct Request {
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
bool avoidAngleLlvmpipeExplicitLodBias = false;
bool coherentAsFlush = false;
bool fixIterationRPSubgroupScratch = false;
bool deriveNumSubgroups = false;
bool iterationRPFixBarrier = false;
int holdMs = 0;
};
@@ -122,9 +122,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
jboolean avoidAngleLlvmpipeExplicitLodBias,
jboolean coherentAsFlush,
jboolean fixIterationRPSubgroupScratch,
jboolean deriveNumSubgroups,
jboolean iterationRPFixBarrier,
jstring texture2dDumps) {
mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath);
@@ -153,9 +150,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
request.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch == JNI_TRUE;
request.deriveNumSubgroups = deriveNumSubgroups == JNI_TRUE;
request.iterationRPFixBarrier = iterationRPFixBarrier == JNI_TRUE;
ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height);
@@ -116,9 +116,6 @@ public final class TraceReplayActivity extends Activity {
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
request.avoidAngleLlvmpipeExplicitLodBias,
request.coherentAsFlush,
request.fixIterationRPSubgroupScratch,
request.deriveNumSubgroups,
request.iterationRPFixBarrier,
request.texture2dDumps
);
Log.i(TAG, result.toString());
@@ -152,9 +149,6 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
String texture2dDumps
);
@@ -180,9 +174,6 @@ public final class TraceReplayActivity extends Activity {
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
final boolean avoidAngleLlvmpipeExplicitLodBias;
final boolean coherentAsFlush;
final boolean fixIterationRPSubgroupScratch;
final boolean deriveNumSubgroups;
final boolean iterationRPFixBarrier;
final String texture2dDumps;
private TraceReplayRequest(
@@ -207,9 +198,6 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
String texture2dDumps
) {
this.tracePath = tracePath;
@@ -233,9 +221,6 @@ public final class TraceReplayActivity extends Activity {
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
this.coherentAsFlush = coherentAsFlush;
this.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch;
this.deriveNumSubgroups = deriveNumSubgroups;
this.iterationRPFixBarrier = iterationRPFixBarrier;
this.texture2dDumps = texture2dDumps;
}
@@ -264,9 +249,6 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
intent.getBooleanExtra("coherent_as_flush", false),
intent.getBooleanExtra("fix_iterationrp_subgroup_scratch", false),
intent.getBooleanExtra("derive_num_subgroups", false),
intent.getBooleanExtra("iterationrp_fix_barrier", false),
readString(intent, "texture_2d_dumps", "")
);
}
-12
View File
@@ -40,9 +40,6 @@ Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
DirectGLES replay.
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
against an offscreen EGL pbuffer instead of the Activity surface.
Set MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
MOBILEGL_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_ITERATIONRP_FIX_BARRIER=1 to
forward the corresponding iterationRP SPIR-V repairs into the APK process.
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
@@ -366,15 +363,6 @@ run_retrace() {
if [ "${coherent_as_flush}" -eq 1 ]; then
set -- "$@" --ez coherent_as_flush true
fi
if [ "${MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
fi
if [ "${MOBILEGL_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
set -- "$@" --ez derive_num_subgroups true
fi
if [ "${MOBILEGL_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
set -- "$@" --ez iterationrp_fix_barrier true
fi
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
+3 -31
View File
@@ -86,19 +86,6 @@ def main():
ap.add_argument("--outdir", required=True)
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
# Without an explicit size, dEQP's FboRenderContext sizes the wrapper FBO to
# GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test allocations
# explode (a 4-sample 16K depth texture alone is 4 GiB).
ap.add_argument("--surface-size", type=int, default=256,
help="--deqp-surface-width/height value")
# With DONT_CARE depth/stencil bits dEQP's FboRenderContext picks the first entry of
# its own format list, GL_DEPTH32F_STENCIL8. framebuffer_blit meanwhile hardcodes
# GL_DEPTH24_STENCIL8 for its own buffers whenever it detects an FBO surface, then
# blits depth between the two - which the spec forbids for mismatched formats, so a
# conformant driver has to fail it. Asking for a config the test agrees with avoids
# the contradiction instead of papering over it.
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
help="--deqp-gl-config-name value (empty string to leave it unset)")
ap.add_argument("--max-rounds", type=int, default=4000)
ap.add_argument("--max-empty-streak", type=int, default=64,
help="abort after this many consecutive chunks that produce no log at all")
@@ -164,22 +151,14 @@ def main():
adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60)
extra_env = "".join(f"{kv} " for kv in args.env)
config_flag = (
f"--deqp-gl-config-name={args.gl_config_name} " if args.gl_config_name else ""
)
# The trailing sync makes the qpa durable: a hard GPU hang reboots the
# device, and f2fs rolls back unsynced writes, silently eating the log.
cmd = (
f"cd {args.device_dir} && "
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
f"./glcts --deqp-caselist-file={dev_list} "
f"--deqp-surface-type={args.surface} "
f"--deqp-surface-width={args.surface_size} "
f"--deqp-surface-height={args.surface_size} "
f"{config_flag}"
f"--deqp-terminate-on-device-lost=disable "
f"--deqp-log-images=disable --deqp-log-shader-sources=disable "
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; rc=$?; sync; echo RC=$rc"
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?"
)
run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout)
if run.returncode == 124:
@@ -245,16 +224,9 @@ def main():
f"to label the rest of the suite as crashes.", file=sys.stderr)
break
# No log at all. If the device rebooted, the first unrun case took the
# whole device down (a reboot can also roll back the freshly created
# qpa on f2fs) - that is a hang to quarantine, not a process crash.
victim = remaining[0]
if rebooted:
print(f"[run_cts] DEVICE HANG in {victim} (no log at all) - quarantining it")
hung.append(victim)
else:
print(f"[run_cts] no output at all; recording {victim} as Crash")
crashed.append(victim)
print(f"[run_cts] no output at all; recording {victim} as Crash")
crashed.append(victim)
done.add(victim)
progressed = 1
+1 -3
View File
@@ -46,10 +46,8 @@ DEFAULT_DEQP_ARGS = (
"--deqp-gl-context-type=wgl",
"--deqp-surface-type=fbo",
"--deqp-gl-config-name=rgba8888d24s8",
# Height <= 0 is DONT_CARE, which FboRenderContext resolves to
# GL_MAX_RENDERBUFFER_SIZE - a 64x16384 surface. Pin both.
"--deqp-surface-width=64",
"--deqp-surface-height=64",
"--deqp-surface-height=-1",
"--deqp-base-seed=3",
"--deqp-visibility=hidden",
"--deqp-watchdog=enable",
-8
View File
@@ -72,16 +72,8 @@ bool ReadDouble(int argc, char **argv, int &index, double &out) {
return true;
}
bool ReadEnvFlag(const char *name) {
const char *value = std::getenv(name);
return value != nullptr && std::string(value) == "1";
}
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
request.backend = "DirectGLES";
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_DERIVE_NUM_SUBGROUPS");
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];