mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31a5da6190 | ||
|
|
8899f065f4 | ||
|
|
a991f63899 | ||
|
|
3ff9cfe5c2 | ||
|
|
c6d1b29407 | ||
|
|
0995dfea35 | ||
|
|
042c61fb75 | ||
|
|
a8bebe1a3c | ||
|
|
85cd6913b3 | ||
|
|
afebf38e90 | ||
|
|
898c39f1de | ||
|
|
b1774e80be | ||
|
|
a9b4c47fea | ||
|
|
1c0be3e715 | ||
|
|
27ec3d3438 | ||
|
|
52718ecf84 | ||
|
|
baeb2fa1bc | ||
|
|
54a88ef1e2 | ||
|
|
ee124018a2 | ||
|
|
d7ce0c48ef | ||
|
|
0b3101bf6b | ||
|
|
a4fda520ed | ||
|
|
f0fd6407ae | ||
|
|
bde14cae29 | ||
|
|
f2f6430e34 | ||
|
|
15e36ad1e9 | ||
|
|
7940a09491 | ||
|
|
1e8d4661e6 | ||
|
|
916702629e | ||
|
|
9b37c77ae2 | ||
|
|
085eb5835b | ||
|
|
56377d2025 | ||
|
|
7d2c16a90e | ||
|
|
261cfd1591 | ||
|
|
bbc7b9ca84 | ||
|
|
c1b3b16cab | ||
|
|
f17cb23ea3 | ||
|
|
f297af7d2b | ||
|
|
d9abf1c2c1 | ||
|
|
392736fb6b | ||
|
|
0944925679 | ||
|
|
eadf7bc474 | ||
|
|
00a326ef78 | ||
|
|
c09045fe59 | ||
|
|
5bd8ef01e5 | ||
|
|
3181ed2c5a | ||
|
|
6aed3b08f3 | ||
|
|
281467a345 | ||
|
|
c7e36986e7 | ||
|
|
d8576a2ed3 | ||
|
|
2b6c2b561c | ||
|
|
12c94111b5 | ||
|
|
7769156cfc | ||
|
|
0ecfdff4e7 | ||
|
|
6df5a6137f | ||
|
|
b3794f4e6a | ||
|
|
14d3901d30 | ||
|
|
d4766513e4 | ||
|
|
72dc7aa6aa |
@@ -209,7 +209,7 @@ jobs:
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: |
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
@@ -337,13 +337,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
backend:
|
||||
- name: DirectGLES
|
||||
gpu: software
|
||||
- name: DirectVulkan
|
||||
gpu: lavapipe
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
@@ -426,6 +420,9 @@ 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}"
|
||||
|
||||
@@ -265,6 +265,9 @@ 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'
|
||||
@@ -491,6 +494,7 @@ jobs:
|
||||
- benchmark
|
||||
- integration
|
||||
outputs:
|
||||
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -498,7 +502,9 @@ jobs:
|
||||
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
run: |
|
||||
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
name: trace fixture (${{ matrix.case }})
|
||||
@@ -577,11 +583,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
backend:
|
||||
- DirectGLES
|
||||
- DirectVulkan
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
@@ -640,6 +642,12 @@ 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
|
||||
|
||||
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/.gradle
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: 10935bb5e4...c8036190fc
+15
-1
@@ -279,14 +279,20 @@ 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
|
||||
@@ -298,6 +304,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
@@ -458,7 +465,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/asio/include
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
)
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
@@ -670,3 +677,10 @@ 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()
|
||||
|
||||
+37
-10
@@ -66,22 +66,53 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||
// SpirvValidationEnabled).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// FixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
// bounds. The pass grows that one array to what the device's topology needs and
|
||||
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// 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
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
@@ -130,10 +161,6 @@ namespace MobileGL::MG_Config {
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
|
||||
@@ -162,11 +162,17 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
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");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
@@ -179,7 +185,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
|
||||
@@ -51,6 +52,11 @@ namespace MobileGL {
|
||||
// before a re-initialized library could pair them with the wrong
|
||||
// backend's DeleteSync).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
// Queries die with their contexts for the same reason, and their registry
|
||||
// is the same shape of process-global map: drain it here too, while the
|
||||
// function table can still pair each backend handle with the backend that
|
||||
// minted it.
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
class FramebufferObject;
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
}
|
||||
|
||||
enum class BackendType {
|
||||
@@ -24,6 +25,19 @@ 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,
|
||||
|
||||
@@ -160,9 +174,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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void (*GenerateMipmap)(GLenum target);
|
||||
@@ -236,6 +250,14 @@ 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
|
||||
@@ -318,6 +340,22 @@ 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,6 +8,7 @@
|
||||
|
||||
#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>
|
||||
@@ -406,9 +407,12 @@ 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) {
|
||||
Bool* outRenderable, Int samples = 1) {
|
||||
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
|
||||
return false;
|
||||
}
|
||||
@@ -428,10 +432,11 @@ 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, 1, internalFormat, 1, 1, GL_TRUE);
|
||||
gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
|
||||
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
|
||||
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
|
||||
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
|
||||
} else {
|
||||
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
|
||||
gl.glDeleteTextures(1, &texture);
|
||||
@@ -527,6 +532,29 @@ 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) {
|
||||
@@ -627,7 +655,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
|
||||
if (IsGLESProbeMultisampleTarget(target)) {
|
||||
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
||||
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
||||
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||
nativeInfo.ImageType, logicalFormat, maxSamples);
|
||||
}
|
||||
}
|
||||
shouldProbeFallback = !nativeCreated || !nativeRenderable;
|
||||
@@ -645,7 +677,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
||||
}
|
||||
if (IsGLESProbeMultisampleTarget(target)) {
|
||||
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
||||
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||
fallbackInfo.ImageType, logicalFormat, maxSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,6 +783,29 @@ 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();
|
||||
}
|
||||
@@ -932,7 +991,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
||||
@@ -1107,6 +1166,12 @@ 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;
|
||||
@@ -1186,9 +1251,31 @@ 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;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
|
||||
// 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,6 +18,16 @@ 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;
|
||||
|
||||
@@ -300,10 +300,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Clear();
|
||||
}
|
||||
#else
|
||||
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {}
|
||||
void ErrorLopper::Clear() {}
|
||||
ErrorLopper::ErrorLopper() = default;
|
||||
ErrorLopper::~ErrorLopper() = default;
|
||||
// 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();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
@@ -1319,6 +1341,16 @@ 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 ||
|
||||
@@ -1374,10 +1406,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
|
||||
const GLboolean layered =
|
||||
SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()) ? imageBinding.Layered : GL_FALSE;
|
||||
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
|
||||
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
|
||||
const GLint layer = layerable ? imageBinding.Layer : 0;
|
||||
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
||||
layered, imageBinding.Layer, imageBinding.Access, imageBinding.Format);
|
||||
layered, layer, imageBinding.Access, imageBinding.Format);
|
||||
}
|
||||
|
||||
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
||||
@@ -1431,6 +1464,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_fboSyncedSlotVersions[SizeT(target)] = slotVersion;
|
||||
g_fboSyncedObjectVersions[SizeT(target)] = objectVersion;
|
||||
g_fboSyncedObjects[SizeT(target)] = fbo;
|
||||
g_fboSyncedBackendIdGenerations[SizeT(target)] = g_attachmentBackendIdGeneration;
|
||||
}
|
||||
|
||||
void SyncCurrentFBO() {
|
||||
@@ -1461,9 +1495,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Uint16 slotVersion = slot.GetVersion();
|
||||
const Uint16 objectVersion = currentFBO ? currentFBO->GetObjectVersion() : 0;
|
||||
auto* currentPtr = currentFBO.get();
|
||||
// The backend-id generation joins the triple: a backend texture re-mint
|
||||
// (RecreateBackendTexture) moves no frontend version, so without it the
|
||||
// early-out would keep the driver FBO on the deleted texture name.
|
||||
if (slotVersion == g_fboSyncedSlotVersions[SizeT(target)] &&
|
||||
objectVersion == g_fboSyncedObjectVersions[SizeT(target)] &&
|
||||
currentPtr == g_fboSyncedObjects[SizeT(target)]) {
|
||||
currentPtr == g_fboSyncedObjects[SizeT(target)] &&
|
||||
g_fboSyncedBackendIdGenerations[SizeT(target)] == g_attachmentBackendIdGeneration) {
|
||||
lastUpdatedFBO = currentPtr;
|
||||
continue;
|
||||
}
|
||||
@@ -2129,6 +2167,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool g_broadcastMemoValid = false;
|
||||
static Uint g_broadcastMemoCount = 1;
|
||||
|
||||
// The identity+version key above is only monotonic WITHIN one GLContext: a
|
||||
// library teardown + re-init frees every FramebufferObject and restarts the
|
||||
// draw slot's counter at zero, so a recycled FBO address with coinciding
|
||||
// fresh versions would false-hit. Cleared at the same boundaries as the
|
||||
// structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache).
|
||||
void InvalidateBroadcastMemo() {
|
||||
g_broadcastMemoValid = false;
|
||||
}
|
||||
|
||||
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -2312,6 +2359,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
|
||||
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
|
||||
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
|
||||
FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] =
|
||||
FramebufferImpl::g_attachmentBackendIdGeneration;
|
||||
}
|
||||
|
||||
static void BindCurrentProgramWithResources(
|
||||
@@ -3829,11 +3878,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
||||
}
|
||||
|
||||
static void DrainBlitErrors() {
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
// 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() { DrainDriverErrors("BlitFramebuffer"); }
|
||||
|
||||
// Sized internal format of the currently bound READ framebuffer's read colour
|
||||
// attachment, 0 when it cannot be determined.
|
||||
static GLenum QueryReadColorAttachmentInternalFormat() {
|
||||
@@ -3950,8 +4016,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
if (resolved) {
|
||||
DrainBlitErrors();
|
||||
// A blit is scissored like a draw (the replicate path's guard documents the
|
||||
// same rule): the application's box would clip this resolve into the
|
||||
// scratch, and the second blit would then copy never-written scratch texels
|
||||
// into the destination - silently, since scissor clipping raises no GL
|
||||
// error. Disable for the staging blit only; the caller-visible blit below
|
||||
// keeps the blit's native scissor semantics. Tracked via the render-state
|
||||
// shadow, exactly like ScopedScissorDisable.
|
||||
const Bool scissorWasEnabled =
|
||||
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
|
||||
GL_NEAREST);
|
||||
if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
||||
resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
if (resolved) {
|
||||
@@ -4097,13 +4174,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
// The per-draw-buffer colour masks are not covered by the non-indexed
|
||||
// glColorMask above.
|
||||
for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) {
|
||||
const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index];
|
||||
if (g_GLESFuncs.glColorMaski) {
|
||||
g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE,
|
||||
colorMask.y() ? GL_TRUE : GL_FALSE, colorMask.z() ? GL_TRUE : GL_FALSE,
|
||||
colorMask.w() ? GL_TRUE : GL_FALSE);
|
||||
// glColorMask above. Restore what the SYNC actually pushed, not the raw
|
||||
// application masks: a widened attachment's alpha write is forced off by
|
||||
// SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the
|
||||
// next sync early-outs on an unchanged version - restoring the undoctored
|
||||
// mask here would leave alpha writes enabled on the widened buffer with
|
||||
// nothing left to repair it. Same three-way pointer fallback as
|
||||
// SyncRenderState's push: gating on the core name alone left EXT/OES-only
|
||||
// devices holding buffer 0's mask broadcast across every buffer.
|
||||
const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski
|
||||
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
|
||||
: g_GLESFuncs.glColorMaskiOES;
|
||||
if (colorMaskiFn) {
|
||||
for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) {
|
||||
BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index];
|
||||
if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) {
|
||||
colorMask.w() = false;
|
||||
}
|
||||
colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE,
|
||||
colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
}
|
||||
if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) {
|
||||
@@ -4585,16 +4674,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (readSamples <= 0 || drawSamples > 0 || (mask & GL_COLOR_BUFFER_BIT) == 0) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
// 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 (dsBits != 0) {
|
||||
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,
|
||||
@@ -4951,9 +5070,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ClearGLErrors() {
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
|
||||
}
|
||||
static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); }
|
||||
|
||||
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
|
||||
// scope (GenerateMipmap must respecify texture storage while no incomplete
|
||||
@@ -5591,27 +5708,87 @@ 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;
|
||||
}
|
||||
};
|
||||
|
||||
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
|
||||
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||
GLESCopyImageEndpoint endpoint{};
|
||||
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
endpoint.x = x;
|
||||
endpoint.y = 0;
|
||||
endpoint.z = y;
|
||||
return endpoint;
|
||||
// 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;
|
||||
}
|
||||
endpoint.x = x;
|
||||
endpoint.y = y;
|
||||
endpoint.z = z;
|
||||
return endpoint;
|
||||
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;
|
||||
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
out.x = x;
|
||||
out.y = 0;
|
||||
out.z = y;
|
||||
return true;
|
||||
}
|
||||
out.x = x;
|
||||
out.y = y;
|
||||
out.z = z;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||
@@ -5627,85 +5804,172 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::swap(height, depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
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,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
// 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);
|
||||
GLESCopyImageEndpoint src{};
|
||||
GLESCopyImageEndpoint dst{};
|
||||
// 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 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__);
|
||||
// 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__);
|
||||
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 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) {
|
||||
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)) {
|
||||
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(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
if (srcTexture->GetFormat() == TextureInternalFormat::R32F ||
|
||||
dstTexture->GetFormat() == TextureInternalFormat::R32F) {
|
||||
if (!anyRenderbuffer &&
|
||||
(srcFormat == TextureInternalFormat::R32F || dstFormat == 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(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
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,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError == GL_NO_ERROR) {
|
||||
return;
|
||||
}
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
|
||||
"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(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dst.Name(), dst.target, dstLevel, dst.x, dst.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearGLErrors();
|
||||
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,
|
||||
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,
|
||||
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
|
||||
@@ -5721,6 +5985,14 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7011,10 +7283,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
data = std::move(expanded);
|
||||
}
|
||||
|
||||
static void DrainESErrors() {
|
||||
for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
static void DrainESErrors() { DrainDriverErrors("ReadPixels"); }
|
||||
|
||||
static GLenum QueryReadAttachmentComponentType() {
|
||||
GLint framebufferId = 0;
|
||||
@@ -7647,6 +7916,30 @@ 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
|
||||
@@ -8304,6 +8597,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Conservatively drop the redundant-glUseProgram guard: re-issuing one bind
|
||||
// after a MakeCurrent is cheaper than trusting a possibly-reset context.
|
||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||
// The GLContext becoming current may be a fresh one whose slot versions
|
||||
// restarted at zero; the broadcast memo's key is only monotonic within one.
|
||||
PrgramImpl::InvalidateBroadcastMemo();
|
||||
BufferImpl::InvalidateIndexedBufferBindingCache();
|
||||
BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
@@ -8832,6 +9128,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||
PixelStoreImpl::InvalidatePackStateCache();
|
||||
PrgramImpl::InvalidateBroadcastMemo();
|
||||
// Texture ids belong to the dying context; wrappers destroyed later must
|
||||
// not glDeleteTextures a recycled name in a successor context.
|
||||
++g_backendContextGeneration;
|
||||
|
||||
@@ -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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Managers.h"
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
#include <Config.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
|
||||
@@ -134,6 +135,18 @@ 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;
|
||||
@@ -206,10 +219,48 @@ 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 +
|
||||
@@ -577,6 +628,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// immutable storage, and any prior mutable store is replaced anyway.
|
||||
if (resource->id != 0) {
|
||||
NoteBufferIdDeleted(resource->id);
|
||||
// Driver VAOs may have this id baked into attribute/element bindings
|
||||
// keyed on frontend versions this re-mint does not move.
|
||||
++g_bufferBackendIdGeneration;
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
resource->immutableStorage = false;
|
||||
@@ -729,8 +783,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
|
||||
auto* resource = ResourceOf(bufferObject);
|
||||
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||
if (resource->persistentMapped) return; // shadow already IS the GPU storage
|
||||
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||
if (resource->persistentMapped) {
|
||||
// Host writes to a persistent map must not race shader writes already queued
|
||||
// on this context. There is no backend copy to read back in this case.
|
||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||
return;
|
||||
}
|
||||
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
|
||||
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
||||
if (size == 0) return;
|
||||
@@ -828,6 +887,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
// See the declaration: re-mints of a live resource's driver id. Written only on
|
||||
// the context thread (both re-mint sites run there), read only by the VAO sync.
|
||||
Uint64 g_bufferBackendIdGeneration = 0;
|
||||
|
||||
void RegisterBufferBackendOps() {
|
||||
MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);
|
||||
// Frontend writes issued while ops were unregistered advanced change
|
||||
@@ -955,6 +1018,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// here, on the thread that can, and the id is re-minted below.
|
||||
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
|
||||
NoteBufferIdDeleted(resource->id);
|
||||
// Same as the persistent-map re-mint: the dying id may be baked into
|
||||
// driver VAO bindings whose frontend versions do not move for this.
|
||||
++g_bufferBackendIdGeneration;
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
resource->immutableStorage = false;
|
||||
@@ -1635,8 +1701,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PrepareForDraw's BindCurrentVAO establishes the draw binding regardless.
|
||||
const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion();
|
||||
const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion();
|
||||
const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
|
||||
const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion;
|
||||
// A live buffer's driver id was re-minted since this twin's last emit
|
||||
// (persistent-map adoption / immutable-store retire): every baked binding may
|
||||
// hold the dead id while every frontend version still matches, so force a
|
||||
// full re-emit. Read once; each buffer re-mints at most once per walk (its
|
||||
// first EnsureBufferResource this draw), before its id is baked, so stamping
|
||||
// the entry value at the end is exact - and a stale stamp only costs one
|
||||
// extra full emit.
|
||||
const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration;
|
||||
const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration;
|
||||
const Bool attributesDirty =
|
||||
bufferIdsRemitted || !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
|
||||
// Identity joins the version compare: the slot version is a wrapping Uint16,
|
||||
// so a wrapped-back count with a different buffer bound must still read dirty.
|
||||
const MG_State::GLState::BufferObject* currentIndexBufferObject =
|
||||
stateVAOObject->GetIndexBufferBindingSlot().GetBoundObject().get();
|
||||
const Bool indexBufferDirty = bufferIdsRemitted ||
|
||||
currentIndexBufferVersion != m_syncedIndexBufferVersion ||
|
||||
currentIndexBufferObject != m_syncedIndexBufferObject;
|
||||
|
||||
// The baseInstance shift lives in the attribute offsets the driver already holds, so
|
||||
// a change of baseInstance has to re-emit the divisor'd arrays even when the frontend
|
||||
@@ -1670,10 +1752,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||
Bool needsSyncBuffer = allAttributeVersions[attribIndex].BufferVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
Bool needsSyncFormat = bufferIdsRemitted || allAttributeVersions[attribIndex].FormatVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||
Bool needsSyncBuffer = bufferIdsRemitted || allAttributeVersions[attribIndex].BufferVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
|
||||
|
||||
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
|
||||
@@ -1793,6 +1875,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (indexBufferSynced) {
|
||||
m_syncedIndexBufferVersion = currentIndexBufferVersion;
|
||||
m_syncedIndexBufferObject = currentIndexBufferObject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1804,6 +1887,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (emitAttributes) {
|
||||
m_syncedFetchBaseInstance = fetchBaseInstance;
|
||||
}
|
||||
m_syncedBufferIdGeneration = currentBufferIdGeneration;
|
||||
}
|
||||
|
||||
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
|
||||
@@ -1941,6 +2025,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BackendTextureObject::RecreateBackendTexture() {
|
||||
if (m_backendTextureId != 0) {
|
||||
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||
// Application FBO twins that attached the dying id memoize on FRONTEND
|
||||
// attachment versions, which this backend-side re-mint does not move;
|
||||
// without this bump their driver FBOs would keep the deleted name
|
||||
// attached forever (see g_attachmentBackendIdGeneration).
|
||||
++FramebufferImpl::g_attachmentBackendIdGeneration;
|
||||
if (m_contextGeneration == g_backendContextGeneration) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
}
|
||||
@@ -2388,6 +2477,26 @@ 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) {
|
||||
@@ -2435,8 +2544,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 3. Size changed
|
||||
// 4. Mipmap levels changed
|
||||
|
||||
if (!stateTextureObject->IsComplete()) {
|
||||
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.",
|
||||
// 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.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
@@ -2538,6 +2653,13 @@ 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);
|
||||
@@ -2610,25 +2732,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||
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 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;
|
||||
}
|
||||
m_backendStorageImmutable = true;
|
||||
// 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));
|
||||
});
|
||||
for (const auto& uploadTarget : uploadTargets) {
|
||||
for (SizeT level = 0; level < mipmapCount; ++level) {
|
||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||
@@ -2731,6 +2887,13 @@ 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);
|
||||
@@ -3512,6 +3675,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_backendReadBuffer = GL_NONE;
|
||||
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
|
||||
static_cast<Uint16>(~0u));
|
||||
// Every attachment version is invalidated above, so the next walk re-attaches
|
||||
// everything regardless; stamp the generation so it does not re-arm twice.
|
||||
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
|
||||
}
|
||||
|
||||
static Bool SyncAttachmentObject(GLenum glFBOTarget,
|
||||
@@ -4000,6 +4166,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
// -------------------- Attach texture to backend FBO -----------------------
|
||||
// A backend texture id was re-minted since this twin's last walk
|
||||
// (RecreateBackendTexture): any point here may still hold the dead id while
|
||||
// its frontend attachment version is unchanged, so the memo below would skip
|
||||
// exactly the attachment that needs repair. Re-arm every point first.
|
||||
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
|
||||
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
|
||||
static_cast<Uint16>(~0u));
|
||||
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
|
||||
}
|
||||
const auto& attachments = stateFBOObject->GetAllAttachmentObjects();
|
||||
const auto& attachmentVersions = stateFBOObject->GetAllFramebufferAttachmentVersions();
|
||||
for (SizeT i = 0; i < attachments.size(); ++i) {
|
||||
@@ -4088,6 +4263,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// The walk itself can re-mint an id (SyncAttachmentObject ->
|
||||
// SyncMipmapsToBackend -> RecreateBackendTexture), invalidating points this
|
||||
// walk already attached or version-skipped - e.g. one texture attached at two
|
||||
// points. Re-enter until the generation is quiescent: every pass syncs each
|
||||
// dirty texture clean, so each repeat finds strictly fewer re-mints and the
|
||||
// common case (no re-mint) never takes a second pass. The head's draw/read-
|
||||
// buffer syncs are memoized against their own shadows, so a repeat re-walks
|
||||
// only the attachments.
|
||||
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
|
||||
SyncToBackend(stateFBOObject, asTarget);
|
||||
}
|
||||
}
|
||||
|
||||
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
|
||||
@@ -4114,6 +4301,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
|
||||
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects = {};
|
||||
Uint64 g_attachmentBackendIdGeneration = 0;
|
||||
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations = {0};
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
namespace ScratchFBOImpl {
|
||||
@@ -4741,6 +4930,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
const Bool enableSpirvValidation = stateProgramObject->GetSpirvValidationEnabled();
|
||||
|
||||
// Blocks a transform-feedback capture request names a member of ("StageData" of
|
||||
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
|
||||
@@ -4794,17 +4984,89 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> loweredSpirv;
|
||||
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
||||
!loweredSpirv.empty()) {
|
||||
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;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
||||
*effectiveSpirv, splitArrayInputSpirv) &&
|
||||
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
||||
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
||||
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
||||
// re-serialised copy either way, and adopting that copy for every vertex
|
||||
@@ -4826,7 +5088,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!xfbCaptureBlockNames.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
||||
*effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames,
|
||||
flattenedXfbSpirv) &&
|
||||
flattenedXfbSpirv, enableSpirvValidation) &&
|
||||
!flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) {
|
||||
effectiveSpirv = &flattenedXfbSpirv;
|
||||
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
||||
@@ -4842,7 +5104,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// declare the member highp; nothing else about emission changes.
|
||||
Vector<unsigned int> uboPrecisionSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||
*effectiveSpirv, uboPrecisionSpirv) &&
|
||||
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
||||
!uboPrecisionSpirv.empty()) {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
@@ -4857,7 +5119,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv) &&
|
||||
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
@@ -4867,7 +5129,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||
// size, which is the whole of the difference between the two.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
@@ -4881,7 +5143,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
||||
Vector<unsigned int> arrayImageSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
||||
arrayImageSpirv) &&
|
||||
arrayImageSpirv, enableSpirvValidation) &&
|
||||
!arrayImageSpirv.empty()) {
|
||||
effectiveSpirv = &arrayImageSpirv;
|
||||
}
|
||||
@@ -4900,7 +5162,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv) &&
|
||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
||||
enableSpirvValidation) &&
|
||||
!imageFormatSpirv.empty()) {
|
||||
effectiveSpirv = &imageFormatSpirv;
|
||||
}
|
||||
@@ -4916,7 +5179,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> outputIndexSpirv;
|
||||
if (glShaderType == GL_FRAGMENT_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
||||
*effectiveSpirv, outputIndexSpirv) &&
|
||||
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
||||
!outputIndexSpirv.empty()) {
|
||||
effectiveSpirv = &outputIndexSpirv;
|
||||
}
|
||||
@@ -4979,6 +5242,17 @@ 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
|
||||
@@ -5554,14 +5828,39 @@ 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) {
|
||||
g_GLESFuncs.glRenderbufferStorageMultisample(
|
||||
GL_RENDERBUFFER, static_cast<GLsizei>(samples), glInternalFormat, static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height));
|
||||
// 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));
|
||||
} 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;
|
||||
|
||||
@@ -21,6 +21,16 @@ 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
|
||||
@@ -129,7 +139,28 @@ 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.
|
||||
@@ -203,8 +234,12 @@ 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;
|
||||
};
|
||||
|
||||
@@ -346,6 +381,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||
// could false-skip when the name is recycled.
|
||||
void NoteBufferIdDeleted(Uint id);
|
||||
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
|
||||
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
|
||||
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
|
||||
// key on FRONTEND versions, which a backend-side re-mint does not move - without
|
||||
// this generation the driver VAO would keep fetching through the deleted id (or
|
||||
// its retained store) forever. Compared and stamped by
|
||||
// BackendVertexArrayObject::SyncToBackend.
|
||||
extern Uint64 g_bufferBackendIdGeneration;
|
||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||
@@ -465,6 +508,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
// Identity of the buffer the version above was stamped against. Raw and never
|
||||
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
|
||||
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
|
||||
// alone would read a wrapped-back count with a different buffer bound as clean.
|
||||
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
|
||||
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
||||
// version on every per-attribute version bump (the three Bump*Version functions are
|
||||
// its only writers), so an unchanged config version proves every per-attribute
|
||||
@@ -480,6 +528,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Kept here because it describes what was last EMITTED, which is what the next sync
|
||||
// has to correct.
|
||||
Uint32 m_syncedFetchBaseInstance = 0;
|
||||
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
|
||||
// mismatch means some live buffer's driver id was re-minted since; the ids
|
||||
// baked into the driver VAO's attribute/element bindings may be dead even
|
||||
// though every frontend version matches, so the next sync re-emits them all.
|
||||
Uint64 m_syncedBufferIdGeneration = 0;
|
||||
};
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||
@@ -799,6 +852,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
||||
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
|
||||
// mismatch means some backend texture id was re-minted since, and any of this
|
||||
// twin's attachment points may still hold the dead id even though the frontend
|
||||
// attachment versions match - so the walk re-attaches everything first.
|
||||
Uint64 m_syncedBackendIdGeneration = 0;
|
||||
};
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
@@ -888,6 +946,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects;
|
||||
|
||||
// Bumped whenever a live backend texture's driver id is re-minted while its
|
||||
// frontend texture may still be attached to application FBOs
|
||||
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
|
||||
// whose backend storage went immutable). The FBO twins' attachment memos key on
|
||||
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
|
||||
// the driver FBO would keep the deleted texture name attached forever. The
|
||||
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
||||
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
||||
extern Uint64 g_attachmentBackendIdGeneration;
|
||||
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
||||
// target; part of the synced tuple above.
|
||||
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
|
||||
|
||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||
|
||||
@@ -569,6 +569,43 @@ 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
|
||||
|
||||
@@ -154,6 +154,16 @@ 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
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include "BackendObject_DirectVulkan.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "DirectVulkan.h"
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -383,6 +385,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -511,7 +516,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||
@@ -687,6 +692,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -697,8 +705,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||
// extension by itself on devices with no native subgroup support at all; a
|
||||
// device with native subgroups always advertises - and uses - those.
|
||||
const Bool subgroupSupportAdvertised =
|
||||
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
@@ -833,6 +847,38 @@ 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;
|
||||
@@ -934,6 +980,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||
// advertised values describe the 32-lane virtual subgroup the compute
|
||||
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||
// GL requires the advertisement and the execution to agree, and on this
|
||||
// path the emulation is what executes; only the compute stage is offered.
|
||||
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
||||
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||
} else {
|
||||
m_dynamicParameters.SubgroupSize = 0;
|
||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -69,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// slot's ownership unambiguous.
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 backendStateVersion = 0;
|
||||
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
|
||||
// version, and the pipeline composite is unnamed so the in-place patch in
|
||||
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
|
||||
// mirror replay bumps only the program's block-binding version. Without this
|
||||
// key the composite's slot kept serving the pre-rebind block.binding.
|
||||
Uint32 blockBindingVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||
@@ -156,18 +162,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
|
||||
// The lifetime id must match too: a new program that reuses a deleted
|
||||
// program's name and happens to land on the same backendStateVersion (both
|
||||
// count from zero) would otherwise be served the dead program's reflection.
|
||||
if (cache.programLifetimeId == programLifetimeId &&
|
||||
cache.backendStateVersion == backendStateVersion &&
|
||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||
if (cache.blockBindingVersion != blockBindingVersion) {
|
||||
// Only the block bindings moved (glShaderStorageBlockBinding, or the
|
||||
// pipeline composite's mirror replay - neither touches the backend
|
||||
// state version): the reflection itself is unchanged, so re-apply the
|
||||
// overrides by name instead of re-running spirv-reflect. Overrides
|
||||
// only ever accumulate, so a block without one still holds its
|
||||
// declared binding.
|
||||
for (auto& block : cache.storageBlocks) {
|
||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||
}
|
||||
cache.blockBindingVersion = blockBindingVersion;
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = {};
|
||||
cache.programLifetimeId = programLifetimeId;
|
||||
cache.backendStateVersion = backendStateVersion;
|
||||
cache.blockBindingVersion = blockBindingVersion;
|
||||
|
||||
Vector<SpvReflectShaderModule> modules;
|
||||
Vector<Bool> validModules;
|
||||
@@ -611,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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
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(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dst, 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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
|
||||
@@ -33,6 +33,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
|
||||
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
|
||||
// Local size of a compute module, read from OpExecutionMode LocalSize; all-zero
|
||||
// when absent. The compile chain pins SPIR-V 1.3, where a literal local size
|
||||
// always reaches the module as this execution mode (LocalSizeId does not exist
|
||||
// yet).
|
||||
struct ComputeLocalSize {
|
||||
Uint32 x = 0;
|
||||
Uint32 y = 0;
|
||||
Uint32 z = 0;
|
||||
Uint64 Total() const { return static_cast<Uint64>(x) * y * z; }
|
||||
};
|
||||
ComputeLocalSize TryGetComputeLocalSize(const Vector<Uint>& spirv) {
|
||||
constexpr SizeT kHeaderWords = 5;
|
||||
constexpr Uint32 kOpExecutionMode = 16;
|
||||
constexpr Uint32 kModeLocalSize = 17;
|
||||
for (SizeT offset = kHeaderWords; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
const Uint32 opcode = spirv[offset] & 0xffffu;
|
||||
if (wordCount == 0 || offset + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpExecutionMode && wordCount >= 6 && spirv[offset + 2] == kModeLocalSize) {
|
||||
return {spirv[offset + 3], spirv[offset + 4], spirv[offset + 5]};
|
||||
}
|
||||
offset += wordCount;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct DescriptorKey {
|
||||
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
|
||||
String name;
|
||||
@@ -2973,8 +2999,73 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindings.push_back(layoutBinding);
|
||||
}
|
||||
|
||||
// UPDATE_AFTER_BIND is strictly an optional per-layout acceleration. The GL
|
||||
// descriptor model still resolves every sampler uniform element independently
|
||||
// (including its texture-unit sampler-object override); selecting this path
|
||||
// changes neither that resolution nor the set versioning in UniformManager.
|
||||
// A conservative count keeps a layout on ordinary descriptors whenever any
|
||||
// relevant update-after-bind limit is not large enough, rather than asking a
|
||||
// driver to reject it during vkCreateDescriptorSetLayout.
|
||||
Uint32 updateAfterBindSamplers = 0;
|
||||
Uint32 updateAfterBindUniformBuffers = 0;
|
||||
Uint32 updateAfterBindStorageBuffers = 0;
|
||||
Uint32 updateAfterBindSampledImages = 0;
|
||||
Uint32 updateAfterBindStorageImages = 0;
|
||||
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
|
||||
const Uint32 count = entry.bindingDescriptorCounts[binding];
|
||||
switch (entry.bindingKinds[binding]) {
|
||||
case DescriptorBindingKind::UniformBufferDynamic:
|
||||
updateAfterBindUniformBuffers += count;
|
||||
break;
|
||||
case DescriptorBindingKind::CombinedImageSampler:
|
||||
updateAfterBindSamplers += count;
|
||||
updateAfterBindSampledImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::UniformTexelBuffer:
|
||||
updateAfterBindSampledImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::StorageBuffer:
|
||||
case DescriptorBindingKind::StorageTexelBuffer:
|
||||
updateAfterBindStorageBuffers += count;
|
||||
break;
|
||||
case DescriptorBindingKind::StorageImage:
|
||||
updateAfterBindStorageImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const Uint32 updateAfterBindResources = updateAfterBindUniformBuffers + updateAfterBindStorageBuffers +
|
||||
updateAfterBindSampledImages + updateAfterBindStorageImages;
|
||||
const auto& uab = m_updateAfterBindLimits;
|
||||
entry.usesUpdateAfterBind =
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
updateAfterBindSampledImages <= uab.maxPerStageSampledImages &&
|
||||
updateAfterBindStorageImages <= uab.maxPerStageStorageImages &&
|
||||
updateAfterBindResources <= uab.maxPerStageResources &&
|
||||
updateAfterBindSamplers <= uab.maxSetSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffersDynamic &&
|
||||
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffersDynamic &&
|
||||
updateAfterBindSampledImages <= uab.maxSetSampledImages &&
|
||||
updateAfterBindStorageImages <= uab.maxSetStorageImages;
|
||||
|
||||
Vector<VkDescriptorBindingFlags> bindingFlags;
|
||||
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{};
|
||||
if (entry.usesUpdateAfterBind) {
|
||||
bindingFlags.assign(bindings.size(), VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT);
|
||||
bindingFlagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
|
||||
bindingFlagsInfo.bindingCount = static_cast<Uint32>(bindingFlags.size());
|
||||
bindingFlagsInfo.pBindingFlags = bindingFlags.data();
|
||||
}
|
||||
|
||||
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
|
||||
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
setLayoutInfo.flags = entry.usesUpdateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
|
||||
setLayoutInfo.pNext = entry.usesUpdateAfterBind ? &bindingFlagsInfo : nullptr;
|
||||
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
|
||||
setLayoutInfo.pBindings = bindings.data();
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout),
|
||||
@@ -3054,6 +3145,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& shaders = program.GetAttachedShaders();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
if (enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(shaders);
|
||||
|
||||
@@ -3094,12 +3189,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
||||
// operations execute natively; module repairs keep the GL contract intact
|
||||
// around them. The opt-in emulation path replaces them only on devices with no
|
||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
||||
// 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(
|
||||
moduleSpirvs[i], emulatedSpirv,
|
||||
m_subgroupPolicy.maxComputeSharedMemoryBytes, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(emulatedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: subgroup emulation failed for program %u; the "
|
||||
"module keeps subgroup operations the device cannot execute",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
} else {
|
||||
// iterationRP under-declares its cross-subgroup scratch
|
||||
// (prefixSumCache[32] for 512 invocations); on a sub-16-lane device
|
||||
// grow that one fingerprinted array to what the topology needs.
|
||||
if (m_subgroupPolicy.fixIterationRPSubgroupScratch) {
|
||||
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 {
|
||||
MGLOG_E("ProgramFactory: iterationRP subgroup scratch patch failed for "
|
||||
"program %u; the pack's declared array sizes stay in effect",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
// gl_NumSubgroups must agree with the gl_SubgroupID range GL promises;
|
||||
// derive it from the workgroup dimensions and gl_SubgroupSize instead of
|
||||
// trusting a driver builtin that can disagree with the topology the same
|
||||
// dispatch emits (Adreno reports 1 while emitting IDs 0..7 for a
|
||||
// 512-invocation, 64-wide workgroup). The ceil() partition this derives
|
||||
// is pinned by REQUIRE_FULL_SUBGROUPS at pipeline creation whenever the
|
||||
// workgroup shape makes that flag legal (see the stage setup below).
|
||||
if (m_subgroupPolicy.deriveNumSubgroups) {
|
||||
Vector<Uint> derivedNumSubgroupsSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
|
||||
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
|
||||
"compute shaders may observe a driver-inconsistent subgroup count",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||
// stored as - which addresses [0,1] where the application addressed texels.
|
||||
{
|
||||
Vector<Uint> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
||||
}
|
||||
@@ -3112,7 +3276,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
{
|
||||
Vector<Uint> invariantSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||
moduleSpirvs[i], invariantSpirv)) {
|
||||
moduleSpirvs[i], invariantSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||
} else {
|
||||
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
||||
@@ -3136,7 +3300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv)) {
|
||||
rebasedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||
@@ -3154,7 +3318,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||
zeroedSpirv)) {
|
||||
zeroedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
@@ -3177,7 +3341,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
moduleSpirvs[i], packedSpirv);
|
||||
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||
MOBILEGL_ASSERT(packOk,
|
||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||
"vertex-input format and the shader input type now disagree",
|
||||
@@ -3201,7 +3365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_unformattedFloatStorageImagesEnabled) {
|
||||
Vector<Uint> unformattedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
moduleSpirvs[i], unformattedSpirv)) {
|
||||
moduleSpirvs[i], unformattedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(unformattedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
||||
@@ -3222,7 +3386,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
}
|
||||
#endif
|
||||
@@ -3239,6 +3403,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stage.stage = ToVkStage(shaderStage);
|
||||
stage.module = module;
|
||||
stage.pName = "main";
|
||||
// Pin the full-subgroup launch the derived gl_NumSubgroups assumes. Legal
|
||||
// exactly when the computeFullSubgroups feature is enabled and local_size_x is
|
||||
// a multiple of the subgroup size (VUID-VkPipelineShaderStageCreateInfo-
|
||||
// flags-02759/-02785), and only worth requesting while the resulting subgroup
|
||||
// count fits the device's maxComputeWorkgroupSubgroups (lavapipe caps it at
|
||||
// 32, below a 512-invocation dispatch's 64). With the bit set, "Full
|
||||
// Subgroups" guarantees every subgroup launches with all invocations active,
|
||||
// making the subgroup count exactly invocations / size. Shapes the flag
|
||||
// cannot cover (e.g. 32x16 on a 64-wide device) fall back to the driver's
|
||||
// own - spec-encouraged - tight partitioning, which the DriverPost witness
|
||||
// verifies per device.
|
||||
if (shaderStage == ShaderStage::Compute && m_subgroupPolicy.requireFullSubgroups &&
|
||||
!m_subgroupPolicy.emulateSubgroups && m_subgroupPolicy.nativeSubgroupSize != 0) {
|
||||
const ComputeLocalSize localSize = TryGetComputeLocalSize(moduleSpv);
|
||||
const Uint64 fullSubgroupCount =
|
||||
localSize.Total() / m_subgroupPolicy.nativeSubgroupSize;
|
||||
if (localSize.x != 0 && localSize.x % m_subgroupPolicy.nativeSubgroupSize == 0 &&
|
||||
fullSubgroupCount <= m_subgroupPolicy.maxComputeWorkgroupSubgroups) {
|
||||
stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
entry.modules.push_back(module);
|
||||
entry.stages.push_back(stage);
|
||||
@@ -3299,14 +3484,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
||||
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
||||
static_cast<unsigned long long>(hash));
|
||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
||||
// so an observer never observes a half-destroyed entry through a lookup.
|
||||
// Observers only need the handle values to purge their keyed caches.
|
||||
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||
it = m_cache.erase(it);
|
||||
// The observer destroys dependent pipelines and frees descriptor sets while
|
||||
// this entry still owns its layout. Vulkan requires every descriptor set to be
|
||||
// freed before its VkDescriptorSetLayout is destroyed.
|
||||
if (m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||
}
|
||||
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||
it = m_cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
@@ -3440,7 +3625,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
#else
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
Uint32 maxPerStageUniformBuffers = 0;
|
||||
Uint32 maxPerStageStorageBuffers = 0;
|
||||
Uint32 maxPerStageSampledImages = 0;
|
||||
Uint32 maxPerStageStorageImages = 0;
|
||||
Uint32 maxPerStageResources = 0;
|
||||
Uint32 maxSetSamplers = 0;
|
||||
Uint32 maxSetUniformBuffers = 0;
|
||||
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||
Uint32 maxSetStorageBuffers = 0;
|
||||
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||
Uint32 maxSetSampledImages = 0;
|
||||
Uint32 maxSetStorageImages = 0;
|
||||
};
|
||||
|
||||
struct VkProgramObject {
|
||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||
|
||||
@@ -88,6 +105,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Layout data (previously in separate VkProgramLayout)
|
||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||
// True only when this layout passed every descriptor-indexing feature and
|
||||
// update-after-bind limit gate at reflection time. It controls both the
|
||||
// layout/binding flags and the pool class used by UniformManager.
|
||||
Bool usesUpdateAfterBind = false;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
Vector<DescriptorBindingKind> bindingKinds;
|
||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||
@@ -196,6 +217,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a pipeline failure would be reported against the wrong SPIR-V.
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -230,6 +252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
@@ -256,6 +279,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
modules = std::move(other.modules);
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -290,6 +314,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
@@ -347,12 +372,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
||||
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
||||
// subgroup properties) so lowering can never disagree with the advertised
|
||||
// capabilities. Native subgroup operations always execute natively; the two
|
||||
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||
// on opted-in devices with no subgroup support at all.
|
||||
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;
|
||||
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
||||
// workgroup needs more subgroups than this cannot request the flag.
|
||||
Uint32 maxComputeWorkgroupSubgroups = 0;
|
||||
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
||||
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
||||
Uint32 maxComputeSharedMemoryBytes = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
@@ -475,6 +527,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||
// own counts would exceed the update-after-bind budget.
|
||||
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
SubgroupLoweringPolicy m_subgroupPolicy{};
|
||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||
Uint32 m_defaultFramebufferHeight = 0;
|
||||
|
||||
@@ -156,13 +156,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.descriptorPools.clear();
|
||||
|
||||
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
|
||||
if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||
frameIndex);
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0});
|
||||
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false});
|
||||
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
|
||||
m_setsPerFrame);
|
||||
}
|
||||
@@ -542,11 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
outImageInfo = {
|
||||
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
||||
*samplerBindingOverride.texture),
|
||||
*samplerBindingOverride.texture,
|
||||
samplerBindingOverride.forceNearestFiltering,
|
||||
resource->sampledLevelCount),
|
||||
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
||||
samplerBindingOverride.imageView :
|
||||
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
||||
.imageLayout = resource->layout,
|
||||
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
||||
samplerBindingOverride.imageLayout : resource->layout,
|
||||
};
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -1269,6 +1272,87 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess) {
|
||||
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
|
||||
}
|
||||
|
||||
Bool UniformManager::CollectSamplerImageFeedback(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
||||
outBindings.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
"CollectSamplerImageFeedback: GL context is null");
|
||||
if (programObj.declinedDescriptors) return true;
|
||||
|
||||
for (const Uint32 samplerBinding : programObj.activeBindings) {
|
||||
if (samplerBinding >= m_maxBindings ||
|
||||
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||
continue;
|
||||
}
|
||||
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
|
||||
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
|
||||
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
|
||||
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
||||
sampledTexture, sampledSampler) ||
|
||||
sampledTexture == nullptr || sampledSampler == nullptr ||
|
||||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
|
||||
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
||||
// alias the image-unit binding of the original texture.
|
||||
continue;
|
||||
}
|
||||
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
||||
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
|
||||
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
|
||||
const TextureTarget sampledTarget = sampledTexture->GetTarget();
|
||||
if (sampledTarget == TextureTarget::Texture2DMultisample ||
|
||||
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
|
||||
continue;
|
||||
}
|
||||
const auto& levelRange = sampledTexture->GetLevelRange();
|
||||
Bool aliasesWritableImage = false;
|
||||
for (const Uint32 imageBinding : programObj.activeBindings) {
|
||||
if (imageBinding >= m_maxBindings ||
|
||||
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||
continue;
|
||||
}
|
||||
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
|
||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
|
||||
if (baseLocation < 0) return false;
|
||||
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
|
||||
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
|
||||
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
|
||||
if (location < 0) return false;
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
return false;
|
||||
}
|
||||
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
// A sampler view exposes all layers of its target; equal texture plus an
|
||||
// overlapping mip therefore aliases the writable image subresource.
|
||||
if (image.Texture.get() == sampledTexture &&
|
||||
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
|
||||
image.Level, image.Access)) {
|
||||
aliasesWritableImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (aliasesWritableImage) break;
|
||||
}
|
||||
if (aliasesWritableImage) {
|
||||
outBindings.push_back({.samplerBinding = samplerBinding,
|
||||
.samplerElement = samplerElement,
|
||||
.texture = sampledTexture,
|
||||
.sampler = sampledSampler,
|
||||
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, UboBindResult& out) const {
|
||||
@@ -1390,7 +1474,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const {
|
||||
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const {
|
||||
outPool = VK_NULL_HANDLE;
|
||||
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
|
||||
return false;
|
||||
@@ -1433,7 +1517,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||
// cache grows - never on the per-draw reuse path.
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT |
|
||||
(updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0);
|
||||
poolInfo.maxSets = maxSets;
|
||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
@@ -1447,24 +1532,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex) {
|
||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
||||
if (frame.descriptorPools.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& currentBucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
|
||||
const Uint32 currentMaxSets = std::max<Uint32>(1, currentBucket.maxSets);
|
||||
const auto matchingBucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
|
||||
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
|
||||
? std::max<Uint32>(1, matchingBucket->maxSets)
|
||||
: m_setsPerFrame;
|
||||
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
||||
: currentMaxSets;
|
||||
|
||||
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
|
||||
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
currentMaxSets, grownMaxSets);
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0});
|
||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
||||
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
||||
MGLOG_D(
|
||||
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
||||
@@ -1474,14 +1563,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
}
|
||||
if (frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
||||
const Bool updateAfterBind = programObj.usesUpdateAfterBind;
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() ||
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind ||
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
||||
const auto availableBucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[](const DescriptorPoolBucket& candidate) { return candidate.allocatedSets < candidate.maxSets; });
|
||||
[updateAfterBind](const DescriptorPoolBucket& candidate) {
|
||||
return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets;
|
||||
});
|
||||
if (availableBucket == frame.descriptorPools.end()) {
|
||||
outDescriptorSet = VK_NULL_HANDLE;
|
||||
return VK_ERROR_OUT_OF_POOL_MEMORY;
|
||||
@@ -1517,7 +1608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
|
||||
if (!GrowFrameDescriptorPool(frame, frameIndex, programObj.usesUpdateAfterBind)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||
return allocResult;
|
||||
}
|
||||
@@ -1635,7 +1726,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint,
|
||||
const SamplerBindingOverride* samplerBindingOverride,
|
||||
Bool samplerDescriptorsUnchangedHint) {
|
||||
Bool samplerDescriptorsUnchangedHint,
|
||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
|
||||
// This program has a descriptor MobileGL could not resolve (see
|
||||
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
|
||||
// binding is still declared in the layout, so the pipeline is consistent with the shader
|
||||
@@ -1662,7 +1754,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sampler binding, and an unchanged (buffer, range) for the single
|
||||
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
||||
// the SAME set delivers without any descriptor write.
|
||||
const Bool cacheable = (samplerBindingOverride == nullptr);
|
||||
const Bool cacheable = samplerBindingOverride == nullptr &&
|
||||
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
|
||||
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
||||
m_fastRebindMemo.frameIndex == frameIndex &&
|
||||
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
||||
@@ -1886,13 +1979,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const SizeT firstImageInfoIndex = imageInfos.size();
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
VkDescriptorImageInfo imageInfo{};
|
||||
Bool hasImage = false;
|
||||
if (overrideThisBinding && element == 0) {
|
||||
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
||||
} else {
|
||||
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
|
||||
imageInfo, samplerDescriptorsUnchangedHint);
|
||||
const SamplerBindingOverride* overrideForElement =
|
||||
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
|
||||
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
|
||||
const auto overrideIt = std::find_if(
|
||||
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
|
||||
[binding, element](const SamplerBindingOverride& candidate) {
|
||||
return candidate.binding == binding && candidate.element == element;
|
||||
});
|
||||
if (overrideIt != samplerBindingOverrides->end()) {
|
||||
overrideForElement = &*overrideIt;
|
||||
}
|
||||
}
|
||||
const Bool hasImage = overrideForElement != nullptr
|
||||
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
|
||||
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
|
||||
element, imageInfo,
|
||||
samplerDescriptorsUnchangedHint);
|
||||
if (!hasImage) {
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||
|
||||
@@ -26,9 +26,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
struct SamplerBindingOverride {
|
||||
Uint32 binding = 0;
|
||||
Uint32 element = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Bool forceNearestFiltering = false;
|
||||
};
|
||||
|
||||
struct SamplerImageFeedbackBinding {
|
||||
Uint32 samplerBinding = 0;
|
||||
Uint32 samplerElement = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
@@ -79,6 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess);
|
||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||
// every input of every combined-image-sampler resolution is unchanged since the
|
||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||
@@ -91,7 +108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||
Bool samplerDescriptorsUnchangedHint = false);
|
||||
Bool samplerDescriptorsUnchangedHint = false,
|
||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
@@ -114,6 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
@@ -223,8 +242,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
|
||||
@@ -287,8 +287,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert.
|
||||
++m_evictionEpoch;
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment).
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||
// factory restarting at a dead factory's epoch value would honor its
|
||||
// dangling entry pointers. The constructor takes a value strictly greater
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
static inline Uint64 s_evictionEpochSource = 0;
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -166,7 +166,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||
dst.mask |= src.mask;
|
||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
// The whole colour story travels together (same rule as
|
||||
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||
// payload carries its value in colorInt/colorUint and its branch selector
|
||||
// in colorEncoding - dropping them here would leave the pending clear
|
||||
// reading as an all-zero float one.
|
||||
dst.color = src.color;
|
||||
dst.colorEncoding = src.colorEncoding;
|
||||
dst.colorInt = src.colorInt;
|
||||
dst.colorUint = src.colorUint;
|
||||
}
|
||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
dst.depth = src.depth;
|
||||
|
||||
@@ -831,6 +831,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
||||
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
||||
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
||||
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||
@@ -855,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
||||
m_rpFastValid = true;
|
||||
m_rpFastFbo = &fbo;
|
||||
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
||||
m_rpFastFboVersion = fbo.GetObjectVersion();
|
||||
m_rpFastSwapchainIndex = swapchainImageIndex;
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
@@ -1507,7 +1509,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
if (pending.hasInlinePayload) {
|
||||
clearPayload = pending.inlinePayload;
|
||||
// The inline payload was snapshotted when the entry was CREATED, but the
|
||||
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
||||
// glClear would replay the creation-time value and drop the new one (the
|
||||
// texture path below is immune because it re-reads the live payload).
|
||||
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
||||
// pending clear, fall back to the snapshot only when none is queued.
|
||||
if (s_renderPassManager != nullptr &&
|
||||
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
||||
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
||||
3) {
|
||||
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||
ForceOpaqueClearAlpha(clearPayload);
|
||||
}
|
||||
} else {
|
||||
clearPayload = pending.inlinePayload;
|
||||
}
|
||||
} else {
|
||||
if (pending.key.texture == nullptr ||
|
||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||
|
||||
@@ -289,6 +289,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||
Bool m_rpFastValid = false;
|
||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||
// the same number of version bumps would otherwise compare equal (both count
|
||||
// from 0), serving the dead framebuffer's pass to the new object.
|
||||
Uint64 m_rpFastFboLifetimeId = 0;
|
||||
Uint16 m_rpFastFboVersion = 0;
|
||||
Uint32 m_rpFastSwapchainIndex = 0;
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
|
||||
@@ -1291,6 +1291,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ok;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot) {
|
||||
outSnapshot = {};
|
||||
TextureResource* source = SyncTextureAndGetDescriptor(texture);
|
||||
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
source->sampledLevelCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
|
||||
if (sampledFormat == VK_FORMAT_UNDEFINED ||
|
||||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
|
||||
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
|
||||
return false;
|
||||
}
|
||||
if (sampledFormat != source->format &&
|
||||
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
|
||||
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
|
||||
return false;
|
||||
}
|
||||
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
switch (source->viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_1D:
|
||||
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||
imageType = VK_IMAGE_TYPE_1D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
imageType = VK_IMAGE_TYPE_3D;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
TextureResource snapshot{};
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.flags = source->imageCreateFlags;
|
||||
imageInfo.imageType = imageType;
|
||||
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
|
||||
imageInfo.mipLevels = source->mipLevels;
|
||||
imageInfo.arrayLayers = source->arrayLayers;
|
||||
imageInfo.format = source->format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
|
||||
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
|
||||
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
|
||||
Vector<VkFormat> viewFormats;
|
||||
VkImageFormatListCreateInfo formatListInfo{};
|
||||
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
viewFormats.push_back(source->format);
|
||||
if (sampledFormat != source->format) {
|
||||
viewFormats.push_back(sampledFormat);
|
||||
}
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
imageInfo.pNext = &formatListInfo;
|
||||
}
|
||||
|
||||
VmaAllocationCreateInfo allocationInfo{};
|
||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
const VkResult createResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
|
||||
if (createResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot.extent = source->extent;
|
||||
snapshot.depth = source->depth;
|
||||
snapshot.arrayLayers = source->arrayLayers;
|
||||
snapshot.mipLevels = source->mipLevels;
|
||||
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
|
||||
snapshot.sampledLevelCount = source->sampledLevelCount;
|
||||
snapshot.format = source->format;
|
||||
snapshot.aspect = source->aspect;
|
||||
snapshot.viewType = source->viewType;
|
||||
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
snapshot.imageCreateFlags = imageInfo.flags;
|
||||
snapshot.usageFlags = imageInfo.usage;
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
|
||||
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
|
||||
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
|
||||
snapshot.arrayLayers, &sampledComponents);
|
||||
if (snapshot.sampledView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags sourceAccessMask = 0;
|
||||
const VkImageLayout sourceLayout = source->layout;
|
||||
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
|
||||
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
|
||||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||
snapshot.sampledLevelCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
copyRegions.reserve(snapshot.sampledLevelCount);
|
||||
for (Uint32 level = snapshot.sampledBaseMipLevel;
|
||||
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
|
||||
VkImageCopy copy{};
|
||||
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
|
||||
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
|
||||
copy.extent = {std::max(source->extent.width >> level, 1u),
|
||||
std::max(source->extent.height >> level, 1u),
|
||||
std::max(source->depth >> level, 1u)};
|
||||
copyRegions.push_back(copy);
|
||||
}
|
||||
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
||||
|
||||
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
|
||||
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||
snapshot.sampledLevelCount) ||
|
||||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
source->aspect, 0, source->mipLevels)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StampResourceRecordingUse(*source);
|
||||
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
|
||||
DeferResourceRelease(Move(snapshot));
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||
}
|
||||
@@ -1696,6 +1848,19 @@ 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);
|
||||
}
|
||||
@@ -1841,6 +2006,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
texture.GetExternalIndex(),
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||
// The preserved image was written by GPU work that may still be in flight
|
||||
// (preserve requires layout != UNDEFINED); park it on the deferred ring
|
||||
// like every other destruction path instead of letting the unique_ptr
|
||||
// destroy it synchronously under the GPU.
|
||||
if (preservedResource) {
|
||||
DeferResourceRelease(Move(*preservedResource));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1863,6 +2035,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
// Same as the probe failure above: the preserved live image must go through
|
||||
// the deferred ring, never a synchronous destructor while frames that
|
||||
// reference it are still in flight.
|
||||
if (preservedResource) {
|
||||
DeferResourceRelease(Move(*preservedResource));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||
|
||||
@@ -310,6 +310,11 @@ public:
|
||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct SampledTextureSnapshot {
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
@@ -343,6 +348,13 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h"
|
||||
#include "MG_Backend/DirectGLES/Utils.h"
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
@@ -1480,7 +1481,8 @@ void main() {
|
||||
const char* label = nullptr;
|
||||
};
|
||||
|
||||
static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties) {
|
||||
static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties,
|
||||
const ProgramFactory::UpdateAfterBindLimits& updateAfterBindLimits) {
|
||||
const auto& limits = properties.limits;
|
||||
static constexpr Uint32 kMinProgramBindings = 16;
|
||||
static constexpr Uint32 kMaxProgramBindingsCap = 256;
|
||||
@@ -1495,6 +1497,21 @@ void main() {
|
||||
maxBindings = std::min(maxBindings, maxCombinedImageSamplers);
|
||||
maxBindings = std::min(maxBindings, maxSampledImages + maxDynamicUniformBuffers);
|
||||
|
||||
if (updateAfterBindLimits.enabled) {
|
||||
const Uint32 updateAfterBindSamplers = std::min(updateAfterBindLimits.maxPerStageSamplers,
|
||||
updateAfterBindLimits.maxSetSamplers);
|
||||
const Uint32 updateAfterBindSampledImages = std::min(updateAfterBindLimits.maxPerStageSampledImages,
|
||||
updateAfterBindLimits.maxSetSampledImages);
|
||||
const Uint32 updateAfterBindDynamicUniformBuffers =
|
||||
std::min(updateAfterBindLimits.maxPerStageUniformBuffers,
|
||||
updateAfterBindLimits.maxSetUniformBuffersDynamic);
|
||||
Uint32 updateAfterBindBindings = updateAfterBindLimits.maxPerStageResources;
|
||||
updateAfterBindBindings = std::min(updateAfterBindBindings, updateAfterBindSamplers);
|
||||
updateAfterBindBindings =
|
||||
std::min(updateAfterBindBindings, updateAfterBindSampledImages + updateAfterBindDynamicUniformBuffers);
|
||||
maxBindings = std::max(maxBindings, updateAfterBindBindings);
|
||||
}
|
||||
|
||||
maxBindings = std::max(kMinProgramBindings, maxBindings);
|
||||
maxBindings = std::min(kMaxProgramBindingsCap, maxBindings);
|
||||
return maxBindings;
|
||||
@@ -3010,7 +3027,7 @@ void main() {
|
||||
succeeded = m_renderPassManager->Initialize();
|
||||
MOBILEGL_ASSERT(succeeded, "VkRenderPassManager initialization failed.");
|
||||
|
||||
const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties);
|
||||
const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties, m_updateAfterBindLimits);
|
||||
MGLOG_I("DirectVulkan: using %u program descriptor bindings", maxProgramBindings);
|
||||
if (IsPowerVRDevice(m_physicalDevice.properties)) {
|
||||
m_config.DisablePipelineCache = true;
|
||||
@@ -3042,9 +3059,23 @@ void main() {
|
||||
}
|
||||
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
||||
}
|
||||
ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{};
|
||||
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
|
||||
subgroupPolicy.fixIterationRPSubgroupScratch =
|
||||
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
|
||||
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
|
||||
subgroupPolicy.deriveNumSubgroups =
|
||||
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
|
||||
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
|
||||
subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize;
|
||||
subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups;
|
||||
subgroupPolicy.maxComputeSharedMemoryBytes =
|
||||
m_physicalDevice.properties.limits.maxComputeSharedMemorySize;
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||
m_shaderDrawParametersFeatureEnabled,
|
||||
m_unformattedFloatStorageImagesEnabled);
|
||||
m_unformattedFloatStorageImagesEnabled,
|
||||
MG_Config::Features.EnableSpirvValidation,
|
||||
m_updateAfterBindLimits, subgroupPolicy);
|
||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||
// The swapchain already exists at this point (Initialize creates it first), so seed the
|
||||
// height the factory could not be told about from CreateSwapchain.
|
||||
@@ -3293,6 +3324,11 @@ void main() {
|
||||
indexView.indexByteSize > bufferSize - indexView.indexByteOffset) {
|
||||
return false;
|
||||
}
|
||||
// Recorded-but-unexecuted GPU writes (XFB capture, SSBO, storage texel
|
||||
// buffer) land in the coherent mapping this scan is about to read;
|
||||
// submit-and-wait first, exactly like the restart-index rewrite does.
|
||||
// A no-op unless the gpu-write flag is set.
|
||||
indexBufferShared->SyncGpuWrites();
|
||||
indexBufferShared->SyncPersistentMappedRange();
|
||||
indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset;
|
||||
} else {
|
||||
@@ -3530,6 +3566,16 @@ void main() {
|
||||
const Uint8* sourceData, SizeT sourceStride,
|
||||
SizeT elementSize, SizeT elementCount,
|
||||
BufferSlice& outSlice) -> Bool {
|
||||
// A resolved stride of 0 is the binding model's "never advance" (see the
|
||||
// factory's layout notes): exactly one element is converted and every vertex
|
||||
// reads it. That single element is read at offset 0, so the stride is never
|
||||
// actually used - but both converters reject 0 as a degenerate input, which
|
||||
// made the documented single-element conversion unreachable and silently
|
||||
// dropped every draw using such a binding. Substitute the element's own
|
||||
// size; the caller's cache key still carries the distinct stride 0.
|
||||
if (sourceStride == 0 && elementCount == 1) {
|
||||
sourceStride = elementSize;
|
||||
}
|
||||
const void* uploadData = nullptr;
|
||||
VkDeviceSize uploadSize = 0;
|
||||
switch (conversion) {
|
||||
@@ -3663,6 +3709,12 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A GPU-written source (XFB capture, SSBO, storage texel buffer) has its
|
||||
// bytes produced by commands that are merely RECORDED at this point, and
|
||||
// MappedData() aliases the coherent GPU memory they will write into -
|
||||
// converting now would read pre-write garbage. Submit-and-wait first,
|
||||
// mirroring the restart-index rewrite; a flag-test no-op otherwise.
|
||||
sourceBufferShared->SyncGpuWrites();
|
||||
sourceBufferShared->SyncPersistentMappedRange();
|
||||
const SizeT availableElementCount =
|
||||
sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
||||
@@ -3956,7 +4008,7 @@ void main() {
|
||||
// Skips the per-draw GetBackendResource chase into a cold resource object.
|
||||
Bool sliceStillValid = false;
|
||||
const Uint64 frameSerial = m_bufferManager.GetFrameSerial();
|
||||
if (indexMemo->indexFrameSerial == frameSerial &&
|
||||
if (indexMemo->indexFrameSerial == frameSerial && !indexMemo->indexBufferMapped &&
|
||||
indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
|
||||
sliceStillValid = true;
|
||||
}
|
||||
@@ -4019,6 +4071,9 @@ void main() {
|
||||
indexMemo->indexVkBuffer = slice.buffer;
|
||||
indexMemo->indexSliceOffset = slice.offset;
|
||||
indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial();
|
||||
// A host-mapped EBO can mutate its shadow with no epoch bump; the hit
|
||||
// path declines on this flag (mirror of anyBufferMapped).
|
||||
indexMemo->indexBufferMapped = indexBufferShared->IsMapped();
|
||||
}
|
||||
}
|
||||
const VkDeviceSize indexBindOffset =
|
||||
@@ -5394,7 +5449,81 @@ void main() {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots(
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
VkPipelineStageFlags consumerShaderStageMask) {
|
||||
auto& feedbackBindings = m_samplerImageFeedbackScratch;
|
||||
auto& overrides = m_samplerImageBindingOverridesScratch;
|
||||
overrides.clear();
|
||||
if (!programObj.hasStorageImages) {
|
||||
feedbackBindings.clear();
|
||||
return true;
|
||||
}
|
||||
if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) {
|
||||
MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__,
|
||||
program.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feedbackBindings.empty()) {
|
||||
return true;
|
||||
}
|
||||
// Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only
|
||||
// gets here after an actual sampled/writable-image mip overlap was found, so ordinary
|
||||
// graphics draws retain the active pass.
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
struct SnapshotCacheEntry {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
VkTextureManager::SampledTextureSnapshot snapshot{};
|
||||
};
|
||||
Vector<SnapshotCacheEntry> snapshotCache;
|
||||
snapshotCache.reserve(feedbackBindings.size());
|
||||
overrides.reserve(feedbackBindings.size());
|
||||
for (const auto& feedback : feedbackBindings) {
|
||||
VkTextureManager::SampledTextureSnapshot snapshot{};
|
||||
const auto existing = std::find_if(
|
||||
snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) {
|
||||
return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain;
|
||||
});
|
||||
if (existing != snapshotCache.end()) {
|
||||
snapshot = existing->snapshot;
|
||||
} else {
|
||||
if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture,
|
||||
feedback.numericDomain, consumerShaderStageMask,
|
||||
snapshot) ||
|
||||
snapshot.imageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__,
|
||||
feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0,
|
||||
feedback.samplerBinding, feedback.samplerElement);
|
||||
return false;
|
||||
}
|
||||
snapshotCache.push_back({.texture = feedback.texture,
|
||||
.numericDomain = feedback.numericDomain,
|
||||
.snapshot = snapshot});
|
||||
}
|
||||
overrides.push_back({
|
||||
.binding = feedback.samplerBinding,
|
||||
.element = feedback.samplerElement,
|
||||
.texture = feedback.texture,
|
||||
.sampler = feedback.sampler,
|
||||
.imageView = snapshot.imageView,
|
||||
.imageLayout = snapshot.layout,
|
||||
.forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger ||
|
||||
feedback.numericDomain == SamplerNumericDomain::UnsignedInteger,
|
||||
});
|
||||
if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) {
|
||||
MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p",
|
||||
feedback.samplerBinding, feedback.samplerElement, snapshot.imageView);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
||||
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
|
||||
@@ -5614,6 +5743,22 @@ void main() {
|
||||
if (program.GetBackendStateVersion() != snap.programVersion) {
|
||||
return false;
|
||||
}
|
||||
// glBegin/EndTransformFeedback moves no key this fast path otherwise observes
|
||||
// (the design makes capture a compile-option FLAG precisely because no version
|
||||
// bumps, VulkanRenderer.h's pipeline-memo note) - but the snapshot bakes that
|
||||
// flag into resolvedTransformFlags and the pipeline. Recompute the one dynamic
|
||||
// bit (the full path's exact predicate) and decline on a mismatch, or the first
|
||||
// captured draw after glBeginTransformFeedback would bind the undecorated
|
||||
// variant and silently capture nothing while the CPU bookkeeping advances.
|
||||
const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled &&
|
||||
MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0;
|
||||
const Bool snapHasXfbCapture =
|
||||
static_cast<Bool>(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) &
|
||||
ProgramFactory::CompileOptionBit::XfbCapture);
|
||||
if (wantsXfbCapture != snapHasXfbCapture) {
|
||||
return false;
|
||||
}
|
||||
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
|
||||
// input state (re-resolved below through the layout-keyed memo, so N VAOs
|
||||
// sharing one attribute layout share one pipeline) and the vertex/index
|
||||
@@ -5627,6 +5772,7 @@ void main() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
||||
drawFbo->GetLifetimeId() != snap.drawFboLifetimeId ||
|
||||
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
||||
return false;
|
||||
}
|
||||
@@ -5810,8 +5956,14 @@ void main() {
|
||||
}
|
||||
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
|
||||
snap.samplingResolutionGeneration = samplingResolutionGeneration;
|
||||
samplerDescriptorsUnchanged = false;
|
||||
// Decline, not re-arm: snap.resolvedTransformFlags bakes the
|
||||
// ExplicitLod0Sampling verdict, which reads the effective sampler's
|
||||
// filters/aniso/LOD range - exactly the state this counter tracks.
|
||||
// Re-arming the stamp here would rebuild the descriptors but keep the
|
||||
// stale SPIR-V variant forever (every later draw compares equal again).
|
||||
// Same shape as the erase-epoch declines above; costs one full-path draw
|
||||
// per sampler/shape change, and the full path's LOD memo re-probes.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything the full path would re-resolve is provably unchanged - or, for
|
||||
@@ -5991,11 +6143,18 @@ void main() {
|
||||
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
|
||||
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
// The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range
|
||||
// (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the
|
||||
// sampling-resolution generation - not the texture params version the sum
|
||||
// below covers. Without this key a filter/aniso change would keep serving
|
||||
// the stale verdict.
|
||||
const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
Bool lodMemoHit = false;
|
||||
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
|
||||
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
|
||||
m_lastLodProgramVersion == lodProgramVersion &&
|
||||
m_lastLodBindGeneration == lodBindGeneration && m_lastLodBaseFlags == transformFlags &&
|
||||
m_lastLodBindGeneration == lodBindGeneration &&
|
||||
m_lastLodSamplingGeneration == lodSamplingGeneration && m_lastLodBaseFlags == transformFlags &&
|
||||
m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId &&
|
||||
m_lastSampledSetProgramVersion == lodProgramVersion &&
|
||||
m_lastSampledSetBindGeneration == lodBindGeneration) {
|
||||
@@ -6020,6 +6179,7 @@ void main() {
|
||||
m_lastLodProgramLifetimeId = lodProgramLifetimeId;
|
||||
m_lastLodProgramVersion = lodProgramVersion;
|
||||
m_lastLodBindGeneration = lodBindGeneration;
|
||||
m_lastLodSamplingGeneration = lodSamplingGeneration;
|
||||
m_lastLodBaseFlags = baseFlags;
|
||||
m_lastLodResultFlags = transformFlags;
|
||||
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
||||
@@ -6076,6 +6236,11 @@ void main() {
|
||||
MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed");
|
||||
return false;
|
||||
}
|
||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) {
|
||||
MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
|
||||
@@ -6320,7 +6485,9 @@ void main() {
|
||||
}
|
||||
|
||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
|
||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
||||
VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false,
|
||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
||||
if (!boundUniforms) {
|
||||
MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed");
|
||||
return false;
|
||||
@@ -6356,6 +6523,7 @@ void main() {
|
||||
snap.vaoLifetimeId = vao.GetLifetimeId();
|
||||
snap.vaoConfigVersion = vao.GetConfigVersion();
|
||||
snap.drawFbo = drawFbo.get();
|
||||
snap.drawFboLifetimeId = drawFbo->GetLifetimeId();
|
||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||
snap.drawFboIsDefault = drawFboIsDefault;
|
||||
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||
@@ -6443,6 +6611,11 @@ void main() {
|
||||
MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
|
||||
MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
@@ -6454,7 +6627,8 @@ void main() {
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
||||
VK_PIPELINE_BIND_POINT_COMPUTE);
|
||||
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
|
||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
||||
if (!boundUniforms) {
|
||||
MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed");
|
||||
return;
|
||||
@@ -6489,6 +6663,11 @@ void main() {
|
||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
|
||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
@@ -6500,7 +6679,8 @@ void main() {
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
||||
VK_PIPELINE_BIND_POINT_COMPUTE);
|
||||
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
|
||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
||||
if (!boundUniforms) {
|
||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed");
|
||||
return;
|
||||
@@ -8689,7 +8869,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 CopyImageEndpoint {
|
||||
struct CopyImageSliceMapping {
|
||||
// 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.
|
||||
@@ -8703,13 +8883,35 @@ void main() {
|
||||
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
|
||||
};
|
||||
|
||||
Bool TryResolveCopyImageEndpoint(TextureTarget target,
|
||||
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
|
||||
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
|
||||
// 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) {
|
||||
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:
|
||||
@@ -8717,12 +8919,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.
|
||||
outEndpoint = {};
|
||||
outMapping = {};
|
||||
return baseSlice == 0 && glDepth == 1;
|
||||
case TextureTarget::Texture3D:
|
||||
outEndpoint.slicesAreDepth = true;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
|
||||
outMapping.slicesAreDepth = true;
|
||||
outMapping.baseSlice = baseSlice;
|
||||
outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
|
||||
return true;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
@@ -8731,9 +8933,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.
|
||||
outEndpoint.slicesAreDepth = false;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = resource.arrayLayers;
|
||||
outMapping.slicesAreDepth = false;
|
||||
outMapping.baseSlice = baseSlice;
|
||||
outMapping.availableSlices = image.arrayLayers;
|
||||
return true;
|
||||
default:
|
||||
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||
@@ -8743,15 +8945,20 @@ 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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
|
||||
"CopyImageSubData requires valid source and destination textures.");
|
||||
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
|
||||
"CopyImageSubData requires valid source and destination images.");
|
||||
// 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.
|
||||
@@ -8768,9 +8975,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 (srcTexture.get() == dstTexture.get()) {
|
||||
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
|
||||
srcTexture->GetExternalIndex());
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8783,8 +8990,42 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture);
|
||||
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture);
|
||||
// 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);
|
||||
// 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
|
||||
@@ -8800,29 +9041,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 (srcResource == nullptr || dstResource == nullptr) {
|
||||
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||
if (!srcResolved || !dstResolved) {
|
||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
||||
return;
|
||||
}
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels ||
|
||||
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) {
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
|
||||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
|
||||
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
|
||||
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels);
|
||||
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
|
||||
return;
|
||||
}
|
||||
const VkImageAspectFlags copyAspectMask =
|
||||
srcResource->aspect & dstResource->aspect &
|
||||
srcImage.aspect & dstImage.aspect &
|
||||
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
MOBILEGL_ASSERT(copyAspectMask != 0 &&
|
||||
(srcResource->aspect & copyAspectMask) == srcResource->aspect &&
|
||||
(dstResource->aspect & copyAspectMask) == dstResource->aspect,
|
||||
(srcImage.aspect & copyAspectMask) == srcImage.aspect &&
|
||||
(dstImage.aspect & copyAspectMask) == dstImage.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, 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);
|
||||
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);
|
||||
// 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,
|
||||
@@ -8845,10 +9086,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.
|
||||
CopyImageEndpoint srcEndpoint;
|
||||
CopyImageEndpoint dstEndpoint;
|
||||
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
|
||||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
|
||||
CopyImageSliceMapping srcSlices;
|
||||
CopyImageSliceMapping dstSlices;
|
||||
if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
|
||||
!TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
|
||||
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);
|
||||
@@ -8859,40 +9100,53 @@ 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 (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
|
||||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
|
||||
if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
|
||||
dstSlices.baseSlice + copySliceCount > dstSlices.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, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
|
||||
__func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
|
||||
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
|
||||
__func__, srcTexture->GetExternalIndex());
|
||||
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));
|
||||
// A clear still parked on the destination would otherwise materialize AFTER this copy and
|
||||
// wipe the texels it just wrote.
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
|
||||
__func__, dstTexture->GetExternalIndex());
|
||||
const Bool dstClearReady = materializeClear(dstEndpoint);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
|
||||
__func__, CopyImageEndpointName(dstEndpoint));
|
||||
|
||||
const VkImageLayout srcOriginalLayout = srcResource->layout;
|
||||
const VkImageLayout dstOriginalLayout = dstResource->layout;
|
||||
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
|
||||
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
|
||||
// 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.
|
||||
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
|
||||
// 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) {
|
||||
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
return originalLayout;
|
||||
}
|
||||
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 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;
|
||||
};
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
@@ -8903,15 +9157,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, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
srcImage.aspect, 0, srcImage.mipLevels);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||
srcCopyLayout = srcResource->layout;
|
||||
srcCopyLayout = *srcImage.trackedLayout;
|
||||
} else {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
frame.commandBuffer, srcImage.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__);
|
||||
@@ -8923,15 +9177,15 @@ void main() {
|
||||
VkImageLayout dstCopyLayout = dstOriginalLayout;
|
||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
dstImage.aspect, 0, dstImage.mipLevels);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||
dstCopyLayout = dstResource->layout;
|
||||
dstCopyLayout = *dstImage.trackedLayout;
|
||||
} else {
|
||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
frame.commandBuffer, dstImage.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__);
|
||||
@@ -8941,18 +9195,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 = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
|
||||
const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
|
||||
VkImageCopy copyRegion{};
|
||||
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
|
||||
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstSlices.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 "
|
||||
@@ -8963,8 +9217,8 @@ void main() {
|
||||
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
|
||||
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
|
||||
vkCmdCopyImage(frame.commandBuffer,
|
||||
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, ©Region);
|
||||
|
||||
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -8972,14 +9226,14 @@ void main() {
|
||||
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
srcImage.aspect, 0, srcImage.mipLevels);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||
} else {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||
frame.commandBuffer, srcImage.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__);
|
||||
@@ -8990,14 +9244,14 @@ void main() {
|
||||
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
|
||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
dstImage.aspect, 0, dstImage.mipLevels);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||
} else {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
||||
frame.commandBuffer, dstImage.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__);
|
||||
@@ -11899,6 +12153,17 @@ void main() {
|
||||
}
|
||||
|
||||
void VulkanRenderer::CreateInstance() {
|
||||
#if defined(VK_USE_PLATFORM_METAL_EXT)
|
||||
// MoltenVK snapshots its configuration when the loader first discovers the ICD. Set
|
||||
// this before instance-extension enumeration, while preserving an explicit user value.
|
||||
if (std::getenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS") == nullptr) {
|
||||
if (::setenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0) == 0) {
|
||||
MGLOG_I("MoltenVK: enabling Metal argument buffers");
|
||||
} else {
|
||||
MGLOG_W("MoltenVK: could not enable Metal argument buffers before ICD discovery");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
m_extensions = EnumerateInstanceExtensions();
|
||||
MGLOG_I("Got %d Vulkan instance extensions: ", m_extensions.size());
|
||||
for (auto& extension : m_extensions) {
|
||||
@@ -12040,17 +12305,19 @@ void main() {
|
||||
|
||||
auto debugMessengerCreateInfo = PopulateDebugMessengerCreateInfo();
|
||||
// Layers
|
||||
const void* instanceCreatePNext = nullptr;
|
||||
if (m_validationLayersEnabled) {
|
||||
MGLOG_I("Enabling validation layer...");
|
||||
instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames));
|
||||
instanceInfo.ppEnabledLayerNames = s_validationLayerNames;
|
||||
// Chaining the messenger create-info is only legal with the extension on.
|
||||
instanceInfo.pNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
|
||||
instanceCreatePNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
|
||||
} else {
|
||||
instanceInfo.enabledLayerCount = 0;
|
||||
instanceInfo.pNext = nullptr;
|
||||
}
|
||||
|
||||
instanceInfo.pNext = instanceCreatePNext;
|
||||
|
||||
VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed");
|
||||
|
||||
if (debugUtilsAvailable) {
|
||||
@@ -12476,6 +12743,75 @@ void main() {
|
||||
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2KHR"));
|
||||
}
|
||||
|
||||
m_updateAfterBindLimits = {};
|
||||
VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures{};
|
||||
descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES;
|
||||
VkPhysicalDeviceDescriptorIndexingProperties descriptorIndexingProperties{};
|
||||
descriptorIndexingProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES;
|
||||
const Bool descriptorIndexingCore = m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2;
|
||||
const Bool descriptorIndexingExtension =
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
|
||||
if (getPhysicalDeviceProperties2 == nullptr) {
|
||||
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
|
||||
}
|
||||
if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr &&
|
||||
getPhysicalDeviceProperties2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &descriptorIndexingFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
VkPhysicalDeviceProperties2 propertyQuery{};
|
||||
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
propertyQuery.pNext = &descriptorIndexingProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
|
||||
|
||||
// This renderer emits every descriptor category listed below, including
|
||||
// dynamic UBOs and combined image samplers. Do not enable a partial
|
||||
// descriptor-indexing contract: it would make a later reflected program
|
||||
// fail in the driver instead of choosing its ordinary descriptor layout.
|
||||
const Bool allUpdateAfterBindFeatures =
|
||||
descriptorIndexingFeatures.descriptorBindingUniformBufferUpdateAfterBind == VK_TRUE &&
|
||||
descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE &&
|
||||
descriptorIndexingFeatures.descriptorBindingStorageImageUpdateAfterBind == VK_TRUE &&
|
||||
descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE &&
|
||||
descriptorIndexingFeatures.descriptorBindingUniformTexelBufferUpdateAfterBind == VK_TRUE &&
|
||||
descriptorIndexingFeatures.descriptorBindingStorageTexelBufferUpdateAfterBind == VK_TRUE &&
|
||||
(!deviceFeatures.robustBufferAccess || descriptorIndexingProperties.robustBufferAccessUpdateAfterBind);
|
||||
if (allUpdateAfterBindFeatures) {
|
||||
if (!descriptorIndexingCore && !IsExtensionAlreadyEnabled(
|
||||
enabledDeviceExtensions,
|
||||
VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
}
|
||||
descriptorIndexingFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &descriptorIndexingFeatures;
|
||||
m_updateAfterBindLimits = {
|
||||
true,
|
||||
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSamplers,
|
||||
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindUniformBuffers,
|
||||
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageBuffers,
|
||||
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSampledImages,
|
||||
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageImages,
|
||||
descriptorIndexingProperties.maxPerStageUpdateAfterBindResources,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSamplers,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffers,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffersDynamic,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffers,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffersDynamic,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSampledImages,
|
||||
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageImages};
|
||||
MGLOG_I("Vulkan: update-after-bind descriptor layouts enabled");
|
||||
} else {
|
||||
MGLOG_I("Vulkan: descriptor indexing is present but lacks the complete update-after-bind feature set; "
|
||||
"using ordinary descriptor layouts");
|
||||
}
|
||||
} else {
|
||||
MGLOG_I("Vulkan: descriptor indexing unavailable; using ordinary descriptor layouts");
|
||||
}
|
||||
|
||||
VkPhysicalDeviceIndexTypeUint8Features indexTypeUint8Features{};
|
||||
indexTypeUint8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES;
|
||||
if (indexTypeUint8ExtensionName != nullptr) {
|
||||
@@ -12547,6 +12883,73 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Native subgroup topology, and VK_EXT_subgroup_size_control's
|
||||
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
|
||||
// turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from
|
||||
// encouraged-but-unspecified driver behaviour into a spec guarantee: with the bit
|
||||
// set and local_size_x a multiple of the subgroup size, every subgroup launches
|
||||
// full, so the subgroup count is exactly invocations / size ("Full Subgroups",
|
||||
// VUID-VkPipelineShaderStageCreateInfo-flags-02759/-02785).
|
||||
m_nativeSubgroupSize = 0;
|
||||
m_nativeSubgroupSupported = false;
|
||||
m_computeFullSubgroupsFeatureEnabled = false;
|
||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
|
||||
VkPhysicalDeviceProperties2 subgroupPropertyQuery{};
|
||||
subgroupPropertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
subgroupPropertyQuery.pNext = &subgroupProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
|
||||
// Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the
|
||||
// MOBILEGL_DISABLE_SUBGROUP escape hatch, so the module lowerings can never
|
||||
// disagree with the advertised capabilities.
|
||||
const Bool usableSubgroups =
|
||||
subgroupProperties.subgroupSize > 0 &&
|
||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
|
||||
if (usableSubgroups && !MG_Config::Features.DisableSubgroup) {
|
||||
m_nativeSubgroupSize = subgroupProperties.subgroupSize;
|
||||
m_nativeSubgroupSupported = true;
|
||||
}
|
||||
}
|
||||
VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeControlFeatures{};
|
||||
subgroupSizeControlFeatures.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT;
|
||||
m_maxComputeWorkgroupSubgroups = 0;
|
||||
if (m_nativeSubgroupSupported &&
|
||||
IsExtensionSupported(availableExtensions, VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &subgroupSizeControlFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
||||
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSizeControlProperties{};
|
||||
subgroupSizeControlProperties.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT;
|
||||
VkPhysicalDeviceProperties2 propertyQuery{};
|
||||
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
propertyQuery.pNext = &subgroupSizeControlProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
|
||||
m_maxComputeWorkgroupSubgroups =
|
||||
subgroupSizeControlProperties.maxComputeWorkgroupSubgroups;
|
||||
}
|
||||
if (subgroupSizeControlFeatures.computeFullSubgroups == VK_TRUE) {
|
||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
||||
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
|
||||
}
|
||||
// Only the full-subgroups guarantee is wanted; required/varying subgroup
|
||||
// sizes stay unrequested.
|
||||
subgroupSizeControlFeatures.subgroupSizeControl = VK_FALSE;
|
||||
subgroupSizeControlFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &subgroupSizeControlFeatures;
|
||||
m_computeFullSubgroupsFeatureEnabled = true;
|
||||
MGLOG_I("Enabled optional device extension: %s (computeFullSubgroups)",
|
||||
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// VK_EXT_transform_feedback backs GL transform feedback capture.
|
||||
m_transformFeedbackFeatureEnabled = false;
|
||||
VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{};
|
||||
@@ -13553,6 +13956,15 @@ void main() {
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline),
|
||||
"GetOrCreateComputePipeline, vkCreateComputePipelines");
|
||||
// A failed creation must never be memoized - same contract as
|
||||
// PipelineFactory::GetOrCreatePipeline: caching the null would serve it back
|
||||
// for the rest of the process and every dispatch of this program would be
|
||||
// silently skipped. Retrying costs one failed vkCreateComputePipelines per
|
||||
// dispatch, which is the correct price.
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
MGLOG_E("GetOrCreateComputePipeline: vkCreateComputePipelines failed; not caching the failure");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_computePipelines.emplace(programObj.hash, pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#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"
|
||||
@@ -197,9 +198,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 SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const CopyImageEndpoint& dstEndpoint,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -554,7 +555,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||
// Native subgroup topology, queried at device creation for the compute-module
|
||||
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
||||
// stage flag; 0 / false when the device has no usable compute subgroups or
|
||||
// MOBILEGL_DISABLE_SUBGROUP forced them off.
|
||||
Uint32 m_nativeSubgroupSize = 0;
|
||||
Bool m_nativeSubgroupSupported = false;
|
||||
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
|
||||
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
|
||||
Uint32 m_maxComputeWorkgroupSubgroups = 0;
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// Set only after descriptor-indexing feature AND property queries prove that
|
||||
// update-after-bind is legal for every descriptor category this renderer emits.
|
||||
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||
// drive a runtime fallback when the device lacks them.
|
||||
@@ -794,6 +808,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_lastLodProgramVersion = 0;
|
||||
Uint64 m_lastLodBindGeneration = 0;
|
||||
Uint64 m_lastLodParamsSum = 0;
|
||||
// Sampling-resolution generation at probe time. The probe reads the effective
|
||||
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
|
||||
// the params-version sum above never moves for them.
|
||||
Uint64 m_lastLodSamplingGeneration = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
@@ -831,6 +849,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 vaoLifetimeId = 0;
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||
// deleted FBO recycled at the same address with the same fresh version
|
||||
// count would otherwise compare equal (same ABA as the render-pass
|
||||
// manager's fast-path memo).
|
||||
Uint64 drawFboLifetimeId = 0;
|
||||
Uint16 fboVersion = 0;
|
||||
Bool drawFboIsDefault = false;
|
||||
Uint renderStateVersion = 0;
|
||||
@@ -926,6 +949,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
|
||||
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||
@@ -1052,6 +1077,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize indexSliceOffset = 0;
|
||||
Uint64 indexFrameSerial = 0;
|
||||
// The EBO carried a host map when the slice was recorded - the mirror of
|
||||
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
|
||||
// persistent map mutates its shadow with no API call and no epoch bump, so
|
||||
// the one-compare rescue must decline and re-run the acquire, whose
|
||||
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
|
||||
// is already covered: AcquirePersistentMap bumps the slice epoch for the
|
||||
// request itself, adopted or declined.
|
||||
Bool indexBufferMapped = false;
|
||||
|
||||
// Bound per draw (first bindingCount elements).
|
||||
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||
@@ -1145,6 +1178,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
||||
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
||||
// storage descriptor continues to name the application texture.
|
||||
Bool PrepareSamplerImageFeedbackSnapshots(
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
VkPipelineStageFlags consumerShaderStageMask);
|
||||
|
||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.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 <Config.h>
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
|
||||
// shared by capability advertisement (BackendObject) and module lowering
|
||||
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
|
||||
//
|
||||
// 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:
|
||||
// - 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
|
||||
// devices with no subgroup support at all, and only when the user opts in with
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
|
||||
|
||||
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
|
||||
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
|
||||
inline constexpr Uint32 kEmulatedSubgroupFeatures =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
||||
|
||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||
!MG_Config::Features.DisableSubgroup;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||
// grows one under-declared array; every other module passes through untouched.
|
||||
return MG_Config::Features.FixIterationRPSubgroupScratch !=
|
||||
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
|
||||
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
||||
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -43,4 +43,5 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
add_subdirectory(Driver)
|
||||
add_subdirectory(Container)
|
||||
add_subdirectory(Container)
|
||||
add_subdirectory(Transpile)
|
||||
@@ -0,0 +1,20 @@
|
||||
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
@@ -18,6 +18,7 @@
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
NamedBufferData,
|
||||
NamedBufferSubData,
|
||||
CopyNamedBufferSubData,
|
||||
ClearBufferData,
|
||||
ClearBufferSubData,
|
||||
ClearNamedBufferData,
|
||||
ClearNamedBufferSubData,
|
||||
MapBufferRange,
|
||||
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return "NamedBufferSubData";
|
||||
case BufferOp::CopyNamedBufferSubData:
|
||||
return "CopyNamedBufferSubData";
|
||||
case BufferOp::ClearBufferData:
|
||||
return "ClearBufferData";
|
||||
case BufferOp::ClearBufferSubData:
|
||||
return "ClearBufferSubData";
|
||||
case BufferOp::ClearNamedBufferData:
|
||||
return "ClearNamedBufferData";
|
||||
case BufferOp::ClearNamedBufferSubData:
|
||||
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The pattern is replicated verbatim, which is only the whole story while the client
|
||||
// layout already matches the internal format - the case every entry point in practice
|
||||
// uses, and the only one the conversion machinery here can express. Say so rather than
|
||||
// quietly writing a differently-sized pattern.
|
||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||
if (sourceSize != elementSize) {
|
||||
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
"converting between them is not implemented",
|
||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
||||
}
|
||||
return elementSize;
|
||||
}
|
||||
|
||||
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
|
||||
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
|
||||
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||
|
||||
Vector<Uint8> zeroInput;
|
||||
const void* inputPixel = data;
|
||||
if (inputPixel == nullptr) {
|
||||
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
|
||||
if (inputSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||
"format and type do not describe a source pixel."));
|
||||
return false;
|
||||
}
|
||||
zeroInput.resize(inputSize);
|
||||
inputPixel = zeroInput.data();
|
||||
}
|
||||
|
||||
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
|
||||
internal, inputFormat, inputType, inputPixel, pattern)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", GetBufferOpName(op),
|
||||
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
|
||||
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
|
||||
internalformat)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data == nullptr) {
|
||||
// GL defines a null clear value as all zero bits in the destination store, while
|
||||
// retaining the format/type validation above.
|
||||
pattern.assign(patternSize, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
||||
if (patternSize == 0) return;
|
||||
|
||||
auto bufferObject = GetNamedBufferObject(buffer, op);
|
||||
if (!bufferObject) return;
|
||||
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
||||
if (size == 0) return;
|
||||
|
||||
Vector<Uint8> clearData(static_cast<SizeT>(size));
|
||||
if (data) {
|
||||
const auto* pattern = static_cast<const Uint8*>(data);
|
||||
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
|
||||
Memcpy(clearData.data() + at, pattern, patternSize);
|
||||
}
|
||||
} else {
|
||||
Memset(clearData.data(), 0, clearData.size());
|
||||
}
|
||||
|
||||
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
|
||||
Vector<Uint8> pattern;
|
||||
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
|
||||
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
|
||||
static_cast<SizeT>(size));
|
||||
}
|
||||
|
||||
auto& GetBufferBindingSlot(BufferTarget target) {
|
||||
@@ -1197,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
||||
}
|
||||
|
||||
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearBufferData);
|
||||
}
|
||||
|
||||
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearBufferSubData);
|
||||
}
|
||||
|
||||
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
||||
if (!bufferObject) return;
|
||||
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearNamedBufferData);
|
||||
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearNamedBufferData);
|
||||
}
|
||||
|
||||
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data) {
|
||||
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearNamedBufferSubData);
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearNamedBufferSubData);
|
||||
}
|
||||
|
||||
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
|
||||
@@ -1662,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
||||
}
|
||||
|
||||
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
ClearBufferData_State(target, internalformat, format, type, data);
|
||||
}
|
||||
|
||||
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data) {
|
||||
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
|
||||
}
|
||||
|
||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
||||
GLsizeiptr size);
|
||||
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data);
|
||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data);
|
||||
|
||||
@@ -108,6 +108,12 @@ 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) {
|
||||
@@ -127,6 +133,11 @@ 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
|
||||
@@ -151,11 +162,23 @@ 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 (!IsAcceptedPrimitiveMode(mode)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
||||
if (!ValidatePrimitiveModeEnum(functionName, mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -596,12 +619,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!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);
|
||||
@@ -715,12 +740,14 @@ 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);
|
||||
@@ -728,6 +755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||
@@ -736,6 +764,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||
@@ -743,18 +772,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!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;
|
||||
@@ -764,18 +796,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!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;
|
||||
@@ -783,6 +818,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
@@ -790,6 +826,7 @@ 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);
|
||||
@@ -797,6 +834,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (drawcount < 0) {
|
||||
@@ -810,6 +848,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||
@@ -817,6 +856,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||
@@ -827,6 +867,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
|
||||
@@ -985,8 +985,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLen
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#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>
|
||||
@@ -617,16 +618,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
||||
return GetAdvertisedMaxSamples();
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
@@ -645,7 +647,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, 1);
|
||||
// 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());
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
@@ -3148,15 +3153,55 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,23 @@ 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;
|
||||
@@ -422,6 +439,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
||||
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
||||
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
||||
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
||||
// just told the application to use. The backends clamp the realised count instead.
|
||||
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;
|
||||
@@ -1536,7 +1565,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxFragmentInputComponents;
|
||||
@@ -1562,7 +1591,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryInputComponents;
|
||||
@@ -1633,10 +1662,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
*params =
|
||||
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
@@ -1662,7 +1692,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||
return;
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
|
||||
return;
|
||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxVertexUniformComponents;
|
||||
@@ -2117,7 +2147,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxClipDistances;
|
||||
break;
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
*params = dynamicParameters.MaxColorTextureSamples;
|
||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||
break;
|
||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||
@@ -2147,7 +2177,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||
break;
|
||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||
*params = dynamicParameters.MaxDepthTextureSamples;
|
||||
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
||||
break;
|
||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||
*params = dynamicParameters.MaxFramebufferWidth;
|
||||
@@ -2174,7 +2204,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||
break;
|
||||
case GL_MAX_INTEGER_SAMPLES:
|
||||
*params = dynamicParameters.MaxIntegerSamples;
|
||||
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
break;
|
||||
case GL_MAX_RENDERBUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxRenderbufferSize;
|
||||
@@ -2340,7 +2370,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: dynamicParameters.MaxDrawBuffers;
|
||||
break;
|
||||
case GL_MAX_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
|
||||
*params = GetAdvertisedMaxSamples();
|
||||
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,4 +24,8 @@ 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
|
||||
|
||||
@@ -1057,21 +1057,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
static Bool allowVSOnlyPrograms;
|
||||
static Bool initialized = false;
|
||||
if (!initialized) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
allowVSOnlyPrograms = (Int)rendererInfo.StaticBackendCapability.AllowVSOnlyPrograms;
|
||||
}
|
||||
// Read fresh every link, never latched in a static: the capability is
|
||||
// per-backend, and a latch would freeze it across a backend teardown +
|
||||
// re-initialization (the previous function-static memo here never even set
|
||||
// its own initialized flag, so it re-read every call anyway - this makes
|
||||
// the always-fresh behavior the stated one). A struct-field read per
|
||||
// glLinkProgram costs nothing.
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (activeBackendObject) {
|
||||
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const Bool allowVSOnlyPrograms =
|
||||
activeBackendObject->GetRendererInfo().StaticBackendCapability.AllowVSOnlyPrograms;
|
||||
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
||||
programObject->Link(!allowVSOnlyPrograms);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Bool ended = false;
|
||||
Bool resultCached = false;
|
||||
Uint64 cachedResult = 0;
|
||||
// Transform feedback primitive counter at BeginQuery time.
|
||||
// The transform feedback primitive counter matching this query's target, 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
|
||||
@@ -122,6 +129,46 @@ 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
|
||||
@@ -407,7 +454,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||
queryObject->accountedCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
||||
queryObject->geometryCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
||||
} else if (isOcclusionQuery) {
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
@@ -448,12 +499,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
// Result comes from the GPU query at read time.
|
||||
} else {
|
||||
queryObject->cachedResult =
|
||||
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
||||
}
|
||||
// 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);
|
||||
queryObject->resultCached = true;
|
||||
}
|
||||
// Otherwise the result comes from the GPU query at read time.
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
@@ -648,4 +708,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||
GetQueryiv(target, pname, params);
|
||||
}
|
||||
|
||||
void DestroyAllQueryObjects() {
|
||||
// Detach the registry under the lock, release outside it - same discipline
|
||||
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
|
||||
// this drain, every query the app left undeleted survived full library
|
||||
// teardown in the process-global registry: the objects and their backend
|
||||
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
|
||||
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
|
||||
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
|
||||
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
|
||||
UnorderedMap<GLuint, QueryObject*> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
orphans.swap(g_liveQueryObjects);
|
||||
g_activeTimeElapsedQueryId = 0;
|
||||
g_activePrimitivesWrittenQueryId = 0;
|
||||
g_activePrimitivesGeneratedQueryId = 0;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
}
|
||||
if (orphans.empty()) {
|
||||
return;
|
||||
}
|
||||
// Backend handles must be released by the backend that created them, so
|
||||
// this runs while the function table is still populated. Both backends'
|
||||
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
|
||||
// or ES context is already gone frees only the wrapper.
|
||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -29,4 +29,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
void QueryCounter(GLuint id, GLenum target);
|
||||
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||
// GL requires queries to die with their context; called only from full library
|
||||
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||
// process-global registry can be drained wholesale. Must run while the backend
|
||||
// function table is still populated: each backend handle has to be released by
|
||||
// the backend that created it, never by a later re-initialized one (whose
|
||||
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
|
||||
// Same contract as DestroyAllSyncObjects.
|
||||
void DestroyAllQueryObjects();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "GL_Sync.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -35,6 +36,22 @@ 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;
|
||||
@@ -64,6 +81,18 @@ 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;
|
||||
|
||||
@@ -474,15 +474,48 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
target == TextureTarget::Texture2DMultisampleArray;
|
||||
}
|
||||
|
||||
Int GetMaxSupportedTextureSamples(TextureInternalFormat textureInternalFormat) {
|
||||
// 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) {
|
||||
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, 1);
|
||||
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
|
||||
}
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
|
||||
@@ -495,7 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
|
||||
: dynamicParameters.MaxColorTextureSamples,
|
||||
1);
|
||||
advertisedMaxSamples);
|
||||
}
|
||||
|
||||
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
|
||||
@@ -532,7 +565,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(textureInternalFormat);
|
||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, 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.
|
||||
@@ -557,6 +590,20 @@ 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);
|
||||
@@ -644,6 +691,34 @@ 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(
|
||||
@@ -679,6 +754,21 @@ 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;
|
||||
}
|
||||
|
||||
@@ -2150,6 +2240,26 @@ 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) {
|
||||
@@ -2296,6 +2406,13 @@ 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) {
|
||||
@@ -2384,6 +2501,13 @@ 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) {
|
||||
@@ -3365,9 +3489,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
const MG_Backend::CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
|
||||
@@ -3378,7 +3502,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support image-to-image copies."));
|
||||
return;
|
||||
}
|
||||
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX,
|
||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
@@ -3425,9 +3549,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 SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
if (endpoint.Exists()) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -3451,21 +3575,106 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
// ---- 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;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
|
||||
// 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,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const MG_Backend::CopyImageEndpoint& dst,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!ValidateCopyImageObjectExists(src, "source") ||
|
||||
!ValidateCopyImageObjectExists(dst, "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))) {
|
||||
return false;
|
||||
}
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
@@ -3473,8 +3682,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
@@ -3488,8 +3697,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 (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||
if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
|
||||
!ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
|
||||
return false;
|
||||
}
|
||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||
@@ -3505,37 +3714,41 @@ 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.
|
||||
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||
const Int srcSamples = GetCopyImageEndpointSamples(src);
|
||||
const Int dstSamples = GetCopyImageEndpointSamples(dst);
|
||||
if (srcSamples != dstSamples) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("The two images have different sample counts ({} vs. {}).",
|
||||
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||
srcSamples, dstSamples)));
|
||||
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.
|
||||
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||
const Bool srcComplete = IsCopyImageEndpointComplete(src);
|
||||
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
|
||||
if (!srcComplete || !dstComplete) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||
srcComplete, dstComplete)));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
@@ -4051,8 +4264,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces.
|
||||
if (!textureObject->IsComplete()) {
|
||||
// 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()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
|
||||
@@ -4084,8 +4303,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
|
||||
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8
|
||||
// (not advertised by MobileGL).
|
||||
// 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.
|
||||
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
||||
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
|
||||
return false;
|
||||
@@ -4111,33 +4330,48 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto* textureMipmapObject =
|
||||
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
const auto& uploadTargets = textureObject->GetUploadTargets();
|
||||
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();
|
||||
// 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 (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||
// 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) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Packing would write past the end of the pixel pack buffer."));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4372,6 +4606,12 @@ 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.
|
||||
@@ -4440,6 +4680,12 @@ 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));
|
||||
@@ -4447,32 +4693,6 @@ 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__);
|
||||
@@ -4515,6 +4735,10 @@ 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);
|
||||
@@ -4524,6 +4748,19 @@ 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));
|
||||
@@ -4684,6 +4921,22 @@ 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
|
||||
@@ -4704,6 +4957,7 @@ 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__);
|
||||
@@ -4714,6 +4968,7 @@ 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),
|
||||
@@ -5715,17 +5970,29 @@ 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 lookup, which answers a null
|
||||
// INVALID_OPERATION - so resolve through the plain lookups, which answer a null
|
||||
// SharedPtr, and let the validator record the error this entry point owes.
|
||||
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,
|
||||
//
|
||||
// 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,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
|
||||
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
|
||||
@@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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");
|
||||
// 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");
|
||||
}
|
||||
|
||||
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
|
||||
|
||||
@@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||
|
||||
# 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")
|
||||
# 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")
|
||||
return()
|
||||
endif()
|
||||
|
||||
@@ -68,6 +73,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/IterationRPFirstReductionScenario.cpp
|
||||
Scenarios/IterationRPProgram203Scenario.cpp
|
||||
Scenarios/IterationRPScratchFixScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
@@ -78,6 +86,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
@@ -92,9 +101,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||
GTest::gtest
|
||||
MobileGL_s
|
||||
${MGL_ITEST_MOBILEGL_TARGET}
|
||||
)
|
||||
|
||||
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
|
||||
@@ -103,6 +123,10 @@ 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
|
||||
@@ -223,6 +247,19 @@ 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
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
|
||||
#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
|
||||
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
||||
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
||||
@@ -32,7 +42,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__) && __has_include(<sys/wait.h>)
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
@@ -53,6 +63,83 @@ namespace MGITest {
|
||||
constexpr int kSurfaceWidth = 128;
|
||||
constexpr int kSurfaceHeight = 96;
|
||||
|
||||
#if defined(_WIN32)
|
||||
HWND g_testWindow = nullptr;
|
||||
|
||||
HWND CreateTestWindow() {
|
||||
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
WNDCLASSW windowClass{};
|
||||
windowClass.lpfnWndProc = DefWindowProcW;
|
||||
windowClass.hInstance = GetModuleHandleW(nullptr);
|
||||
windowClass.lpszClassName = kClassName;
|
||||
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
return nullptr;
|
||||
}
|
||||
registered = true;
|
||||
}
|
||||
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
|
||||
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
|
||||
}
|
||||
|
||||
std::string EnvOr(const char* name, const char* fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
||||
@@ -87,10 +174,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. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// fallback shape on desktop. Android instead supplies an AImageReader
|
||||
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// driver that consults them directly cannot reintroduce the dependency
|
||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
||||
// for Android, so no device path is affected.
|
||||
// behind EGL's back.
|
||||
void EnsureHeadlessPlatform() {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
static bool done = false;
|
||||
@@ -134,8 +221,9 @@ namespace MGITest {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const bool useWindowSurface = UseWindowSurface();
|
||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
||||
EGL_PBUFFER_BIT,
|
||||
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
|
||||
EGL_RED_SIZE,
|
||||
8,
|
||||
EGL_GREEN_SIZE,
|
||||
@@ -152,7 +240,9 @@ namespace MGITest {
|
||||
EGLConfig config = nullptr;
|
||||
EGLint configCount = 0;
|
||||
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
||||
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||
outReason = WithEglError(useWindowSurface
|
||||
? "eglChooseConfig found no window-capable RGBA8/D24 config"
|
||||
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||
return 4;
|
||||
}
|
||||
|
||||
@@ -166,10 +256,32 @@ namespace MGITest {
|
||||
return 5;
|
||||
}
|
||||
|
||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
if (useWindowSurface) {
|
||||
#if defined(_WIN32)
|
||||
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
|
||||
if (g_testWindow == nullptr) {
|
||||
outReason = "failed to create the Windows integration-test window";
|
||||
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) {
|
||||
outReason = WithEglError("eglCreatePbufferSurface failed");
|
||||
#if defined(__ANDROID__)
|
||||
DestroyImageReaderWindow();
|
||||
#endif
|
||||
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
||||
: "eglCreatePbufferSurface failed");
|
||||
return 6;
|
||||
}
|
||||
// The step that brings the whole backend up (DirectVulkan creates its
|
||||
@@ -491,6 +603,14 @@ namespace MGITest {
|
||||
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
||||
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
||||
eglTerminate(display);
|
||||
#if defined(_WIN32)
|
||||
if (g_testWindow != nullptr) {
|
||||
DestroyWindow(g_testWindow);
|
||||
g_testWindow = nullptr;
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
DestroyImageReaderWindow();
|
||||
#endif
|
||||
m_context = nullptr;
|
||||
m_surface = nullptr;
|
||||
m_display = 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, 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).
|
||||
// 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).
|
||||
//
|
||||
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||
|
||||
@@ -31,8 +31,14 @@ 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");
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
||||
#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",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||
surfaceKind,
|
||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||
} else if (MGITest::RequireGpu()) {
|
||||
std::fprintf(stderr,
|
||||
|
||||
@@ -374,6 +374,86 @@ 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;
|
||||
@@ -435,6 +515,31 @@ 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
|
||||
|
||||
@@ -0,0 +1,898 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.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
|
||||
//
|
||||
// Scenario - ITERATIONRP'S FIRST SUBGROUP REDUCTION.
|
||||
//
|
||||
// iterationRP reduces a 32 x 16 exposure tile with a vector subgroup inclusive add,
|
||||
// then a shared-memory scan of subgroup totals. The source assumes that every
|
||||
// subgroup has a last lane, that there are 2..32 subgroups, and that local index
|
||||
// 511 belongs to the last subgroup and its last lane. Those are source assumptions,
|
||||
// not API contracts. This probe intentionally does not repair them: it records the
|
||||
// observed topology and makes each handoff independently observable.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kInvocationCount = 512;
|
||||
constexpr std::size_t kScanStageCount = 6;
|
||||
constexpr std::uint32_t kQuietNanBits = 0x7fc00000u;
|
||||
constexpr std::size_t kNoSlot = std::numeric_limits<std::size_t>::max();
|
||||
|
||||
struct UVec4 {
|
||||
std::uint32_t x;
|
||||
std::uint32_t y;
|
||||
std::uint32_t z;
|
||||
std::uint32_t w;
|
||||
};
|
||||
|
||||
struct Vec4 {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
float w;
|
||||
};
|
||||
|
||||
// Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte
|
||||
// stride, floats are a dense scalar array, and the outer scan array is
|
||||
// stage-major in both GLSL and C++.
|
||||
struct ProbeOutput {
|
||||
std::array<UVec4, kInvocationCount> invocation;
|
||||
std::array<UVec4, kInvocationCount> subgroup;
|
||||
std::array<Vec4, kInvocationCount> reduction;
|
||||
std::array<float, kInvocationCount> finalAverage;
|
||||
std::array<std::array<float, kInvocationCount>, kScanStageCount> scanAfter;
|
||||
};
|
||||
|
||||
static_assert(sizeof(UVec4) == 16);
|
||||
static_assert(sizeof(Vec4) == 16);
|
||||
static_assert(std::is_standard_layout_v<ProbeOutput>);
|
||||
static_assert(offsetof(ProbeOutput, invocation) == 0);
|
||||
static_assert(offsetof(ProbeOutput, subgroup) == 8192);
|
||||
static_assert(offsetof(ProbeOutput, reduction) == 16384);
|
||||
static_assert(offsetof(ProbeOutput, finalAverage) == 24576);
|
||||
static_assert(offsetof(ProbeOutput, scanAfter) == 26624);
|
||||
static_assert(sizeof(ProbeOutput) == 38912);
|
||||
|
||||
enum class InputMode {
|
||||
SampledRgba32f,
|
||||
IndexedSsbo,
|
||||
};
|
||||
|
||||
const char* InputModeName(InputMode mode) {
|
||||
return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO";
|
||||
}
|
||||
|
||||
std::uint32_t FloatBits(float value) {
|
||||
return std::bit_cast<std::uint32_t>(value);
|
||||
}
|
||||
|
||||
bool SameBits(float lhs, float rhs) {
|
||||
return FloatBits(lhs) == FloatBits(rhs);
|
||||
}
|
||||
|
||||
bool IsQuietNanSentinel(float value) {
|
||||
return FloatBits(value) == kQuietNanBits;
|
||||
}
|
||||
|
||||
bool DrainGlErrors() {
|
||||
bool hadError = false;
|
||||
while (glGetError() != GL_NO_ERROR) hadError = true;
|
||||
return hadError;
|
||||
}
|
||||
|
||||
bool HasExtension(const char* wanted) {
|
||||
GLint extensionCount = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
||||
for (GLint i = 0; i < extensionCount; ++i) {
|
||||
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (extension != nullptr && std::string(extension) == wanted) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct CapabilityInfo {
|
||||
bool subgroupExtension = false;
|
||||
GLint subgroupSize = 0;
|
||||
GLint supportedStages = 0;
|
||||
GLint supportedFeatures = 0;
|
||||
GLint maxComputeStorageBlocks = 0;
|
||||
GLint maxStorageBindings = 0;
|
||||
GLint maxWorkGroupInvocations = 0;
|
||||
std::array<GLint, 3> maxWorkGroupSize{};
|
||||
bool queryHadError = false;
|
||||
|
||||
// iterationRP's source contract needs gl_NumSubgroups in [2, 32] for its 512
|
||||
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
||||
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
|
||||
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
|
||||
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
|
||||
// this probe deliberately does not model. The width only gates the domain;
|
||||
// lane placement and group counts still come from observed values alone.
|
||||
bool SubgroupWidthInSourceDomain() const {
|
||||
return subgroupSize >= 16 && subgroupSize <= 256;
|
||||
}
|
||||
|
||||
bool SupportsProbe() const {
|
||||
const auto stages = static_cast<GLbitfield>(supportedStages);
|
||||
const auto features = static_cast<GLbitfield>(supportedFeatures);
|
||||
return !queryHadError && subgroupExtension &&
|
||||
(stages & GL_COMPUTE_SHADER_BIT) != 0 &&
|
||||
(features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) ==
|
||||
(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) &&
|
||||
SubgroupWidthInSourceDomain() &&
|
||||
maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 &&
|
||||
maxWorkGroupInvocations >= static_cast<GLint>(kInvocationCount) && maxWorkGroupSize[0] >= 32 &&
|
||||
maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1;
|
||||
}
|
||||
|
||||
std::string MissingRequirements() const {
|
||||
std::vector<std::string> missing;
|
||||
const auto stages = static_cast<GLbitfield>(supportedStages);
|
||||
const auto features = static_cast<GLbitfield>(supportedFeatures);
|
||||
if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error");
|
||||
if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup");
|
||||
if ((stages & GL_COMPUTE_SHADER_BIT) == 0) {
|
||||
missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR");
|
||||
}
|
||||
const auto requiredFeatures =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
||||
if ((features & requiredFeatures) != requiredFeatures) {
|
||||
missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR");
|
||||
}
|
||||
if (!SubgroupWidthInSourceDomain()) {
|
||||
missing.emplace_back(
|
||||
"GL_SUBGROUP_SIZE_KHR in [16, 256] (iterationRP's source contract needs "
|
||||
"gl_NumSubgroups in [2, 32] for 512 invocations; width " +
|
||||
std::to_string(subgroupSize) + " is outside the fixture's domain)");
|
||||
}
|
||||
if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) {
|
||||
missing.emplace_back("two compute SSBO bindings");
|
||||
}
|
||||
if (maxWorkGroupInvocations < static_cast<GLint>(kInvocationCount) || maxWorkGroupSize[0] < 32 ||
|
||||
maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) {
|
||||
missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup");
|
||||
}
|
||||
|
||||
std::ostringstream message;
|
||||
for (std::size_t i = 0; i < missing.size(); ++i) {
|
||||
if (i != 0) message << ", ";
|
||||
message << missing[i];
|
||||
}
|
||||
return message.str();
|
||||
}
|
||||
};
|
||||
|
||||
CapabilityInfo QueryCapabilities() {
|
||||
CapabilityInfo info;
|
||||
DrainGlErrors();
|
||||
info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
|
||||
glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize);
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages);
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures);
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks);
|
||||
glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings);
|
||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations);
|
||||
for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) {
|
||||
glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]);
|
||||
}
|
||||
info.queryHadError = DrainGlErrors();
|
||||
return info;
|
||||
}
|
||||
|
||||
void PrintMetadata(const CapabilityInfo& info, std::ostream& output) {
|
||||
output << "IterationRPFirstReductionScenario metadata: "
|
||||
<< "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize
|
||||
<< ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex
|
||||
<< static_cast<GLbitfield>(info.supportedStages)
|
||||
<< ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x"
|
||||
<< static_cast<GLbitfield>(info.supportedFeatures) << std::dec
|
||||
<< ", subgroupExtension=" << info.subgroupExtension
|
||||
<< ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks
|
||||
<< ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings
|
||||
<< ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations
|
||||
<< ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x'
|
||||
<< info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2]
|
||||
<< ", queryHadError=" << info.queryHadError << '\n';
|
||||
}
|
||||
|
||||
bool DumpRequested() {
|
||||
const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP");
|
||||
return value != nullptr && std::string(value) == "1";
|
||||
}
|
||||
|
||||
constexpr const char* kShaderPreamble = R"(#version 430 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 = 1) buffer SubgroupProbeOutput {
|
||||
uvec4 invocation[512];
|
||||
uvec4 subgroup[512];
|
||||
vec4 reduction[512];
|
||||
float finalAverage[512];
|
||||
float scanAfter[6][512];
|
||||
} outProbe;
|
||||
|
||||
shared vec2 prefixSumCache[32];
|
||||
)";
|
||||
|
||||
constexpr const char* kSampledInput = R"(
|
||||
uniform sampler2D colortex2;
|
||||
uniform vec2 pixelSize;
|
||||
)";
|
||||
|
||||
constexpr const char* kIndexedInput = R"(
|
||||
layout(std430, binding = 0) readonly buffer Input {
|
||||
float value[512];
|
||||
} inputData;
|
||||
)";
|
||||
|
||||
// Only the expression producing tileExposure differs between the two
|
||||
// tests. The remainder is the iterationRP first reduction, with stores
|
||||
// placed after its existing barriers to expose each handoff.
|
||||
constexpr const char* kSampledTileExposure = R"(
|
||||
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) *
|
||||
vec2(1.0 / 32.0, 1.0 / 16.0);
|
||||
vec2 sampleCoord = texCoord * (1.0 / 64.0);
|
||||
sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0;
|
||||
|
||||
float tileExposure = dot(
|
||||
textureLod(colortex2, sampleCoord, 0.0).rgb,
|
||||
vec3(0.2125, 0.7154, 0.0721));
|
||||
)";
|
||||
|
||||
constexpr const char* kIndexedTileExposure = R"(
|
||||
float tileExposure = inputData.value[gl_LocalInvocationIndex];
|
||||
)";
|
||||
|
||||
constexpr const char* kReductionBody = R"(
|
||||
vec2 sampleLuminance = vec2(tileExposure, 0.0);
|
||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
||||
float nativeInclusive = sampleLuminance.x;
|
||||
|
||||
// This is a uniform, safety-only branch: it leaves an invalid source
|
||||
// contract visible without indexing past the 32-entry cache or underflowing
|
||||
// loopLength - 1. It is deliberately a failure on the CPU, not a skip.
|
||||
bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u;
|
||||
if (!sourceDomain) {
|
||||
float qNaN = uintBitsToFloat(0x7fc00000u);
|
||||
uint localIndex = gl_LocalInvocationIndex;
|
||||
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
|
||||
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
|
||||
gl_SubgroupInvocationID);
|
||||
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN);
|
||||
outProbe.finalAverage[localIndex] = qNaN;
|
||||
for (uint stage = 0u; stage < 6u; ++stage)
|
||||
outProbe.scanAfter[stage][localIndex] = qNaN;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
barrier();
|
||||
|
||||
float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x;
|
||||
|
||||
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) {
|
||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
}
|
||||
barrier();
|
||||
outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x;
|
||||
}
|
||||
|
||||
float sourceMergedPrefix = sampleLuminance.x;
|
||||
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
prefixSumCache[0] = sampleLuminance / 512.0;
|
||||
barrier();
|
||||
|
||||
float avg = prefixSumCache[0].x;
|
||||
|
||||
uint localIndex = gl_LocalInvocationIndex;
|
||||
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
|
||||
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
|
||||
gl_SubgroupInvocationID);
|
||||
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix);
|
||||
outProbe.finalAverage[localIndex] = avg;
|
||||
}
|
||||
)";
|
||||
|
||||
std::string BuildProbeShader(InputMode mode) {
|
||||
std::string source = kShaderPreamble;
|
||||
source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput;
|
||||
source += "\nvoid main() {\n";
|
||||
source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure;
|
||||
source += kReductionBody;
|
||||
return source;
|
||||
}
|
||||
|
||||
std::string FormatFloat(float value) {
|
||||
std::ostringstream text;
|
||||
text << std::hexfloat << value;
|
||||
return text.str();
|
||||
}
|
||||
|
||||
struct ValidationResult {
|
||||
bool ok = true;
|
||||
std::string phase;
|
||||
std::string message;
|
||||
bool scanStageMismatch = false;
|
||||
int scanStage = -1;
|
||||
bool ownerEvaluated = false;
|
||||
bool index511IsSourceLastLaneWriter = false;
|
||||
bool index511IsHighestSubgroupMember = false;
|
||||
std::uint32_t highestObservedSubgroup = 0;
|
||||
};
|
||||
|
||||
ValidationResult Failure(std::string phase, std::string message) {
|
||||
ValidationResult result;
|
||||
result.ok = false;
|
||||
result.phase = std::move(phase);
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f;
|
||||
|
||||
float ExpectedInput(InputMode mode, std::uint32_t localIndex) {
|
||||
return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast<float>(localIndex + 1u);
|
||||
}
|
||||
|
||||
ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) {
|
||||
std::array<std::size_t, kInvocationCount> slotForLocal{};
|
||||
slotForLocal.fill(kNoSlot);
|
||||
|
||||
// 1. Record identity. Slots are only used to locate each reported
|
||||
// local index; all subgroup behavior below groups recorded IDs/lanes.
|
||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
||||
const std::uint32_t localIndex = output.invocation[slot].x;
|
||||
if (localIndex >= kInvocationCount) {
|
||||
std::ostringstream message;
|
||||
message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]";
|
||||
return Failure("record identity", message.str());
|
||||
}
|
||||
if (slotForLocal[localIndex] != kNoSlot) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex]
|
||||
<< " and " << slot;
|
||||
return Failure("record identity", message.str());
|
||||
}
|
||||
slotForLocal[localIndex] = slot;
|
||||
}
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
if (slotForLocal[localIndex] == kNoSlot) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " is missing from all 512 records";
|
||||
return Failure("record identity", message.str());
|
||||
}
|
||||
}
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
const std::size_t slot = slotForLocal[localIndex];
|
||||
const UVec4& invocation = output.invocation[slot];
|
||||
const std::uint32_t expectedX = static_cast<std::uint32_t>(localIndex % 32u);
|
||||
const std::uint32_t expectedY = static_cast<std::uint32_t>(localIndex / 32u);
|
||||
if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ','
|
||||
<< invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY
|
||||
<< ",0)";
|
||||
return Failure("record identity", message.str());
|
||||
}
|
||||
const float expectedInput = ExpectedInput(mode, static_cast<std::uint32_t>(localIndex));
|
||||
const float actualInput = output.reduction[slot].x;
|
||||
if (!SameBits(actualInput, expectedInput)) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected "
|
||||
<< FormatFloat(expectedInput);
|
||||
return Failure("input", message.str());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Observed topology. Do not derive lanes or subgroup membership
|
||||
// from local invocation indices: only the values the shader recorded
|
||||
// participate in grouping.
|
||||
const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y;
|
||||
if (reportedNumSubgroups == 0u) {
|
||||
return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0");
|
||||
}
|
||||
if (reportedNumSubgroups > kInvocationCount) {
|
||||
std::ostringstream message;
|
||||
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
|
||||
<< " exceeds the 512 recorded invocations, so at least one subgroup ID is missing";
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
std::vector<std::vector<std::size_t>> subgroupSlots(reportedNumSubgroups);
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
const std::size_t slot = slotForLocal[localIndex];
|
||||
const UVec4& subgroup = output.subgroup[slot];
|
||||
if (subgroup.x == 0u || subgroup.y == 0u) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x
|
||||
<< ", numSubgroups=" << subgroup.y;
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
if (subgroup.y != reportedNumSubgroups) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y
|
||||
<< ", while localIndex 0 reported " << reportedNumSubgroups;
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
if (subgroup.z >= reportedNumSubgroups) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z
|
||||
<< " outside [0, " << (reportedNumSubgroups - 1u) << ']';
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
if (subgroup.w >= subgroup.x) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " reported laneID=" << subgroup.w
|
||||
<< " outside its subgroupSize=" << subgroup.x;
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
subgroupSlots[subgroup.z].push_back(slot);
|
||||
}
|
||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
||||
if (subgroupSlots[subgroupID].empty()) {
|
||||
std::ostringstream message;
|
||||
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
|
||||
<< " but subgroupID " << subgroupID << " has no recorded members";
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
auto& members = subgroupSlots[subgroupID];
|
||||
std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) {
|
||||
return output.subgroup[lhs].w < output.subgroup[rhs].w;
|
||||
});
|
||||
for (std::size_t i = 1; i < members.size(); ++i) {
|
||||
if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) {
|
||||
std::ostringstream message;
|
||||
message << "subgroupID " << subgroupID << " contains duplicate laneID "
|
||||
<< output.subgroup[members[i]].w;
|
||||
return Failure("observed topology", message.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Native subgroup arithmetic, in the actual lane ordering emitted
|
||||
// by the driver. The fixture values and all partial sums are exactly
|
||||
// representable binary32 values, so compare representation, not epsilon.
|
||||
std::array<float, kInvocationCount> nativePrefix{};
|
||||
std::vector<float> nativeSubtotal(reportedNumSubgroups, 0.0f);
|
||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
||||
float inclusive = 0.0f;
|
||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
||||
const std::uint32_t localIndex = output.invocation[slot].x;
|
||||
inclusive += ExpectedInput(mode, localIndex);
|
||||
nativePrefix[slot] = inclusive;
|
||||
const float actualNative = output.reduction[slot].y;
|
||||
if (!SameBits(actualNative, inclusive)) {
|
||||
std::ostringstream message;
|
||||
message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w
|
||||
<< ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative)
|
||||
<< ", expected " << FormatFloat(inclusive);
|
||||
return Failure("native subgroup arithmetic", message.str());
|
||||
}
|
||||
}
|
||||
nativeSubtotal[subgroupID] = inclusive;
|
||||
}
|
||||
|
||||
// sourceDomain is the narrow source-side safety branch. It is checked
|
||||
// after native arithmetic so an unsupported source topology still
|
||||
// reports native subgroup behavior before failing explicitly.
|
||||
if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) {
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
const std::size_t slot = slotForLocal[localIndex];
|
||||
const Vec4& reduction = output.reduction[slot];
|
||||
if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) ||
|
||||
!IsQuietNanSentinel(output.finalAverage[slot])) {
|
||||
std::ostringstream message;
|
||||
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
|
||||
<< reportedNumSubgroups << "; localIndex " << localIndex
|
||||
<< " did not preserve its qNaN source-reduction sentinel";
|
||||
return Failure("source domain", message.str());
|
||||
}
|
||||
for (std::size_t stage = 0; stage < kScanStageCount; ++stage) {
|
||||
if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) {
|
||||
std::ostringstream message;
|
||||
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
|
||||
<< reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage
|
||||
<< " did not preserve its qNaN source-reduction sentinel";
|
||||
return Failure("source domain", message.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::ostringstream message;
|
||||
message << "iterationRP source reduction has no valid contract for observed gl_NumSubgroups="
|
||||
<< reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded";
|
||||
return Failure("source domain", message.str());
|
||||
}
|
||||
|
||||
// 4. iterationRP source writer and first shared-memory handoff.
|
||||
std::vector<std::size_t> sourceWriter(reportedNumSubgroups, kNoSlot);
|
||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
||||
std::size_t writerCount = 0;
|
||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
||||
const UVec4& subgroup = output.subgroup[slot];
|
||||
if (subgroup.w == subgroup.x - 1u) {
|
||||
sourceWriter[subgroupID] = slot;
|
||||
++writerCount;
|
||||
}
|
||||
}
|
||||
if (writerCount != 1u) {
|
||||
std::ostringstream message;
|
||||
message << "subgroupID " << subgroupID << " has " << writerCount
|
||||
<< " recorded lane(s) where laneID == subgroupSize - 1; iterationRP leaves that "
|
||||
"shared-cache entry unwritten";
|
||||
return Failure("source writer", message.str());
|
||||
}
|
||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
||||
const float actualRawSubtotal = output.reduction[slot].z;
|
||||
if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) {
|
||||
std::ostringstream message;
|
||||
message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x
|
||||
<< " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected "
|
||||
<< FormatFloat(nativeSubtotal[subgroupID]);
|
||||
return Failure("source raw subtotal", message.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Reproduce the source loop exactly, including the redundant final
|
||||
// scan iteration on power-of-two subgroup counts. Reads and writes in
|
||||
// one iteration target disjoint cache entries, so update the cache at
|
||||
// the CPU equivalent of the source barrier.
|
||||
std::array<float, kInvocationCount> mergedPrefix = nativePrefix;
|
||||
std::vector<float> cache = nativeSubtotal;
|
||||
std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u;
|
||||
loopLength +=
|
||||
static_cast<std::uint32_t>(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||
for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
||||
std::vector<float> cacheAfterStage = cache;
|
||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
||||
if ((subgroupID & (1u << scanStage)) == 0u) continue;
|
||||
const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u;
|
||||
const float sourcePrefix = cache[sourceCacheIndex];
|
||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
||||
mergedPrefix[slot] += sourcePrefix;
|
||||
}
|
||||
cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]];
|
||||
}
|
||||
cache.swap(cacheAfterStage);
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
const std::size_t slot = slotForLocal[localIndex];
|
||||
const float actualAfterStage = output.scanAfter[scanStage][slot];
|
||||
if (!SameBits(actualAfterStage, mergedPrefix[slot])) {
|
||||
std::ostringstream message;
|
||||
message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z
|
||||
<< ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex
|
||||
<< " scanAfter was " << FormatFloat(actualAfterStage) << ", expected "
|
||||
<< FormatFloat(mergedPrefix[slot]);
|
||||
ValidationResult result = Failure("source scan", message.str());
|
||||
result.scanStageMismatch = true;
|
||||
result.scanStage = static_cast<int>(scanStage);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
||||
const std::size_t slot = slotForLocal[localIndex];
|
||||
const float actualMergedPrefix = output.reduction[slot].w;
|
||||
if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) {
|
||||
std::ostringstream message;
|
||||
message << "localIndex " << localIndex << " sourceMergedPrefix was "
|
||||
<< FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]);
|
||||
return Failure("source scan", message.str());
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Final owner and average. The uniformity check is intentionally
|
||||
// separate from the source's topology contract at local index 511.
|
||||
const float firstAverage = output.finalAverage[slotForLocal[0]];
|
||||
for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) {
|
||||
const float actualAverage = output.finalAverage[slotForLocal[localIndex]];
|
||||
if (!SameBits(actualAverage, firstAverage)) {
|
||||
std::ostringstream message;
|
||||
message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage)
|
||||
<< ", localIndex " << localIndex << " has " << FormatFloat(actualAverage);
|
||||
return Failure("final average", message.str());
|
||||
}
|
||||
}
|
||||
|
||||
ValidationResult ownerResult;
|
||||
ownerResult.ownerEvaluated = true;
|
||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
||||
if (!subgroupSlots[subgroupID].empty()) {
|
||||
ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID);
|
||||
}
|
||||
}
|
||||
const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u];
|
||||
const UVec4& index511Subgroup = output.subgroup[index511Slot];
|
||||
ownerResult.index511IsSourceLastLaneWriter =
|
||||
index511Subgroup.w == index511Subgroup.x - 1u;
|
||||
ownerResult.index511IsHighestSubgroupMember =
|
||||
index511Subgroup.z == ownerResult.highestObservedSubgroup;
|
||||
if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) {
|
||||
std::ostringstream message;
|
||||
message << "iterationRP topology incompatibility: localIndex 511 is sourceLastLaneWriter="
|
||||
<< ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
|
||||
<< ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z
|
||||
<< ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')';
|
||||
ownerResult.ok = false;
|
||||
ownerResult.phase = "final average";
|
||||
ownerResult.message = message.str();
|
||||
return ownerResult;
|
||||
}
|
||||
|
||||
float total = 0.0f;
|
||||
for (const float subtotal : nativeSubtotal) total += subtotal;
|
||||
float sampledExpectedTotal = 0.0f;
|
||||
for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance;
|
||||
const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal;
|
||||
if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) {
|
||||
std::ostringstream message;
|
||||
message << "iterationRP source total was " << FormatFloat(mergedPrefix[index511Slot])
|
||||
<< " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal);
|
||||
ownerResult.ok = false;
|
||||
ownerResult.phase = "final average";
|
||||
ownerResult.message = message.str();
|
||||
return ownerResult;
|
||||
}
|
||||
|
||||
const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f;
|
||||
if (!SameBits(firstAverage, expectedAverage)) {
|
||||
std::ostringstream message;
|
||||
message << "finalAverage was " << FormatFloat(firstAverage) << ", expected "
|
||||
<< FormatFloat(expectedAverage);
|
||||
ownerResult.ok = false;
|
||||
ownerResult.phase = "final average";
|
||||
ownerResult.message = message.str();
|
||||
return ownerResult;
|
||||
}
|
||||
return ownerResult;
|
||||
}
|
||||
|
||||
void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation,
|
||||
bool includeScanStages) {
|
||||
PrintMetadata(capabilities, std::cout);
|
||||
if (validation.ok) {
|
||||
std::cout << "IterationRPFirstReductionScenario firstFailure=none\n";
|
||||
} else {
|
||||
std::cout << "IterationRPFirstReductionScenario firstFailure=" << validation.phase << ": "
|
||||
<< validation.message << '\n';
|
||||
}
|
||||
std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input,"
|
||||
"nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n";
|
||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
||||
const UVec4& invocation = output.invocation[slot];
|
||||
const UVec4& subgroup = output.subgroup[slot];
|
||||
const Vec4& reduction = output.reduction[slot];
|
||||
std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ','
|
||||
<< subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ','
|
||||
<< std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ','
|
||||
<< reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n';
|
||||
}
|
||||
if (includeScanStages) {
|
||||
std::cout << "scanStage,localIndex,scanAfter\n";
|
||||
for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) {
|
||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
||||
std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat
|
||||
<< output.scanAfter[scanStage][slot] << std::defaultfloat << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IterationRPFirstReductionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_capabilities = QueryCapabilities();
|
||||
// GL_SUBGROUP_SIZE_KHR gates only whether the fixture's source contract
|
||||
// can hold on this device (SubgroupWidthInSourceDomain); it is
|
||||
// deliberately never used to infer lane placement or an expected group
|
||||
// count - those come from observed values alone.
|
||||
PrintMetadata(m_capabilities, std::cout);
|
||||
RecordProperty("iterationrp_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize));
|
||||
if (!m_capabilities.SupportsProbe()) {
|
||||
GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer);
|
||||
if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
m_texture = 0;
|
||||
m_inputBuffer = 0;
|
||||
m_outputBuffer = 0;
|
||||
m_program = 0;
|
||||
}
|
||||
|
||||
GLuint CompileComputeProgram(const std::string& source, std::string* outError) {
|
||||
const char* text = source.c_str();
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
*outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0";
|
||||
return 0;
|
||||
}
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[8192] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
*outError = std::string("the subgroup probe compute shader did not compile: ") + log;
|
||||
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_FALSE) {
|
||||
char log[8192] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
*outError = std::string("the subgroup probe compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) {
|
||||
m_program = CompileComputeProgram(BuildProbeShader(mode), outError);
|
||||
if (m_program == 0) return false;
|
||||
|
||||
ProbeOutput poison{};
|
||||
std::memset(&poison, 0xa5, sizeof(poison));
|
||||
glGenBuffers(1, &m_outputBuffer);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer);
|
||||
|
||||
if (mode == InputMode::IndexedSsbo) {
|
||||
std::array<float, kInvocationCount> values{};
|
||||
for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast<float>(i + 1u);
|
||||
glGenBuffers(1, &m_inputBuffer);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer);
|
||||
} else {
|
||||
constexpr std::array<float, 4> kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, m_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, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data());
|
||||
}
|
||||
|
||||
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
|
||||
std::ostringstream message;
|
||||
message << "subgroup probe resource setup left " << GLErrorName(error);
|
||||
*outError = message.str();
|
||||
return false;
|
||||
}
|
||||
|
||||
glUseProgram(m_program);
|
||||
if (mode == InputMode::SampledRgba32f) {
|
||||
const GLint sampler = glGetUniformLocation(m_program, "colortex2");
|
||||
const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize");
|
||||
if (sampler == -1 || pixelSize == -1) {
|
||||
*outError = "the sampled probe uniforms were optimized away or not reflected";
|
||||
return false;
|
||||
}
|
||||
glUniform1i(sampler, 3);
|
||||
glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f);
|
||||
}
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output);
|
||||
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
|
||||
std::ostringstream message;
|
||||
message << "subgroup probe dispatch/readback left " << GLErrorName(error);
|
||||
*outError = message.str();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RunAndValidate(InputMode mode) {
|
||||
ProbeOutput output{};
|
||||
std::string error;
|
||||
ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error;
|
||||
|
||||
const ValidationResult validation = ValidateProbe(output, mode);
|
||||
if (validation.ownerEvaluated) {
|
||||
RecordProperty("iterationrp_index511_source_last_lane_writer",
|
||||
validation.index511IsSourceLastLaneWriter ? "true" : "false");
|
||||
RecordProperty("iterationrp_index511_highest_subgroup_member",
|
||||
validation.index511IsHighestSubgroupMember ? "true" : "false");
|
||||
RecordProperty("iterationrp_highest_observed_subgroup",
|
||||
std::to_string(validation.highestObservedSubgroup));
|
||||
std::cout << "IterationRPFirstReductionScenario owner: localIndex511 sourceLastLaneWriter="
|
||||
<< validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
|
||||
<< validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup="
|
||||
<< validation.highestObservedSubgroup << '\n';
|
||||
}
|
||||
if (!validation.ok || DumpRequested()) {
|
||||
DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested());
|
||||
}
|
||||
EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message;
|
||||
}
|
||||
|
||||
CapabilityInfo m_capabilities;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_inputBuffer = 0;
|
||||
GLuint m_outputBuffer = 0;
|
||||
GLuint m_texture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(IterationRPFirstReductionScenario, SampledRgba32fFirstAverage) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunAndValidate(InputMode::SampledRgba32f);
|
||||
}
|
||||
|
||||
TEST_F(IterationRPFirstReductionScenario, IndexedInputTopologyAndReduction) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunAndValidate(InputMode::IndexedSsbo);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,379 @@
|
||||
// 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
|
||||
@@ -0,0 +1,302 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPScratchFixScenario.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
|
||||
//
|
||||
// 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
|
||||
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
|
||||
// width) on the modules that match the pack's reduction fingerprint.
|
||||
//
|
||||
// This scenario replays the fixture's reduction shape verbatim - the same 32-entry
|
||||
// declaration, the same last-lane handoff, the same findMSB combine loop, and NO
|
||||
// domain guard - and asserts only the width-independent result: the workgroup total.
|
||||
// The inputs are small integers, so the fp32 sum is exact under any lane order and any
|
||||
// association; a correct run produces the exact constant on a 4-lane device and a
|
||||
// 128-lane device alike. Without the patch, a sub-16-lane device indexes the
|
||||
// 32-entry array out of bounds - on lavapipe that is literal heap corruption - and
|
||||
// this scenario is the regression test that keeps the patch working, and it runs on every device that
|
||||
// has basic+arithmetic compute subgroups (unlike IterationRPFirstReductionScenario,
|
||||
// which probes the UNREPAIRED source contract and must skip outside [16, 256]).
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr 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
|
||||
#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 total;
|
||||
uint numSubgroups;
|
||||
uint maxSubgroupId;
|
||||
} outputData;
|
||||
|
||||
shared vec2 prefixSumCache[32];
|
||||
|
||||
void main() {
|
||||
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex), 0.0);
|
||||
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 scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
||||
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
|
||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
|
||||
if (gl_LocalInvocationIndex == 511u) {
|
||||
outputData.total = sampleLuminance.x;
|
||||
outputData.numSubgroups = gl_NumSubgroups;
|
||||
}
|
||||
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 {
|
||||
float total = -1.0f;
|
||||
std::uint32_t numSubgroups = 0;
|
||||
std::uint32_t maxSubgroupId = 0;
|
||||
};
|
||||
|
||||
bool HasExtension(const char* wanted) {
|
||||
GLint extensionCount = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
||||
for (GLint i = 0; i < extensionCount; ++i) {
|
||||
const auto* extension =
|
||||
reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (extension != nullptr && std::string(extension) == wanted) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class IterationRPScratchFixScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
GLint stages = 0;
|
||||
GLint features = 0;
|
||||
GLint invocations = 0;
|
||||
const bool subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
|
||||
if (subgroupExtension) {
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
|
||||
}
|
||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
|
||||
const GLbitfield requiredFeatures =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
||||
if (!subgroupExtension || (static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
|
||||
(static_cast<GLbitfield>(features) & requiredFeatures) != requiredFeatures ||
|
||||
invocations < static_cast<GLint>(kInvocationCount)) {
|
||||
GTEST_SKIP() << "needs GL_KHR_shader_subgroup basic+arithmetic in compute and a "
|
||||
"512-invocation workgroup";
|
||||
}
|
||||
|
||||
m_maxInvocations = static_cast<std::uint32_t>(invocations);
|
||||
|
||||
glGenBuffers(1, &m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
// maxSubgroupId starts at zero HOST-side: the word is touched only by
|
||||
// atomicMax during the dispatch, since a plain shader-side zeroing store
|
||||
// would race the other invocations' atomics (barrier() orders shared
|
||||
// memory, not SSBO stores).
|
||||
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(OutputBlock), &poison, GL_DYNAMIC_READ);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
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{};
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
OutputBlock block{};
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &block);
|
||||
return block;
|
||||
}
|
||||
|
||||
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);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// The topology diagnostics catch the failure modes by name before the sum does:
|
||||
// an out-of-bounds handoff corrupts the total, a wrong gl_NumSubgroups breaks
|
||||
// the combine loop's length.
|
||||
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 511 never reached its store";
|
||||
EXPECT_GE(block.numSubgroups, 1u);
|
||||
EXPECT_LE(block.numSubgroups, kInvocationCount);
|
||||
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
|
||||
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
|
||||
"DeriveNumSubgroupsPass exists to repair";
|
||||
|
||||
// Integer-valued fp32 inputs: the workgroup total is exact under any subgroup
|
||||
// width, lane order, and association. This is the value iterationRP's exposure
|
||||
// average is built from; without FixIterationRPSubgroupScratchPass an 8-lane
|
||||
// device writes prefixSumCache[32..63] out of bounds and this comparison fails.
|
||||
EXPECT_EQ(block.total, kExpectedTotal)
|
||||
<< "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
|
||||
@@ -428,15 +428,15 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
const float near = static_cast<float>(i) / 16.0f;
|
||||
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||
const float nearDepth = static_cast<float>(i) / 16.0f;
|
||||
const float farDepth = 1.0f - static_cast<float>(i) / 16.0f;
|
||||
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||
// from every other index by at least 1/16.
|
||||
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||
EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f)
|
||||
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], farDepth, 1.0e-3f)
|
||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
// 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
|
||||
@@ -250,6 +250,34 @@ namespace MobileGL::MG_State::GLState {
|
||||
NotifyContentWrite(atOffset, data.size);
|
||||
}
|
||||
|
||||
void BufferObject::FillSubData(DataPtr pattern, SizeT atOffset, SizeT size) {
|
||||
MOBILEGL_ASSERT(pattern.data != nullptr && pattern.size > 0,
|
||||
"FillSubData requires a non-empty pattern.");
|
||||
MOBILEGL_ASSERT(size % pattern.size == 0,
|
||||
"FillSubData size (%zu) must be a multiple of pattern size (%zu).", size, pattern.size);
|
||||
MOBILEGL_ASSERT(atOffset <= m_size && size <= m_size - atOffset,
|
||||
"FillSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
|
||||
m_size);
|
||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
||||
"Cannot fill data while buffer is non-persistently mapped.");
|
||||
if (size == 0) return;
|
||||
|
||||
// A clear is ordered after all earlier GPU writes. Partial clears additionally need the
|
||||
// retained shadow bytes; whole-store clears need the same synchronization before writing
|
||||
// an adopted persistent mapping that the GPU may still be accessing.
|
||||
SyncGpuWrites();
|
||||
|
||||
Uint8* dst = m_resource.Bytes() + atOffset;
|
||||
if (pattern.size == 1) {
|
||||
Memset(dst, *static_cast<const Uint8*>(pattern.data), size);
|
||||
} else {
|
||||
for (SizeT at = 0; at < size; at += pattern.size) {
|
||||
Memcpy(dst + at, pattern.data, pattern.size);
|
||||
}
|
||||
}
|
||||
NotifyContentWrite(atOffset, size);
|
||||
}
|
||||
|
||||
void BufferObject::DownloadSubData(void* dst, SizeT atOffset, SizeT size) const {
|
||||
MOBILEGL_ASSERT(atOffset + size <= m_size,
|
||||
"DownloadSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
|
||||
|
||||
@@ -132,6 +132,9 @@ namespace MobileGL {
|
||||
|
||||
void UploadData(DataPtr data, SizeT atOffset);
|
||||
void UploadSubData(DataPtr data, SizeT atOffset);
|
||||
// Repeats one already-converted element through [atOffset, atOffset + size) and
|
||||
// publishes the range as one content mutation.
|
||||
void FillSubData(DataPtr pattern, SizeT atOffset, SizeT size);
|
||||
// Reads `size` bytes from the CPU shadow at `atOffset` into `dst` (glGetBufferSubData).
|
||||
// The shadow reflects CPU writes (BufferData/SubData/maps) and backend write-backs, but not
|
||||
// arbitrary GPU-side writes.
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace MobileGL::MG_State {
|
||||
return m_compileEnv;
|
||||
}
|
||||
|
||||
void GLContext::InvalidateCompileEnv() {
|
||||
m_compileEnv.reset();
|
||||
m_compileEnvBackend = nullptr;
|
||||
}
|
||||
|
||||
// Error
|
||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
// Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
|
||||
@@ -641,9 +646,17 @@ namespace MobileGL::MG_State {
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
for (const auto& shader : stageProgram->GetAttachedShaders()) {
|
||||
if (!shader || static_cast<SizeT>(shader->GetShaderStage()) != stage) continue;
|
||||
composite->AttachShader(shader);
|
||||
// The stage program contributes the shaders its LAST LINK consumed, never
|
||||
// its live attach list: per GL 4.6 7.3/7.4 a pipeline stage executes the
|
||||
// stage program as last linked - glAttachShader and glCompileShader take
|
||||
// effect only at the program's next link - and neither of those moves the
|
||||
// link version this cache keys on, so reading live state here would let a
|
||||
// post-link attach or recompile leak into the composite while the signature
|
||||
// still hits. The pinned (source, node) makes the composite's Link()
|
||||
// consume the very inputs that link consumed.
|
||||
for (const auto& ref : stageProgram->GetLinkedShaderSnapshot()) {
|
||||
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
|
||||
composite->AttachShaderWithPinnedLinkInput(ref);
|
||||
anyStage = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,7 @@ 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;
|
||||
@@ -342,8 +343,30 @@ 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
|
||||
@@ -413,9 +436,12 @@ namespace MobileGL {
|
||||
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
|
||||
// so there is no backend to query yet. Re-captured whenever the active backend
|
||||
// object changes, which also rolls the fingerprint and therefore invalidates
|
||||
// every P0b preprocess memo keyed against the old one.
|
||||
// every P0b preprocess memo keyed against the old one. A backend whose dynamic
|
||||
// capabilities become available without changing object identity must call
|
||||
// InvalidateCompileEnv() after publishing them.
|
||||
// GL thread only.
|
||||
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
|
||||
void InvalidateCompileEnv();
|
||||
|
||||
private:
|
||||
// State Components
|
||||
@@ -436,6 +462,9 @@ 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 {
|
||||
|
||||
@@ -9,7 +9,18 @@
|
||||
#include "FramebufferObject.h"
|
||||
#include "MG_Util/Types.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
|
||||
// Atomic for the same reason as the VAO counter: it costs nothing, and a duplicate
|
||||
// id would resurrect exactly the ABA this id exists to kill.
|
||||
static std::atomic<Uint64> s_nextFramebufferLifetimeId{1};
|
||||
|
||||
Uint64 FramebufferObject::AllocateLifetimeId() {
|
||||
return s_nextFramebufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// FramebufferAttachmentObject
|
||||
FramebufferAttachmentObject::FramebufferAttachmentObject(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget textureUploadTarget, Int level,
|
||||
|
||||
@@ -148,13 +148,25 @@ namespace MobileGL {
|
||||
|
||||
Uint16 GetObjectVersion() const { return m_objectVersion; }
|
||||
|
||||
// Globally-unique, never-reused id for THIS object's lifetime - the same
|
||||
// contract as VertexArrayObject::GetLifetimeId(), and needed for the same
|
||||
// reason: neither the GL name nor the heap address can tell a
|
||||
// deleted-and-recreated framebuffer from the original, and m_objectVersion
|
||||
// starts at 0 for every new object, so a backend memo keyed on
|
||||
// (pointer, version) alone would silently inherit the dead object's entry
|
||||
// (see VkRenderPassManager's per-draw fast-path memo).
|
||||
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
||||
|
||||
Uint GetExternalIndex() const;
|
||||
Bool IsDefaultFramebuffer() const { return m_externalIndex == 0; }
|
||||
|
||||
private:
|
||||
static Uint64 AllocateLifetimeId();
|
||||
|
||||
void BumpAttachmentVersion(FramebufferAttachmentType type);
|
||||
|
||||
const Uint m_externalIndex = 0;
|
||||
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
||||
FramebufferAttachmentObjectArray m_attachmentObjects;
|
||||
FramebufferAttachmentVersionArray m_attachmentVersions;
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint externalIndex = 0; // logs only
|
||||
Vector<LinkShaderInput> shaders; // already stage-sorted
|
||||
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||
// Startup configuration copied with the task, never read from worker code.
|
||||
Bool enableSpirvValidation = false;
|
||||
// The four "takes effect at the next link" request maps. Snapshotted rather than
|
||||
// referenced, which is precisely what makes glBindAttribLocation and friends
|
||||
// legal to call over a pending link without cancelling it: the pending link keeps
|
||||
|
||||
@@ -393,6 +393,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProgramObject::AttachShaderWithPinnedLinkInput(const LinkedShaderRef& ref) {
|
||||
if (!AttachShader(ref.shader)) {
|
||||
return false;
|
||||
}
|
||||
m_pinnedLinkInputs[ref.shader.get()] = ref;
|
||||
return true;
|
||||
}
|
||||
|
||||
SizeT ProgramObject::DetachShader(const SharedPtr<ShaderObject>& shader) {
|
||||
MGLOG_D("DetachShader called for shader %p from ProgramObject %u", shader.get(), m_externalIndex);
|
||||
if (!ShaderIsAttached(shader)) {
|
||||
@@ -475,6 +483,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
AddDefaultFragmentShaderIfMissing();
|
||||
}
|
||||
if (m_shaders.empty()) {
|
||||
// This IS the last link now, and it consumed nothing.
|
||||
m_linkedShaderSnapshot.clear();
|
||||
m_artifacts.infoLog = "No shader objects are attached to program.";
|
||||
MGLOG_E("ProgramObject %u: Link failed - no shader objects attached.", m_externalIndex);
|
||||
return;
|
||||
@@ -494,6 +504,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto task = MakeShared<ProgramLinkTask>();
|
||||
task->in.externalIndex = m_externalIndex;
|
||||
task->in.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
||||
task->in.enableSpirvValidation = MG_Config::Features.EnableSpirvValidation;
|
||||
task->in.explicitAttribLocations = m_explicitAttribLocations;
|
||||
task->in.explicitFragDataLocation = m_explicitFragDataLocation;
|
||||
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
|
||||
@@ -504,8 +515,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<SharedPtr<ShaderCompileTask>> deps;
|
||||
deps.reserve(m_shaders.size());
|
||||
task->in.shaders.reserve(m_shaders.size());
|
||||
m_linkedShaderSnapshot.clear();
|
||||
m_linkedShaderSnapshot.reserve(m_shaders.size());
|
||||
for (const auto& shader : m_shaders) {
|
||||
const SharedPtr<ShaderCompileTask>& node = shader->CompiledNodeForLink();
|
||||
// A pipeline composite pins the (source, node) each stage program's LAST link
|
||||
// consumed (AttachShaderWithPinnedLinkInput); an ordinary program takes the
|
||||
// shader's current ones. Without the pin a post-link recompile would leak a
|
||||
// shader the stage program never linked into the composite.
|
||||
SharedPtr<const String> sourcePtr = shader->GetShaderSourcePtr();
|
||||
SharedPtr<ShaderCompileTask> node = shader->CompiledNodeForLink();
|
||||
if (const auto pinned = m_pinnedLinkInputs.find(shader.get()); pinned != m_pinnedLinkInputs.end()) {
|
||||
sourcePtr = pinned->second.source;
|
||||
node = pinned->second.node;
|
||||
}
|
||||
if (node) {
|
||||
// This link is now an observer of that node's result, and the ShaderObject is
|
||||
// no longer the only route to it: without the marker, the ordinary
|
||||
@@ -514,7 +536,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
node->MarkLinkReferenced();
|
||||
if (!node->IsTerminal()) deps.push_back(node);
|
||||
}
|
||||
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
||||
task->in.shaders.push_back({shader->GetShaderStage(), sourcePtr, node});
|
||||
// What "as last linked" will mean for this program from now on - the pipeline
|
||||
// composite cache rebuilds from exactly this set (GetProgramForDraw).
|
||||
m_linkedShaderSnapshot.push_back({shader, sourcePtr, node});
|
||||
}
|
||||
|
||||
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
||||
|
||||
@@ -60,6 +60,26 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
|
||||
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
|
||||
|
||||
// One shader exactly as this program's last Link() consumed it: the object, the
|
||||
// source snapshot, and the compile node taken at that link's enqueue. GL 4.6 7.3/7.4
|
||||
// makes this triple - not the live attach list, not the shader's current compile -
|
||||
// what a program pipeline stage executes ("as last linked"): glAttachShader and
|
||||
// glCompileShader take effect only at the program's next link, yet neither moves
|
||||
// m_linkVersion, so anything keyed on the link generation must consume this
|
||||
// snapshot rather than re-read the live state.
|
||||
struct LinkedShaderRef {
|
||||
SharedPtr<ShaderObject> shader;
|
||||
SharedPtr<const String> source;
|
||||
SharedPtr<ShaderCompileTask> node;
|
||||
};
|
||||
// The last link's full input set; empty when this program has never linked (or its
|
||||
// last link had no shaders attached). GL-thread-owned, rebuilt in Link()'s prologue.
|
||||
const Vector<LinkedShaderRef>& GetLinkedShaderSnapshot() const { return m_linkedShaderSnapshot; }
|
||||
// Pipeline-composite attach: AttachShader plus a pin that makes THIS program's
|
||||
// Link() consume ref's (source, node) instead of the shader's current ones, so a
|
||||
// post-link recompile of the stage program's shader cannot leak into the composite.
|
||||
bool AttachShaderWithPinnedLinkInput(const LinkedShaderRef& ref);
|
||||
const String& GetInfoLog() const { return Artifacts().infoLog; }
|
||||
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
|
||||
// is the only place a caller can read it from once the shader name is gone.
|
||||
@@ -786,6 +806,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// order - and the name is the only coordinate all three agree on. Absent from the map
|
||||
// means "never rebound", and the shader's declared binding still stands.
|
||||
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
|
||||
// Equality bail-out like SetUniformBlockBinding's: the pipeline composite
|
||||
// mirror replays every override each draw, and without this every replay
|
||||
// would churn m_blockBindingVersion and rebuild whatever keys on it.
|
||||
const auto it = Artifacts().shaderStorageBlockBinding.find(blockName);
|
||||
if (it != Artifacts().shaderStorageBlockBinding.end() && it->second == static_cast<Int>(binding)) {
|
||||
return;
|
||||
}
|
||||
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
|
||||
// Deliberately NOT m_backendStateVersion: Espryt's entry point never forces a
|
||||
// program build off this, and bumping that version would start doing so. The
|
||||
@@ -819,6 +846,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
// backend asks this exactly where it used to ask GetLinkStatus(), i.e. right before
|
||||
// it builds or draws with the program.
|
||||
Bool GetSpirvStatus() const { return Spirv().spirvStatus; }
|
||||
// Copied from the link task that generated this program's SPIR-V. Backends use it for
|
||||
// their final transforms, which must honor the same diagnostic setting as phase B.
|
||||
Bool GetSpirvValidationEnabled() const { return Spirv().enableSpirvValidation; }
|
||||
|
||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||
// lists no typed getter above exposes: the GL program-interface query layer
|
||||
@@ -985,6 +1015,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
@@ -1219,6 +1250,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
|
||||
Vector<SharedPtr<ShaderObject>> m_shaders;
|
||||
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
|
||||
// See GetLinkedShaderSnapshot. Holding the SharedPtrs here is deliberate: the
|
||||
// "as last linked" set must survive detach-and-delete of its shaders (the
|
||||
// glCreateShaderProgramv shape) until the next link replaces it.
|
||||
Vector<LinkedShaderRef> m_linkedShaderSnapshot;
|
||||
// See AttachShaderWithPinnedLinkInput. Populated only on pipeline composites,
|
||||
// which never detach, so entries need no removal path. GL-thread-owned.
|
||||
UnorderedMap<const ShaderObject*, LinkedShaderRef> m_pinnedLinkInputs;
|
||||
|
||||
// Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation,
|
||||
// glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer
|
||||
|
||||
@@ -102,7 +102,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
||||
GenerateSpirv(handoff, externalIndex);
|
||||
const Bool deferOutputValidationForDirectVulkan =
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
|
||||
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
|
||||
artifacts.enableSpirvValidation = enableSpirvValidation;
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
||||
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
||||
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
||||
@@ -137,7 +141,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.generatedSpirv.size());
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -169,7 +175,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
if (!success) {
|
||||
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
||||
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
||||
|
||||
@@ -65,7 +65,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
private:
|
||||
void RunBody() override;
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
|
||||
@@ -140,6 +140,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void ShaderObject::Compile() {
|
||||
// The compile-environment snapshot is taken HERE, on the GL thread, and handed to
|
||||
// the job. Everything the pipeline needs to know about the device comes through it,
|
||||
// never through pActiveBackendObject - that is what makes the body movable.
|
||||
// Hoisted above the memo check because the memo must be env-disciplined too (below).
|
||||
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env =
|
||||
MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
||||
|
||||
// P0b layer 1, as a tri-state: the memo is "the node in m_compiled was built from
|
||||
// the string m_source still points at". SetShaderSource only swaps that pointer when
|
||||
// the text actually differs, so this is a pointer compare, and it covers Pending as
|
||||
@@ -152,7 +159,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
// ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed
|
||||
// the next link a fresh parse, the no-op hands it a fresh re-parse of the identical
|
||||
// source instead. Same result, one parse either way.
|
||||
if (HasMemoizedCompile()) return;
|
||||
//
|
||||
// The environment joins the check (ShaderSourceKey.h's memo-hazard rule: a memo
|
||||
// must never be handed back under an environment other than the one it was
|
||||
// computed against). Layers 2 and 3 key on the fingerprint, but this memo sits
|
||||
// ABOVE both, so without this compare a node computed against a dead environment
|
||||
// - e.g. a compute shader rejected against the pre-capability fallback limits -
|
||||
// would keep answering forever while a fresh object with byte-identical source
|
||||
// compiles fine. The fingerprint is a content hash, so a republish of identical
|
||||
// capabilities still hits.
|
||||
if (HasMemoizedCompile() && m_compiled->env != nullptr && m_compiled->env->fingerprint == env->fingerprint) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Two reasons to stay on this thread, one rule. Without the async flag the whole
|
||||
// path must be byte-identical to the synchronous implementation, and a cache-less
|
||||
@@ -168,12 +186,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// glMaxShaderCompilerThreadsKHR(0) and a flag-off build both bypass sharing exactly
|
||||
// as they bypass the pool, and their behaviour stays byte-identical to pre-stage-6.
|
||||
const Bool runOnPool = m_preprocessCache && MG_Util::Async::AsyncShaderCompileActive();
|
||||
|
||||
// The compile-environment snapshot is taken HERE, on the GL thread, and handed to
|
||||
// the job. Everything the pipeline needs to know about the device comes through it,
|
||||
// never through pActiveBackendObject - that is what makes the body movable.
|
||||
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env =
|
||||
MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
||||
const Uint64 sourceHash = ShaderPreprocessCache::HashSource(*m_source);
|
||||
|
||||
// ---- P1 stage 6: adopt an equivalent compile instead of enqueueing a duplicate ----
|
||||
|
||||
@@ -48,6 +48,7 @@ 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;
|
||||
@@ -79,6 +80,9 @@ 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) {
|
||||
@@ -110,6 +114,16 @@ 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;
|
||||
|
||||
@@ -120,6 +134,7 @@ 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,6 +96,18 @@ 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).
|
||||
@@ -115,6 +127,7 @@ 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,6 +111,16 @@ 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,6 +373,18 @@ 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,6 +220,15 @@ 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
|
||||
@@ -286,6 +295,9 @@ 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,6 +96,18 @@ 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,6 +39,10 @@ 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;
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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,5 +16,22 @@ 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,6 +21,7 @@ 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 {
|
||||
@@ -550,3 +551,55 @@ 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,6 +34,17 @@ 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;
|
||||
@@ -111,7 +122,30 @@ namespace {
|
||||
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
||||
switch (pname) {
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxVertexSsboBlocks;
|
||||
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;
|
||||
break;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxVertexImageUniforms;
|
||||
@@ -400,6 +434,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -527,6 +565,83 @@ 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();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GetProcAddress.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -599,6 +600,117 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
|
||||
EXPECT_EQ(actual, (Vector<Uint32>{0, pattern, pattern, pattern, 0}));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
|
||||
Vector<Uint8> initial(32, 0x7F);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(
|
||||
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const GLbyte zero = 0;
|
||||
const auto clear = reinterpret_cast<PFNGLCLEARBUFFERSUBDATAPROC>(
|
||||
MobileGL::MG_Impl::GetProcAddress("glClearBufferSubData"));
|
||||
ASSERT_NE(clear, nullptr);
|
||||
clear(GL_SHADER_STORAGE_BUFFER, GL_R8, 4, 24, GL_RED, GL_BYTE, &zero);
|
||||
|
||||
Vector<Uint8> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, (Vector<Uint8>{0x7F, 0x7F, 0x7F, 0x7F,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x7F, 0x7F, 0x7F, 0x7F}));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferSubDataInitializesCompleteIrisStaticSsbo) {
|
||||
constexpr SizeT irisStaticSsboSize = 5'000'192;
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
|
||||
Vector<Uint8> initial(irisStaticSsboSize, 0x7F);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(
|
||||
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const GLbyte zero = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
|
||||
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, irisStaticSsboSize, GL_RED, GL_BYTE, &zero);
|
||||
|
||||
Vector<Uint8> actual(irisStaticSsboSize);
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, Vector<Uint8>(irisStaticSsboSize, 0));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferDataConvertsOneClientPixelBeforeRepeatingIt) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
Vector<Uint32> initial(4, 0u);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size() * sizeof(Uint32), initial.data(),
|
||||
GL_STATIC_DRAW);
|
||||
const Uint8 value = 0xAB;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, &value);
|
||||
|
||||
Vector<Uint32> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size() * sizeof(Uint32));
|
||||
EXPECT_EQ(actual, Vector<Uint32>(initial.size(), value));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferSubDataRejectsUnboundTarget) {
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
const GLbyte zero = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
|
||||
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, 1, GL_RED, GL_BYTE, &zero);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferDataRejectsInvalidPixelFormatTypePairs) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
const Vector<Uint8> initial{0x7F, 0x7F};
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const Uint16 packed = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, &packed);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
Vector<Uint8> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, initial);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
|
||||
// GL 4.6 core 6.5: glBufferSubData fails only when the written range OVERLAPS the mapped range.
|
||||
|
||||
@@ -78,6 +78,7 @@ add_subdirectory(Query)
|
||||
add_subdirectory(Pipeline)
|
||||
add_subdirectory(ShaderTranspiler)
|
||||
add_subdirectory(Util)
|
||||
add_subdirectory(SelfTest)
|
||||
# The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the
|
||||
# DirectVulkan suite below this one needs no device and always builds.
|
||||
add_subdirectory(Backend/DirectGLES)
|
||||
|
||||
@@ -2073,153 +2073,6 @@ void main() {
|
||||
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
|
||||
}
|
||||
|
||||
namespace {
|
||||
String MakeLinearSubgroupPrefixScanShader() {
|
||||
return R"(#version 460 core
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : enable
|
||||
layout(local_size_x = 1024) in;
|
||||
shared float prefixSumCache[64];
|
||||
|
||||
layout(std430, binding = 0) writeonly buffer OutputBuffer {
|
||||
float outputValues[];
|
||||
};
|
||||
|
||||
void main() {
|
||||
float importance = 1.0f;
|
||||
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 i = 0; i < loopLength; i++) {
|
||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
||||
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
|
||||
barrier();
|
||||
float sum = prefixSumCache[0];
|
||||
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
|
||||
outputValues[gl_GlobalInvocationID.x] = warp;
|
||||
}
|
||||
)";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = MakeLinearSubgroupPrefixScanShader();
|
||||
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
|
||||
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
|
||||
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
|
||||
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
|
||||
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
|
||||
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
|
||||
|
||||
const String onceRewritten = source;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
EXPECT_EQ(source, onceRewritten);
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
ASSERT_TRUE(programResult) << programResult.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
||||
ASSERT_EQ(binaryResult->size(), 1u);
|
||||
|
||||
String validationDiagnostics;
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||
validationDiagnostics += message;
|
||||
validationDiagnostics += '\n';
|
||||
});
|
||||
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
|
||||
|
||||
String spirvText;
|
||||
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
|
||||
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String original = MakeLinearSubgroupPrefixScanShader();
|
||||
for (const auto& [stage, subgroupSize] :
|
||||
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
|
||||
std::pair{ShaderStage::Compute, Uint32{96}}}) {
|
||||
String source = original;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
|
||||
EXPECT_EQ(source, original);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const auto expectUnchanged = [](String source) {
|
||||
const String original = source;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
EXPECT_EQ(source, original);
|
||||
};
|
||||
|
||||
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
|
||||
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
|
||||
"local_size_x = 512");
|
||||
expectUnchanged(std::move(wrongLocalSize));
|
||||
|
||||
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
|
||||
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
|
||||
expectUnchanged(std::move(cacheHasAnotherUse));
|
||||
|
||||
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
|
||||
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
|
||||
expectUnchanged(std::move(extraSubgroupBuiltin));
|
||||
|
||||
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
|
||||
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
|
||||
expectUnchanged(std::move(alteredBarrier));
|
||||
|
||||
String nestedScan = MakeLinearSubgroupPrefixScanShader();
|
||||
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
|
||||
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
|
||||
ASSERT_NE(consumerEnd, String::npos);
|
||||
nestedScan.insert(consumerEnd + 1, "\n }");
|
||||
expectUnchanged(std::move(nestedScan));
|
||||
|
||||
// ARB/NV spellings of lane-width-sensitive builtins must block the rewrite exactly
|
||||
// like their KHR counterparts.
|
||||
String arbSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
arbSubgroupBuiltin.insert(arbSubgroupBuiltin.find("float importance"),
|
||||
"uint arbLane = gl_SubGroupInvocationARB;\n ");
|
||||
expectUnchanged(std::move(arbSubgroupBuiltin));
|
||||
|
||||
String arbBallotCall = MakeLinearSubgroupPrefixScanShader();
|
||||
arbBallotCall.insert(arbBallotCall.find("float importance"),
|
||||
"uint64_t arbMask = ballotARB(true);\n ");
|
||||
expectUnchanged(std::move(arbBallotCall));
|
||||
|
||||
String nvWarpBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
nvWarpBuiltin.insert(nvWarpBuiltin.find("float importance"),
|
||||
"uint warpSize = gl_WarpSizeNV;\n ");
|
||||
expectUnchanged(std::move(nvWarpBuiltin));
|
||||
|
||||
String nvShuffleCall = MakeLinearSubgroupPrefixScanShader();
|
||||
nvShuffleCall.insert(nvShuffleCall.find("float importance"),
|
||||
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
|
||||
expectUnchanged(std::move(nvShuffleCall));
|
||||
}
|
||||
|
||||
// The LEXICAL half must fire at the source level (before the parse) for the
|
||||
// preempt-list names - the end-to-end ESSL tests cannot tell which half did the
|
||||
// rename, and for these names the parse would fail without the source rewrite.
|
||||
@@ -2448,9 +2301,6 @@ TEST_F(ProgramUtilTest, CompileEnvFingerprintTracksEveryInput) {
|
||||
otherExtensions.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64);
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(otherExtensions), baseline);
|
||||
|
||||
CompileEnv otherQuirk = base;
|
||||
otherQuirk.subgroupPrefixScanQuirk = MobileGL::MG_Config::QuirkOverride::ForceOn;
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(otherQuirk), baseline);
|
||||
}
|
||||
|
||||
// The no-backend fallback must stay exactly what the pipeline used to do inline:
|
||||
@@ -2849,16 +2699,6 @@ vec4 helperTint() { return vec4(1.0); }
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
struct SpirvValidationScope {
|
||||
bool previous;
|
||||
explicit SpirvValidationScope(bool enabled)
|
||||
: previous(MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(enabled);
|
||||
}
|
||||
~SpirvValidationScope() {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(previous);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBinary) {
|
||||
@@ -2880,7 +2720,7 @@ TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBina
|
||||
<< "entry-point-with-calls shape it exists for";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
|
||||
const SpirvVariableCensus after = TakeVariableCensus(optimized);
|
||||
EXPECT_EQ(after.inputCount, 1u)
|
||||
@@ -2918,7 +2758,7 @@ void main() {
|
||||
ASSERT_GE(before.outputCount, 3u);
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_EQ(TakeVariableCensus(optimized).outputCount, before.outputCount)
|
||||
<< "a declared-but-unwritten output was deleted; a fragment stage reading it now "
|
||||
<< "fails to link (ES) or breaks the Vulkan stage interface";
|
||||
@@ -2977,17 +2817,15 @@ void main() {
|
||||
// succeeds - fail-open call sites downstream must not see a different world),
|
||||
// and the failure latch is the signal. This is the catch that took a device
|
||||
// bisect to find when the validator was off everywhere.
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_GT(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "an invalid optimized module must bump the validation-failure latch";
|
||||
}
|
||||
{
|
||||
// The shipping configuration: same result, no validation, latch untouched.
|
||||
SpirvValidationScope validationOff(false);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, false));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
}
|
||||
@@ -3057,10 +2895,9 @@ void main() {
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_GE(CountRectImageTypes(raw), 1u) << "glslang no longer emits Dim::Rect for sampler2DRect";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "a rectangle module must leave the chain valid, not latched as a failure";
|
||||
@@ -3085,10 +2922,9 @@ void main() {
|
||||
ASSERT_TRUE(AnyLocationOnUniformStorage(raw))
|
||||
<< "glslang no longer keeps the explicit uniform location; the strip pass may be obsolete";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the stripped module must validate clean";
|
||||
@@ -3185,11 +3021,10 @@ void main() {
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3226,11 +3061,10 @@ void main() {
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw))
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3270,11 +3104,10 @@ void main() {
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3313,7 +3146,7 @@ void main() {
|
||||
ASSERT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
EXPECT_EQ(legalized, raw) << "the module must not be rewritten - not even re-serialized - when "
|
||||
"nothing indexes a fragment output dynamically";
|
||||
}
|
||||
@@ -3563,11 +3396,10 @@ TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate)
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
|
||||
<< "the shared chain must leave the 1D-array image for this pass to handle";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
|
||||
@@ -3615,11 +3447,10 @@ void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1,
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
@@ -3649,7 +3480,7 @@ void main() { ssb.sum = imageLoad(i0, 2).r; }
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
@@ -3673,7 +3504,7 @@ void main() { fragColor = texture(uTex, vUv); }
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
|
||||
}
|
||||
|
||||
@@ -3697,7 +3528,7 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
|
||||
<< "the fixture must contain the shape the pass declines";
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
|
||||
<< "declining means the 1D-array type is still there for the driver to reject";
|
||||
@@ -3748,11 +3579,10 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
|
||||
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
|
||||
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
@@ -3813,7 +3643,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
|
||||
Vector<Uint32> baked;
|
||||
// Even asked to, with a format of the right component class.
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
|
||||
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
|
||||
}
|
||||
@@ -3835,11 +3665,10 @@ void main() { writeIt(uni_image); }
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
|
||||
"not partly rewritten:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
@@ -3861,18 +3690,17 @@ void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
|
||||
// ...and the same image with a float bind format is baked, so the decline above is about the
|
||||
// class and not about the pass refusing float images.
|
||||
Vector<Uint32> bakedFloat;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat, true));
|
||||
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
|
||||
}
|
||||
|
||||
@@ -3896,12 +3724,11 @@ void main() {
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
|
||||
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
|
||||
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
|
||||
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
|
||||
@@ -3934,11 +3761,10 @@ void main() {
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
|
||||
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
|
||||
@@ -3963,11 +3789,10 @@ void main() {
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
@@ -3993,7 +3818,7 @@ void main() { fragColor = texture(uni_sampler, vUv); }
|
||||
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#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;
|
||||
|
||||
@@ -119,6 +120,56 @@ 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 {
|
||||
@@ -448,6 +499,178 @@ 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
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Types.h>
|
||||
@@ -364,6 +365,13 @@ 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;
|
||||
@@ -402,6 +410,9 @@ 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;
|
||||
@@ -427,6 +438,10 @@ 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;
|
||||
@@ -441,6 +456,8 @@ 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) {
|
||||
@@ -710,6 +727,37 @@ TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||
EXPECT_TRUE(backend.GetDynamicParameters().SubgroupQuadOperationsInAllStages);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, CapabilityRefreshInvalidatesTheCachedCompileEnvironment) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
auto backend = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
|
||||
auto* backendPtr = backend.get();
|
||||
MG_Backend::pActiveBackendObject = Move(backend);
|
||||
|
||||
const auto before = MG_State::pGLContext->GetCompileEnv();
|
||||
EXPECT_EQ(before->params.SubgroupSize, 0u);
|
||||
|
||||
MG_External::VulkanCapabilities caps;
|
||||
caps.SupportsShaderSubgroup = true;
|
||||
caps.SubgroupSize = 8;
|
||||
caps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
caps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
|
||||
backendPtr->ApplyVulkanCapabilitiesForTesting(caps);
|
||||
|
||||
const auto after = MG_State::pGLContext->GetCompileEnv();
|
||||
EXPECT_NE(after.get(), before.get());
|
||||
EXPECT_NE(after->fingerprint, before->fingerprint);
|
||||
EXPECT_EQ(after->backend, BackendType::DirectVulkan);
|
||||
EXPECT_EQ(after->params.SubgroupSize, 8u);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, KeepsOptionalGpuShaderInt64BranchForVoxyQuadDecode) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -2355,3 +2403,13 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
|
||||
|
||||
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_WRITE_ONLY));
|
||||
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 3, GL_READ_WRITE));
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_READ_ONLY));
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY));
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# MobileGL - MobileGL/MG_Test/SelfTest/CMakeLists.txt
|
||||
|
||||
add_executable(
|
||||
DriverPostIterationRPWitnessTest
|
||||
DriverPostIterationRPWitnessTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DriverPostIterationRPWitnessTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
@@ -0,0 +1,188 @@
|
||||
// MobileGL - MobileGL/MG_Test/SelfTest/DriverPostIterationRPWitnessTest.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "MG_Util/SelfTest/DriverPostIterationRPWitness.h"
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
IterationRPWitnessOutput MakeValidWitness(std::uint32_t numSubgroups) {
|
||||
IterationRPWitnessOutput output{};
|
||||
output.magic = kIterationRPWitnessMagic;
|
||||
output.numSubgroups = numSubgroups;
|
||||
output.loopLength = ComputeIterationRPWitnessLoopLength(numSubgroups);
|
||||
output.seenSubgroupMask =
|
||||
numSubgroups == kIterationRPWitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
|
||||
|
||||
// Valid test layouts use equal contiguous groups of the indexed
|
||||
// 1..512 input. The compact witness only needs their independent sums.
|
||||
const std::uint32_t subgroupSize = kIterationRPWitnessInvocationCount / numSubgroups;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
const std::uint32_t first = subgroup * subgroupSize + 1u;
|
||||
const std::uint32_t last = first + subgroupSize - 1u;
|
||||
output.lastLaneWriterCount[subgroup] = 1u;
|
||||
output.indexedInputTotal[subgroup] = subgroupSize * (first + last) / 2u;
|
||||
output.rawPrefix[subgroup] = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
|
||||
}
|
||||
output.owner511 = {subgroupSize, numSubgroups, numSubgroups - 1u, subgroupSize - 1u};
|
||||
|
||||
auto cache = output.rawPrefix;
|
||||
for (std::uint32_t scanStage = 0u; scanStage < output.loopLength; ++scanStage) {
|
||||
auto cacheAfterStage = cache;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
if ((subgroup & (1u << scanStage)) == 0u) continue;
|
||||
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
|
||||
cacheAfterStage[subgroup].x += cache[sourceCacheIndex].x;
|
||||
cacheAfterStage[subgroup].y += cache[sourceCacheIndex].y;
|
||||
}
|
||||
cache = cacheAfterStage;
|
||||
output.scanCache[scanStage] = cache;
|
||||
}
|
||||
output.finalAverage = {256.5f, 0.0f};
|
||||
return output;
|
||||
}
|
||||
|
||||
IterationRPWitnessLimits MakeSufficientLimits() {
|
||||
IterationRPWitnessLimits limits;
|
||||
limits.computeStageSupported = true;
|
||||
limits.basicSubgroupSupported = true;
|
||||
limits.arithmeticSubgroupSupported = true;
|
||||
limits.subgroupSize = 32u;
|
||||
limits.maxComputeWorkGroupInvocations = kIterationRPWitnessInvocationCount;
|
||||
limits.maxComputeWorkGroupSize = {32u, 16u, 1u};
|
||||
limits.maxComputeSharedMemorySize = kIterationRPWitnessSharedMemoryBytes;
|
||||
limits.maxPerStageDescriptorStorageBuffers = 1u;
|
||||
limits.maxDescriptorSetStorageBuffers = 1u;
|
||||
limits.maxBoundDescriptorSets = 1u;
|
||||
limits.maxStorageBufferRange = sizeof(IterationRPWitnessOutput);
|
||||
return limits;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, ValidTwoSubgroupWitness) {
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(MakeValidWitness(2u));
|
||||
ASSERT_TRUE(validation.ok) << validation.detail;
|
||||
EXPECT_EQ(validation.detail, "N=2, owner511=id1/lane255, 2 scan stages, average=(256.5,0)");
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, ValidThirtyTwoSubgroupWitness) {
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(MakeValidWitness(32u));
|
||||
ASSERT_TRUE(validation.ok) << validation.detail;
|
||||
EXPECT_EQ(validation.detail, "N=32, owner511=id31/lane15, 6 scan stages, average=(256.5,0)");
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, RejectsNonuniformNumSubgroups) {
|
||||
IterationRPWitnessOutput output = MakeValidWitness(16u);
|
||||
output.topologyFlags |= IterationRPWitnessNonuniformNumSubgroups;
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::Topology);
|
||||
EXPECT_NE(validation.detail.find("gl_NumSubgroups differed"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, RejectsMissingAndOutOfRangeSubgroupIds) {
|
||||
IterationRPWitnessOutput missing = MakeValidWitness(16u);
|
||||
missing.seenSubgroupMask &= ~(1u << 7u);
|
||||
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(missing);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_NE(validation.detail.find("seen subgroup-ID mask"), std::string::npos);
|
||||
|
||||
IterationRPWitnessOutput outOfRange = MakeValidWitness(16u);
|
||||
outOfRange.topologyFlags |= IterationRPWitnessInvalidSubgroupId;
|
||||
validation = ValidateIterationRPWitness(outOfRange);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_NE(validation.detail.find("invalid gl_SubgroupID"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, RejectsInvalidMultipleAndMissingLastLaneWriters) {
|
||||
IterationRPWitnessOutput invalidLane = MakeValidWitness(16u);
|
||||
invalidLane.topologyFlags |= IterationRPWitnessInvalidSubgroupLane;
|
||||
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(invalidLane);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_NE(validation.detail.find("invalid subgroup lane"), std::string::npos);
|
||||
|
||||
IterationRPWitnessOutput multiple = MakeValidWitness(16u);
|
||||
multiple.lastLaneWriterCount[4] = 2u;
|
||||
validation = ValidateIterationRPWitness(multiple);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_NE(validation.detail.find("subgroup 4 has 2 source last-lane writers"), std::string::npos);
|
||||
|
||||
IterationRPWitnessOutput missing = MakeValidWitness(16u);
|
||||
missing.lastLaneWriterCount[6] = 0u;
|
||||
validation = ValidateIterationRPWitness(missing);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_NE(validation.detail.find("subgroup 6 has 0 source last-lane writers"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, ReportsEarliestCorruptSourceScanStage) {
|
||||
IterationRPWitnessOutput output = MakeValidWitness(32u);
|
||||
output.scanCache[0][1].x += 1.0f;
|
||||
output.scanCache[3][5].x += 1.0f;
|
||||
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::SourceScan);
|
||||
EXPECT_EQ(validation.scanStage, 0u);
|
||||
EXPECT_NE(validation.detail.find("source scan stage 0, subgroup 1"), std::string::npos);
|
||||
|
||||
output = MakeValidWitness(32u);
|
||||
output.scanCache[3][5].x += 1.0f;
|
||||
validation = ValidateIterationRPWitness(output);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::SourceScan);
|
||||
EXPECT_EQ(validation.scanStage, 3u);
|
||||
EXPECT_NE(validation.detail.find("source scan stage 3, subgroup 5"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, RejectsOwner511OutsideHighestFinalLane) {
|
||||
IterationRPWitnessOutput output = MakeValidWitness(16u);
|
||||
output.owner511.z = 14u;
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::FinalOwner);
|
||||
EXPECT_NE(validation.detail.find("not in the highest subgroup"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, RejectsIncorrectVectorFinalAverage) {
|
||||
IterationRPWitnessOutput output = MakeValidWitness(16u);
|
||||
output.finalAverage.y = 1.0f;
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
EXPECT_FALSE(validation.ok);
|
||||
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::FinalAverage);
|
||||
EXPECT_NE(validation.detail.find("final average"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DriverPostIterationRPWitnessTest, MissingNativeFeatureIsTheOnlySkipCondition) {
|
||||
for (const auto toggleMissingFeature : {0u, 1u, 2u}) {
|
||||
IterationRPWitnessLimits limits = MakeSufficientLimits();
|
||||
if (toggleMissingFeature == 0u) limits.computeStageSupported = false;
|
||||
if (toggleMissingFeature == 1u) limits.basicSubgroupSupported = false;
|
||||
if (toggleMissingFeature == 2u) limits.arithmeticSubgroupSupported = false;
|
||||
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet)
|
||||
<< eligibility.detail;
|
||||
}
|
||||
|
||||
IterationRPWitnessLimits zeroSubgroupSize = MakeSufficientLimits();
|
||||
zeroSubgroupSize.subgroupSize = 0u;
|
||||
IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(zeroSubgroupSize);
|
||||
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
|
||||
|
||||
IterationRPWitnessLimits limits = MakeSufficientLimits();
|
||||
limits.maxComputeWorkGroupInvocations = 511u;
|
||||
eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
|
||||
|
||||
limits = MakeSufficientLimits();
|
||||
limits.maxStorageBufferRange = sizeof(IterationRPWitnessOutput) - 1u;
|
||||
eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -3,8 +3,14 @@ cmake_minimum_required(VERSION 3.14)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
// 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,6 +8,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -154,7 +155,6 @@ class DemoteFloat64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
|
||||
ASSERT_TRUE(DeclaresFloat64Capability(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
|
||||
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
|
||||
@@ -210,7 +210,7 @@ void main() {
|
||||
<< Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
|
||||
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
|
||||
@@ -241,7 +241,7 @@ void main() {
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
|
||||
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
|
||||
@@ -273,7 +273,7 @@ void main() {
|
||||
ASSERT_FALSE(before.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
|
||||
// churn at best, and a disagreement with glslang's own layout at worst.
|
||||
@@ -287,7 +287,7 @@ TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
|
||||
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
|
||||
// has to be gone: both sides are 32 bits now.
|
||||
@@ -307,7 +307,7 @@ void main() {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
|
||||
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
|
||||
@@ -326,7 +326,7 @@ void main() { gl_Position = inPos; }
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
|
||||
// builds) that such a run round-trips byte-identically.
|
||||
EXPECT_EQ(output, input);
|
||||
@@ -351,7 +351,7 @@ void main() {
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
|
||||
}
|
||||
@@ -362,7 +362,7 @@ TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
|
||||
|
||||
Vector<Uint32> demoted;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted, true));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
|
||||
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
|
||||
@@ -375,7 +375,7 @@ TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
|
||||
SpvcSession session(output, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
@@ -482,55 +482,168 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
|
||||
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// Asserted on the optimized module rather than through a driver, because that is where the
|
||||
// rewrite happens and its fingerprint there is unambiguous: the epsilon form introduces a
|
||||
// rewrite happens and its fingerprint there is unambiguous: the rewrite introduces a
|
||||
// GLSL.std.450 FAbs, and nothing else in these shaders would.
|
||||
namespace {
|
||||
Bool RewritesToAnEpsilonTest(const String& source) {
|
||||
String OptimizedDisassembly(const String& source) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
EXPECT_FALSE(input.empty());
|
||||
if (input.empty()) return false;
|
||||
if (input.empty()) return {};
|
||||
Vector<Uint32> output;
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
return Disassemble(output).find("FAbs") != String::npos;
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
return Disassemble(output);
|
||||
}
|
||||
|
||||
String CompareAgainst(const String& type, const String& literal) {
|
||||
Bool RewritesToAnAbsoluteValueTest(const String& source) {
|
||||
return OptimizedDisassembly(source).find("FAbs") != String::npos;
|
||||
}
|
||||
|
||||
String CompareAgainstUsing(const String& type, const String& op, 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 != " + literal + ") g_result = 1;\n"
|
||||
" if (g_0 " + op + " " + 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(RewritesToAnEpsilonTest(CompareAgainst("double", "1.0LF")))
|
||||
<< "a double compared against 1.0lf was rewritten into an epsilon test against zero";
|
||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "1.0LF")))
|
||||
<< "a double compared against 1.0lf was rewritten into a test against zero";
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
|
||||
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("double", "0.0LF")))
|
||||
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0LF")))
|
||||
<< "the rewrite must still fire for a genuine comparison against zero";
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
|
||||
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("float", "1.0")))
|
||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "1.0")))
|
||||
<< "a float compared against 1.0 must not be rewritten";
|
||||
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("float", "0.0")))
|
||||
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(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");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.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>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Uint32 FindBuiltinTarget(const Vector<Uint32>& spirv, spv::BuiltIn builtin) {
|
||||
Uint32 target = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpDecorate && wordCount >= 4u &&
|
||||
static_cast<spv::Decoration>(words[2]) == spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(words[3]) == builtin) {
|
||||
target = words[1];
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
Uint32 CountLoadsFrom(const Vector<Uint32>& spirv, Uint32 pointerId) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpLoad && wordCount >= 4u && words[3] == pointerId) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
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 Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
constexpr const char* kNumSubgroupsOnlySource = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
||||
void main() {
|
||||
if (gl_LocalInvocationIndex == 0u)
|
||||
outputData.value = gl_NumSubgroups;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kNoNumSubgroupsSource = R"(#version 450 core
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
||||
void main() {
|
||||
if (gl_LocalInvocationIndex == 0u)
|
||||
outputData.value = gl_WorkGroupSize.x;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, ReplacesBuiltinLoadAndSynthesizesSubgroupSize) {
|
||||
const Vector<Uint32> input = CompileCompute(kNumSubgroupsOnlySource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputNumSubgroups = FindBuiltinTarget(input, spv::BuiltIn::NumSubgroups);
|
||||
ASSERT_NE(inputNumSubgroups, 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(input, inputNumSubgroups), 1u);
|
||||
EXPECT_EQ(FindBuiltinTarget(input, spv::BuiltIn::SubgroupSize), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
|
||||
ASSERT_TRUE(Validates(output));
|
||||
|
||||
const Uint32 outputNumSubgroups = FindBuiltinTarget(output, spv::BuiltIn::NumSubgroups);
|
||||
const Uint32 outputSubgroupSize = FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize);
|
||||
ASSERT_NE(outputNumSubgroups, 0u);
|
||||
ASSERT_NE(outputSubgroupSize, 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(output, outputNumSubgroups), 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(output, outputSubgroupSize), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpCompositeExtract), 3u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpIMul), 2u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpUDiv), 1u);
|
||||
}
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, IsIdempotent) {
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(CompileCompute(kNumSubgroupsOnlySource), once, true));
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, LeavesUnrelatedComputeShaderUntouched) {
|
||||
const Vector<Uint32> input = CompileCompute(kNoNumSubgroupsSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
EXPECT_EQ(FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize), 0u);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.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>
|
||||
|
||||
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> CompileStage(GLenum stage, const String& source) {
|
||||
using namespace MobileGL::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();
|
||||
}
|
||||
|
||||
Uint32 CountGroupNonUniform(const Vector<Uint32>& spirv) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode >= spv::Op::OpGroupNonUniformElect && opcode <= spv::Op::OpGroupNonUniformQuadSwap) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Uint32 CountGroupNonUniformCapabilities(const Vector<Uint32>& spirv) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpCapability || wordCount < 2u) return;
|
||||
const auto capability = static_cast<spv::Capability>(words[1]);
|
||||
if (capability >= spv::Capability::GroupNonUniform &&
|
||||
capability <= spv::Capability::GroupNonUniformQuad) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
bool HasWorkgroupVariable(const Vector<Uint32>& spirv) {
|
||||
bool found = false;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpVariable && wordCount >= 4u &&
|
||||
static_cast<spv::StorageClass>(words[3]) == spv::StorageClass::Workgroup) {
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// One shader touching every lowered category: builtins, vote, arithmetic
|
||||
// scans, ballot math, shuffles, clustered and quad operations.
|
||||
constexpr const char* kEveryCategorySource = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_vote : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
#extension GL_KHR_shader_subgroup_ballot : require
|
||||
#extension GL_KHR_shader_subgroup_shuffle : require
|
||||
#extension GL_KHR_shader_subgroup_shuffle_relative : require
|
||||
#extension GL_KHR_shader_subgroup_clustered : require
|
||||
#extension GL_KHR_shader_subgroup_quad : require
|
||||
layout(local_size_x = 48, local_size_y = 1, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { float value[]; } outputData;
|
||||
void main() {
|
||||
uint slot = gl_LocalInvocationIndex * 24u;
|
||||
float v = float(gl_LocalInvocationIndex + 1u);
|
||||
outputData.value[slot + 0u] = float(gl_SubgroupSize);
|
||||
outputData.value[slot + 1u] = float(gl_NumSubgroups);
|
||||
outputData.value[slot + 2u] = float(gl_SubgroupID);
|
||||
outputData.value[slot + 3u] = float(gl_SubgroupInvocationID);
|
||||
outputData.value[slot + 4u] = float(gl_SubgroupEqMask.x + gl_SubgroupLtMask.x);
|
||||
outputData.value[slot + 5u] = subgroupElect() ? 1.0 : 0.0;
|
||||
outputData.value[slot + 6u] = subgroupAll(v > 0.0) ? 1.0 : 0.0;
|
||||
outputData.value[slot + 7u] = subgroupAny(v > 40.0) ? 1.0 : 0.0;
|
||||
outputData.value[slot + 8u] = subgroupAllEqual(gl_WorkGroupID.x) ? 1.0 : 0.0;
|
||||
outputData.value[slot + 9u] = subgroupAdd(v);
|
||||
outputData.value[slot + 10u] = subgroupInclusiveAdd(v);
|
||||
outputData.value[slot + 11u] = subgroupExclusiveMax(v);
|
||||
outputData.value[slot + 12u] = float(subgroupMin(gl_LocalInvocationIndex));
|
||||
uvec4 ballot = subgroupBallot((gl_LocalInvocationIndex & 1u) == 0u);
|
||||
outputData.value[slot + 13u] = float(subgroupBallotBitCount(ballot));
|
||||
outputData.value[slot + 14u] = float(subgroupBallotFindLSB(ballot));
|
||||
outputData.value[slot + 15u] = float(subgroupBallotFindMSB(ballot));
|
||||
outputData.value[slot + 16u] = subgroupInverseBallot(ballot) ? 1.0 : 0.0;
|
||||
outputData.value[slot + 17u] = subgroupBallotBitExtract(ballot, 3u) ? 1.0 : 0.0;
|
||||
outputData.value[slot + 18u] = subgroupBroadcast(v, 2u);
|
||||
outputData.value[slot + 19u] = subgroupBroadcastFirst(v);
|
||||
outputData.value[slot + 20u] = subgroupShuffle(v, gl_SubgroupInvocationID ^ 5u);
|
||||
outputData.value[slot + 21u] = subgroupShuffleXor(v, 1u) + subgroupShuffleUp(v, 1u) +
|
||||
subgroupShuffleDown(v, 1u);
|
||||
outputData.value[slot + 22u] = subgroupClusteredAdd(v, 4u);
|
||||
outputData.value[slot + 23u] = subgroupQuadBroadcast(v, 1u) + subgroupQuadSwapHorizontal(v);
|
||||
subgroupBarrier();
|
||||
subgroupMemoryBarrierShared();
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kNoSubgroupSource = R"(#version 450 core
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
||||
void main() {
|
||||
if (gl_LocalInvocationIndex == 0u) outputData.value = gl_WorkGroupSize.x;
|
||||
}
|
||||
)";
|
||||
|
||||
// An extended subgroup instruction (SPV_KHR_subgroup_rotate) alongside core
|
||||
// ones: outside the lowered set, so the pass must fail rather than emit
|
||||
// "subgroup-free" output that still rotates.
|
||||
constexpr const char* kRotateSource = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
#extension GL_KHR_shader_subgroup_rotate : require
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, binding = 0) buffer Output { float value[]; } outputData;
|
||||
void main() {
|
||||
float v = subgroupAdd(float(gl_SubgroupInvocationID));
|
||||
outputData.value[gl_LocalInvocationIndex] = subgroupRotate(v, 1u);
|
||||
}
|
||||
)";
|
||||
|
||||
// A 1024-invocation workgroup exchanging a vec4 and a float: the lowering
|
||||
// would need 16 KiB + 4 KiB of scratch, past the Vulkan-minimum shared
|
||||
// budget of 16384 bytes.
|
||||
constexpr const char* kScratchHungrySource = 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(std430, binding = 0) buffer Output { vec4 value[]; } outputData;
|
||||
void main() {
|
||||
vec4 wide = subgroupAdd(vec4(float(gl_LocalInvocationIndex)));
|
||||
wide.x += subgroupInclusiveAdd(float(gl_SubgroupInvocationID));
|
||||
outputData.value[gl_LocalInvocationIndex] = wide;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(EmulateSubgroupsPass, LowersEveryCategoryToSharedMemory) {
|
||||
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_GT(CountGroupNonUniform(input), 0u);
|
||||
ASSERT_GT(CountGroupNonUniformCapabilities(input), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, true));
|
||||
ASSERT_TRUE(Validates(output));
|
||||
|
||||
// The whole point: nothing subgroup-shaped survives, so the module runs on a
|
||||
// device with no subgroup support at all.
|
||||
EXPECT_EQ(CountGroupNonUniform(output), 0u);
|
||||
EXPECT_EQ(CountGroupNonUniformCapabilities(output), 0u);
|
||||
// The exchanges go through workgroup-shared scratch behind control barriers.
|
||||
EXPECT_TRUE(HasWorkgroupVariable(output));
|
||||
EXPECT_GT(CountOpcode(output, spv::Op::OpControlBarrier), CountOpcode(input, spv::Op::OpControlBarrier));
|
||||
}
|
||||
|
||||
TEST(EmulateSubgroupsPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, once, 16384u, true));
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(once, twice, 16384u, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
TEST(EmulateSubgroupsPass, LeavesSubgroupFreeComputeUntouched) {
|
||||
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kNoSubgroupSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(EmulateSubgroupsPass, RefusesExtendedSubgroupInstructions) {
|
||||
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kRotateSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, false));
|
||||
}
|
||||
|
||||
TEST(EmulateSubgroupsPass, RefusesAModuleOverTheScratchBudget) {
|
||||
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kScratchHungrySource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// vec4 scratch (1024 slots * 16 bytes) plus float scratch (4 KiB) exceeds
|
||||
// the 16 KiB Vulkan-minimum budget.
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, false));
|
||||
// A device advertising more shared memory takes the same module fine.
|
||||
Vector<Uint32> roomier;
|
||||
EXPECT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, roomier, 32768u, true));
|
||||
EXPECT_TRUE(Validates(roomier));
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPSubgroupScratchTest.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 <algorithm>
|
||||
#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();
|
||||
}
|
||||
|
||||
// The declared lengths of every Workgroup-storage array variable, sorted.
|
||||
std::vector<Uint32> WorkgroupArrayLengths(const Vector<Uint32>& spirv) {
|
||||
std::map<Uint32, Uint32> constantValues; // constant id -> value
|
||||
std::map<Uint32, Uint32> arrayLengthIds; // array type id -> length constant id
|
||||
std::map<Uint32, Uint32> pointerPointees; // pointer type id -> pointee type id
|
||||
std::vector<Uint32> workgroupPointerTypes; // type ids of Workgroup variables
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpConstant:
|
||||
if (wordCount >= 4u) constantValues[words[2]] = words[3];
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
if (wordCount >= 4u) arrayLengthIds[words[1]] = words[3];
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (wordCount >= 4u &&
|
||||
static_cast<spv::StorageClass>(words[2]) == spv::StorageClass::Workgroup) {
|
||||
pointerPointees[words[1]] = words[3];
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
if (wordCount >= 4u &&
|
||||
static_cast<spv::StorageClass>(words[3]) == spv::StorageClass::Workgroup) {
|
||||
workgroupPointerTypes.push_back(words[1]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
std::vector<Uint32> lengths;
|
||||
for (const Uint32 pointerTypeId : workgroupPointerTypes) {
|
||||
const auto pointee = pointerPointees.find(pointerTypeId);
|
||||
if (pointee == pointerPointees.end()) continue;
|
||||
const auto lengthId = arrayLengthIds.find(pointee->second);
|
||||
if (lengthId == arrayLengthIds.end()) continue;
|
||||
const auto value = constantValues.find(lengthId->second);
|
||||
if (value != constantValues.end()) lengths.push_back(value->second);
|
||||
}
|
||||
std::sort(lengths.begin(), lengths.end());
|
||||
return lengths;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
#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 prefixSumCache[32];
|
||||
shared float plainScratch[4];
|
||||
void main() {
|
||||
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex), 0.0);
|
||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
plainScratch[gl_LocalInvocationIndex & 3u] = sampleLuminance.x;
|
||||
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) {
|
||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
outputData.value = prefixSumCache[0].x / 512.0 + plainScratch[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// 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
|
||||
#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 value; } outputData;
|
||||
shared float prefixSumCache[64];
|
||||
void main() {
|
||||
float importance = float(gl_LocalInvocationID.x) * 0.5;
|
||||
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.value = prefixSumCache[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// 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
|
||||
#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];
|
||||
void main() {
|
||||
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
|
||||
perInvocation[gl_LocalInvocationIndex & 31u] = 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;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(FixIterationRPSubgroupScratchPass, GrowsTheExposureScratchForNarrowSubgroups) {
|
||||
const Vector<Uint32> input = CompileCompute(kExposureShapedSource);
|
||||
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));
|
||||
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);
|
||||
ASSERT_FALSE(input.empty());
|
||||
for (const Uint32 nativeSize : {16u, 32u, 64u}) {
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||
input, output, nativeSize, 32768u, 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, 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);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,6 @@ class FlattenXfbInterfaceBlocksTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMe
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_EQ(flattened, (std::set<String>{"StageData"}));
|
||||
|
||||
@@ -145,7 +144,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABloc
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after;
|
||||
@@ -162,7 +161,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations)
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
@@ -184,7 +183,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) {
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(
|
||||
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output));
|
||||
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output, true));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
|
||||
const String after = Transpile(output);
|
||||
@@ -200,7 +199,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) {
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output));
|
||||
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output, true));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
EXPECT_TRUE(output.empty());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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,6 +18,11 @@
|
||||
// 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.
|
||||
//
|
||||
@@ -33,10 +38,12 @@
|
||||
#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>
|
||||
@@ -470,4 +477,114 @@ 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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user